您的位置:首页 > 移动开发 > IOS开发

Outlet in iOS and MacOS

2013-04-05 13:27 211 查看


Outlets - Property vs. Instance Variable

When we were writing our iPhone programming book, all of Apple's sample code and documentation
used the IBOutlet keyword exactly how we had always used it in Cocoa for Mac OS X, placed in front of the instance variable declaration, like so

#import <UIKit/UIKit.h>

@interface MyViewController : UIViewController {
IBOutlet UILabel *myLabel;

}
@property (nonatomic, retain) UILabel *myLabel;
@end


Somewhere along the line, I'm not exactly sure when, they started putting the IBOutlet modifier in the property declaration instead, like so:

#import <UIKit/UIKit.h>

@interface MyViewController : UIViewController {
UILabel *myLabel;

}
@property (nonatomic, retain) IBOutlet UILabel *myLabel;
@end


Now, in practice, there's absolutely no difference in the way a controller class functions based on where you place the IBOutlet keyword. I've used both approaches, even mingled within the same class, and have noticed
no difference. That wasn't enough for me, though; I wanted to know if there was any underlying difference between the two - any difference in how the connections were made.

You see, early on, about the time of SDK2, I recall somebody saying that they had tested it and the mutators for their IBOutlets weren't getting called when the outlet connections were made. This came as a surprise
because the sample source code was using properties for all IBOutlets, even though that wasn't the way it had been done in Cocoa on the Mac, and if the mutators weren't getting used, there wouldn't be any reason to
declare a property for the outlets.

This made me wonder if, perhaps, Interface Builder only used the mutator if you put the IBOutlet keyword on the property declaration line but not if you put it preceding the instance variable declaration. There seemed
to be a logic to that, given Apple's change in the preferred placement of the IBOutlet keyword from the instance variable to the property. If that's the case, then it would be important to know, because you could
get slight differences in behavior in some rare circumstances based on which you used.

Wondering should lead to exploration, so I decided to find out. It's easy enough to test - I created an iPhone application project with two labels on a view, and two outlets to be connected to those two labels. One of the outlets was given the IBOutlet keyword
preceding the instance variable declaration, the other was given the IBOutlet declaration as part of the property. In both cases, I implemented the mutator methods manually so that they would print a statement to
the log whenever they were called in addition to doing the assignment.


And the Result?

It appears that where you place the IBOutlet keyword, on all current versions of the SDK (2.0, 2.1, 2.2), is purely stylistic and has no impact on what's happening under the hood. For both outlets, the mutator was
called when the outlets were connected during application startup.

So, it seems that you can use whichever keyword placement you prefer, although Apple definitely seems to be going with placing the IBOutlet in the property declaration for all their new sample code and documentation
code listings, so unless you feel strongly that it should be on the instance variable, that's probably the best place to put it, and probably where we'll put it if we ever publish a second edition.

POSTED BY JEFF LAMARCHE AT 8:48
AM
LABELS: COCOA TOUCH, IPHONE
SDK, OBJECTIVE-C, PROPERTIES


THURSDAY, OCTOBER 14, 2010


Outlets, Cocoa vs. Cocoa Touch

I almost always follow Apple's lead on Cocoa and Cocoa Touch conventions. I figure that by the time outside developers like me see something for the first time, Apple engineers have been living with that thing for many months, so they've likely got a much better
idea than I do about the best way to use that new thing.

But, after spending time with their stuff, sometimes — not often, but sometimes — I disagree with what appears to be Apple's recommended "best practice" for doing something. I think I've come to the decision that the IBOutlet behavior
in iOS is one of these areas.

If you look at Apple's documentation snippets and sample code, you find that they almost always retainIBOutlet properties, like:

#import <UIKit/UIKit.h>

@interface FooView : UIView
{

}
@synthesize (nonatomic, retain) IBOutlet UIButton button;
@synthesize (nonatomic, retain) IBOutlet UITextField textField;
@synthesize (nonatomic, retain) IBOutlet UIImageView imageView;
@end


There's a good reason for this. In iOS, the documentation explicitly states that you need to retain all outlets because the bundle loader for iOS autoreleases all objects created as a result of loading a nib.

Objects in the nib file are created with a retain count of 1 and then autoreleased. As it rebuilds the object hierarchy, however, UIKit reestablishes connections between the objects using the setValue:forKey: method, which uses the available setter method
or retains the object by default if no setter method is available. If you define outlets for nib-file objects, you should always define a setter method (or declared property) for accessing that outlet. Setter methods for outlets should retain their values,
and setter methods for outlets containing top-level objects must retain their values to prevent them from being deallocated. If you do not store the top-level objects in outlets, you must retain either the array returned by the loadNibNamed:owner:options:
method or the objects inside the array to prevent those objects from being released prematurely.

This is different from Cocoa on the Mac, where it wasn't necessary to retain outlets and people rarely did. In fact, we didn't usually bother with accessor or mutator methods for outlets (it was just unnecessary extra
typing in most cases), we just put the IBOutlet keyword in front of the instance variable and the nib loader was happy to attach our outlets like that, retaining the objects that needed retaining.

The behavior under Cocoa/Mac is not actually to retain everything in the nib, but rather, to retain any object that doesn't have a parent object to retain it. So, in other words, if an object in a nib will be retained by something else, like a superview,
the nib loader doesn't bother to retain it again. But, if it doesn't, the bundle loader retains it so that it doesn't get deallocated.

This is a logical approach and, in fact, was necessary back in the pre-Objective-C 2.0 days because outlets back then were just iVars and there was no easy way for the controller class to retain objects that needed to be retained.

I have to wonder why they would change the fundamental behavior of a foundation object like NSBundlebetween Mac OS and iOS? NSBundle is not part of Cocoa or Cocoa Touch, it's part of Foundation, and the whole
point of Foundation is to have common objects between the different operating systems.

I wrote a small project to test if the Bundle Loader really did behave differently, as documented, by using an instance of UIView in
the nib with no superview. Sure enough, when I didn't retain the outlet, I either got an EXC_BAD_ACCESS or a different object altogether when I printed the outlet to NSLog().
The difference is real. The bundle loader on the Mac will retain outlets for you if they need to be retained, which allows you to continue using instance variables, or properties with the assign keyword. This means
you don't have to release your outlets in dealloc and you don't have to mess around with anything likeviewDidUnload on iOS.

The bundle loader on the iOS, on the other hand, does not retain anything for you, so if an object does not have a parent object to retain it, you have to retain it in your controller class or you will end up with an invalid outlet.

I really don't see the value in changing this behavior. I'm guessing the decision was made for the sake of memory efficiency in the early days of the iPhone. The idea being that you might load a nib with object instances that you aren't actually using, and
with the old behavior, those would take up memory as long as the nib was loaded. That doesn't necessarily sound like a good idea on an embedded device with no virtual memory and 128 megs of RAM, which is what the original iPhone and iPhone 3G had.

Despite that, I think the cure here is worse than the disease. If you don't remember to release your outlets in viewDidUnload (which, if I remember right, we couldn't even do in the 2.0 version of the SDK), your outlets
will continue to use up memory after the nib is unloaded, obviating any advantage of the lazy loading. Essentially, it's more fragile, because it depends on the programmer doing the right thing and there are few if any situations where a programmer would need
to not do the right thing.

By virtue of the bundle loader not retaining outlets, it also requires more rote, boilerplate code to be written in every controller class in every iOS application, yet it runs just as much of a risk of unnecessary memory use, arguably a greater risk. In other
words, the cure is no better than the disease.

iPhones are getting more robust and less memory constrained with every new device that comes out. I would argue that it's already time (or, at very least, soon will be time), to bring the behavior of the two bundle loaders together. If they are brought together,
they should be brought together using the old Cocoa behavior not the new iOS behavior. When you think of the number of people coding for the iOS now, those extra required dealloc and viewDidUnload lines
in every single controller class in every single iOS application are really adding up to a lot of engineering hours lost on boilerplate.

A few weeks ago, I started experimenting with using assign instead of retain for IBOutlets except in cases where the outlet's object
didn't have a superview or another object retaining it. If an outlet's not a view or control, then I also use retain. In essence, I'm mimicking the old behavior of the nib in designing my controller classes.

This has led to a lot less typing and less code to maintain because 95% or more of the outlets I create are connected to objects retained by their superview during the entire existence of the controller class.

Now, I'm not necessarily saying you should do what I'm doing. It can be tricky, at times, remembering which objects need to be retained and they can be hard to debug if you get it wrong. Apple has made a recommendation with good reason and I don't think you
should disregard that recommendation lightly. That being said, if you're comfortable enough with Objective-C memory management and the bundle loader to be able to distinguish when a nib object will be automatically retained by something else, you could save
yourself a fair bit of typing over time.

I normally try to embrace changes Apple makes, but in this case, I just can't convince myself that this was a good change. The old nib behavior of retaining only things that need retaining has been in use for over 20 years, dating back to when desktop computers
were less powerful than our iPhones are, and there doesn't appear to be any practical advantage to the change. On the other hand, we'd all benefit from going back to the old Mac OS bundle behavior because we'd have less make-work to do when setting up a controller
class. There's also little danger in changing this behavior because code that follows Apple's current recommendations would continue to work correctly.

POSTED BY JEFF LAMARCHE AT 8:46
AM
LABELS: COCOA, COCOA
TOUCH, OBJECTIVE-C, RANT


MONDAY, MAY 17, 2010


Custom Alert Views

Quite some time ago, I posted a way to accept text input using an alert view. The problem with that
technique is that it relies on one of Apple's private APIs. I've had many people ask me how to use that technique without getting rejected in the review process. The short answer is: you can't.

What you can do, however, is create your own class that simulates the behavior of UIAlertView. You can find a sample implementation of the technique discussed in this post by downloading
this project. In this simple example, we're going to create an alert view that lets the user enter a value into a single text field. The same technique can be used to present any view that you can build in Interface Builder. From a programming perspective,
you will not get rejected for using private APIs if you use this technique, but be aware that you can still get rejected for HIG violations, so make sure you're familiar
with the HIG.

There are many ways to mimic the behavior of the UIAlertView. One of the most common ways I've seen is to simply design an alert view in the nib of the view controller that needs to present it. This works, but it's
a bit messy and pretty much completely non-reusable.

A better approach is to design the alert view as its own view controller and nib pair. We can even model it after UIAlertView's fire-and-forget approach so that the calling code can look exactly like code to create
and display a UIAlertView. Before we get started, we need a few resources.

You've probably noticed that when you use an alert view, the active view grays out a bit. I don't know for sure how Apple has accomplished this, but we can simulate the behavior using a PNG image with a circular gradient that goes from 60% opaque black to 40%
opaque black:



We also need a background image for the alert view. Here's one I hacked out in Pixelmator


For your own alerts, you might need to resize or turn this into a stretchable image. For simplicity's sake, I just made it the size I want it. We also need buttons. Again, in a real example, you might want to turn these into stretchable images, but I just kept
things simple by making the image the size I wanted it:



The next thing we need is some animation code. We could, of course, create the CAAnimation instances right in our class, but I'm a big fan of both code reuse and Objective-C categories, so instead of doing that, I've
created a category on UIView that will handle the two animations we need. One animation "pops in" a view and will be used to show the alert view, the other fades in a view and will be used for the gray background
image. The two animations happen at the same time so that when the alert has fully popped into view, the background view is grayed out.

The keyframe timings on the "pop in" animation code are not quite a 100% match for Apple's animation, so if anybody wants to tweak the keyframe values to make a closer match, I'd be happy to update the code with the "correct" values. Here is the category I
created to hold the animations:

UIView-AlertAnimations.h

#import <Foundation/Foundation.h>

@interface UIView(AlertAnimations)
- (void)doPopInAnimation;
- (void)doPopInAnimationWithDelegate:(id)animationDelegate;
- (void)doFadeInAnimation;
- (void)doFadeInAnimationWithDelegate:(id)animationDelegate;
@end


UIView-AlertAnimations.m

#import "UIView-AlertAnimations.h"
#import <QuartzCore/QuartzCore.h>

#define kAnimationDuration  0.2555

@implementation UIView(AlertAnimations)
- (void)doPopInAnimation
{
[self doPopInAnimationWithDelegate:nil];
}
- (void)doPopInAnimationWithDelegate:(id)animationDelegate
{
CALayer *viewLayer = self.layer;
CAKeyframeAnimation* popInAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];

popInAnimation.duration = kAnimationDuration;
popInAnimation.values = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.6],
[NSNumber numberWithFloat:1.1],
[NSNumber numberWithFloat:.9],
[NSNumber numberWithFloat:1],
nil];
popInAnimation.keyTimes = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:0.6],
[NSNumber numberWithFloat:0.8],
[NSNumber numberWithFloat:1.0],
nil];
popInAnimation.delegate = animationDelegate;

[viewLayer addAnimation:popInAnimation forKey:@"transform.scale"];
}
- (void)doFadeInAnimation
{
[self doFadeInAnimationWithDelegate:nil];
}
- (void)doFadeInAnimationWithDelegate:(id)animationDelegate
{
CALayer *viewLayer = self.layer;
CABasicAnimation *fadeInAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
fadeInAnimation.fromValue = [NSNumber numberWithFloat:0.0];
fadeInAnimation.toValue = [NSNumber numberWithFloat:1.0];
fadeInAnimation.duration = kAnimationDuration;
fadeInAnimation.delegate = animationDelegate;
[viewLayer addAnimation:fadeInAnimation forKey:@"opacity"];
}
@end


As you can see, I've provided two versions of each animation, one that accepts an animation delegate and one that doesn't. This allows the calling code to set an animation delegate so it can be notified of things like when the animation finishes. We'll use
a delegate for one of our animations, but we might as well have the option with both.

The next thing we need to do is create the view controller header, implementation, and nib files. In my sample code, I've named the class CustomAlertView. It may seem odd that I giving a view controller class a name
that makes it sound like a view instead of something like CustomAlertViewController. You can feel free to name yours however you wish, but this controller class will actually mimic the behavior of UIAlertViewand
I wanted the name to reflect that. I debated with myself over the name for a bit, but ultimately decided that CustomAlertView felt better since the name would provide a clue as to how to use it. If it feels dirty
to you to "lie" in the class name, then by all means, name yours differently.

We're going to need some action methods and some outlets so that our controller and nib file can interact. One outlet will be for the background image, another for the alert view itself, and one for the text field. The latter is needed so we can tell the text
field to accept and resign first responder status. We'll use a single action method for both of the buttons on the alert, and we'll use the button's tag value to differentiate between the two buttons.

Because we're implementing fire-and-forget, we also need to define a protocol containing the methods the delegate can implement. Since the alert is designed to accept input, I've made the delegate method used to receive the input @required,
but the other method (called only when cancelled) @optional. The enumhere is just to make code more readable when it comes to dealing with the button tag values.

CustomAlertView.h

#import <UIKit/UIKit.h>

enum
{
CustomAlertViewButtonTagOk = 1000,
CustomAlertViewButtonTagCancel
};

@class CustomAlertView;

@protocol CustomAlertViewDelegate
@required
- (void) CustomAlertView:(CustomAlertView *)alert wasDismissedWithValue:(NSString *)value;

@optional
- (void) customAlertViewWasCancelled:(CustomAlertView *)alert;
@end

@interface CustomAlertView : UIViewController <UITextFieldDelegate>
{
UIView                                  *alertView;
UIView                                  *backgroundView;
UITextField                             *inputField;

id<NSObject, CustomAlertViewDelegate>   delegate;
}
@property (nonatomic, retain) IBOutlet  UIView *alertView;
@property (nonatomic, retain) IBOutlet  UIView *backgroundView;
@property (nonatomic, retain) IBOutlet  UITextField *inputField;

@property (nonatomic, assign) IBOutlet id<CustomAlertViewDelegate, NSObject> delegate;
- (IBAction)show;
- (IBAction)dismiss:(id)sender;
@end


Once the header file is completed and saved, we can skip over to Interface Builder and create our interface. I won't walk you through the process of building the interface, but I'll point out the important things. Here's what the view will looks like in Interface
Builder:



Some important things:

The content view needs to be NOT opaque and its background color should be set to a white with 0% opacity. The view's alpha should be 1.0. Alpha is inherited by subviews, background color is not, so by making it transparent using background color, we'll
be able to see all of the subviews. If we had set alpha to 0.0 instead, we wouldn't be able see the alert view;
The background view is a UIImageView that is the same size as the content view. Its autosize attributes are set so that it resizes with the content view and it is connected to the backgroundViewoutlet.
It needs to not be opaque and its alpha needs to be set to 1.0. Even though we will be animating the alpha, we want the nib to reflect the final value;
All of the UI elements that make up the actual alert are all subviews of a single instance of UIViewwhose background color is also set to white with 0% opacity and is NOT opaque so that it is invisible. This view
is used just to group the elements so they can be animated together and is what the alertView outlet will point to;
The alert view is not centered. In this case, we want it centered in the space remaining above the keyboard;
The OK button has its tag set to 1,000 in the attribute inspector. The Cancel button has its tag set to 1,001. These numbers match the values in the enum we created in the header file
File's Owner is the delegate of the text field. This allows the controller to be notified when the user hits the return key on the keyboard

Once the interface is created, all that's left to do is implement the controller class. Here is the implementation; I'll explain what's going on in a moment:

CustomAlertView.m

#import "CustomAlertView.h"
#import "UIView-AlertAnimations.h"
#import <QuartzCore/QuartzCore.h>

@interface CustomAlertView()
- (void)alertDidFadeOut;
@end

@implementation CustomAlertView
@synthesize alertView;
@synthesize backgroundView;
@synthesize inputField;
@synthesize delegate;
#pragma mark -
#pragma mark IBActions
- (IBAction)show
{
// Retaining self is odd, but we do it to make this "fire and forget"
[self retain];

// We need to add it to the window, which we can get from the delegate
id appDelegate = [[UIApplication sharedApplication] delegate];
UIWindow *window = [appDelegate window];
[window addSubview:self.view];

// Make sure the alert covers the whole window
self.view.frame = window.frame;
self.view.center = window.center;

// "Pop in" animation for alert
[alertView doPopInAnimationWithDelegate:self];

// "Fade in" animation for background
[backgroundView doFadeInAnimation];
}
- (IBAction)dismiss:(id)sender
{
[inputField resignFirstResponder];
[UIView beginAnimations:nil context:nil];
self.view.alpha = 0.0;
[UIView commitAnimations];

[self performSelector:@selector(alertDidFadeOut) withObject:nil afterDelay:0.5];

if (sender == self || [sender tag] == CustomAlertViewButtonTagOk)
[delegate CustomAlertView:self wasDismissedWithValue:inputField.text];
else
{
if ([delegate respondsToSelector:@selector(customAlertViewWasCancelled:)])
[delegate customAlertViewWasCancelled:self];
}
}
#pragma mark -
- (void)viewDidUnload
{
[super viewDidUnload];
self.alertView = nil;
self.backgroundView = nil;
self.inputField = nil;
}
- (void)dealloc
{
[alertView release];
[backgroundView release];
[inputField release];
[super dealloc];
}
#pragma mark -
#pragma mark Private Methods
- (void)alertDidFadeOut
{
[self.view removeFromSuperview];
[self autorelease];
}
#pragma mark -
#pragma mark CAAnimation Delegate Methods
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
[self.inputField becomeFirstResponder];
}
#pragma mark -
#pragma mark Text Field Delegate Methods
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[self dismiss:self];
return YES;
}
@end


So, what're we doing here? First, we start by importing the category with our alert view animations and also QuartzCore.h which gives us access to all the datatypes used in Core Animation.

#import "CustomAlertView.h"
#import "UIView-AlertAnimations.h"
#import <QuartzCore/QuartzCore.h>


Next, we declare a class extension with a single method. By putting this method here, we can use it anywhere in our class without getting warnings from the compiler yet we do not advertise the existence of this method to the world. This is, essentially, a private
method. In a dynamic language like Objective-C, there are no truly private methods, but since the method is not declared in the header file, that's our way of saying "this is ours, don't touch". This method, which you'll see in a moment, will be called after
the alert has been dismissed to remove it from its superview. We don't want to remove it until after the fade-out animation has finished, which is why we've declared a separate method.

@interface CustomAlertView()
- (void)alertDidFadeOut;
@end


After we synthesize our properties, the first method we write is show. This is the method that gets called to, well… show the alert. I matched the method name used in UIAlertView and
also made it an IBAction so that it can be triggered directly inside a nib file.

The weirdest part of this method is that it actually retains self. This is something you're generally not going to want to do. Since we've implemented our alert view as a view controller instead of a UIViewsubclass
like UIAlertView, we need to cheat a little because a view controller is not retained by anything by virtue of its view being in the view hierarchy. This isn't wrong - we're going to bookend our retain with a release
(well, actually, an autorelease) so no memory will leak, but it is unusual and not something you're going to want to use in very many places. When you retain self, you need to take a long hard look at your code and
make sure you have a darn good reason for doing it. In this instance, we do.

After retaining, we grab a reference to the window by way of the application delegate and add our view to the window, matching its frame. Then we call the two animation methods we created earlier to fade in the image with the circular gradient and "pop" in
the alert view:

- (IBAction)show
{
// Retaining self is odd, but we do it to make this "fire and forget"
[self retain];

// We need to add it to the window, which we can get from the delegate
id appDelegate = [[UIApplication sharedApplication] delegate];
UIWindow *window = [appDelegate window];
[window addSubview:self.view];

// Make sure the alert covers the whole window
self.view.frame = window.frame;
self.view.center = window.center;

// "Pop in" animation for alert
[alertView doPopInAnimationWithDelegate:self];

// "Fade in" animation for background
[backgroundView doFadeInAnimation];
}


The next action method we write is the one that gets called by the two buttons on the alert. Regardless of which button was pushed, we want the text field to resign first responder status so that the keyboard disappears, and we want the alert to fade away.
We're going to use implicit animations this time and then use performSelector:withObject:afterDelay: to trigger our private method that will remove the view from its superview. After that, we check sender's tag value
to see which delegate method to notify.

- (IBAction)dismiss:(id)sender
{
[inputField resignFirstResponder];
[UIView beginAnimations:nil context:nil];
self.view.alpha = 0.0;
[UIView commitAnimations];

[self performSelector:@selector(alertDidFadeOut) withObject:nil afterDelay:0.5];

if (sender == self || [sender tag] == CustomAlertViewButtonTagOk)
[delegate CustomAlertView:self wasDismissedWithValue:inputField.text];
else
{
if ([delegate respondsToSelector:@selector(customAlertViewWasCancelled:)])
[delegate customAlertViewWasCancelled:self];
}
}


The viewDidUnload and dealloc are bog standard, so there's no point in discussing them. The next method after those is our "private" method. It does nothing more than remove
the now invisible alert view from the view hierarchy and autoreleases self so that the view controller will be released at the end of the current run loop iteration. We don't want to use release because
we really don't want the object disappearing while one of its method is executing:

- (void)alertDidFadeOut
{
[self.view removeFromSuperview];
[self autorelease];
}


Earlier, when we created the "pop in" animation, we specified self as the delegate. The next method we implement is called when the "pop in" animation completes by virtue of that fact. All we do here is make sure the
keyboard is shown to the user and associated with our text field. We do all that simply by making the text field the first responder:

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
[self.inputField becomeFirstResponder];
}


Finally, we implement one of the text field delegate methods so that when the user presses the return key on the keyboard, it dismisses the dialog.

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[self dismiss:self];
return YES;
}


At this point, we're done. We can now use this custom alert view exactly the same way we use UIAlertView:

CustomAlertView *alert = [[CustomAlertView alloc]init];
alert.delegate = self;
[alert show];
[alert release];


As I stated earlier, you can use this same technique to present anything you can build in Interface Builder, and the result will be a highly-reusable alert object.

Ref:

http://iphonedevelopment.blogspot.jp/search?q=iboutlet
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: