Showing posts with label UINavigationController. Show all posts
Showing posts with label UINavigationController. Show all posts

August 3, 2009

Assigning Delegates

Kind reader Demitri Muna pointed out a flaw in my TableFormEditor package. I stumbled into a trap I fear many newbie Objective-C programmers also fall into: that of retaining delegates. It is a two-line change, but I feel this issue is important enough to write about it.

The basic problem is a retain cycle which results in a memory leak. This has been fairly well documented in this thread, but I will go into more detail here in the context of the TableFormEditor.

In this context, there are 2 actors involved:

  1. the client object
  2. the TableFormEditor object

When the client object creates a TableFormEditor instance, it needs to pass a delegate to handle the various callbacks. Typically, but not required, the delegate is the client object (i.e. self).

The TableFormEditor has this declaration in its .h file:

@property (nonatomic,retain) id <TableFormEditorDelegate> delegate; 


So what this means is that the client has a retain on the TableFormEditor (because it created it via [alloc]), and the TableFormEditor instance has a retain on the client (via the "retain" keyword in the @property declaration).

This is the dreaded A -> B and B -> A scenario described in the Stack Overflow thread mentioned above. When you are in this situation, A and B will never go away because B has a retain on A that will not release until B goes away. But B won't go away until A goes away. Result: memory leak!

However, all is not lost if you followed the pattern I have in the TableFormEditor example. In the example, I do not keep the TableFormEditor object around. As soon as I push it on the Navigation Controller's stack, I release it:

   
// stick the editor onto the navigator's stack 
[self.navigationController pushViewController:form animated:YES];      

// release the form since the navigator retained it 
[form release]; 


But, if you retain the pointer to the TableFormEditor, like keeping it in an instance variable, then you will likely end up with a memory leak.

Thus I have released version 1.6 of the TableFormEditor class to make this change:

@property (nonatomic,assign) id <TableFormEditorDelegate> delegate; 


This simply assigns the pointer without bumping up the retain count. This breaks the cycle and the potential for memory leaks.

For more information on retain cycles and how to prevent them, I urge you to check out this blog article at Cocoa With Love.





May 29, 2009

Trapping the UINavigationBar Back Button (part 3)

I have covered the topic of trapping the back button in order to do something. These previous forays are part 1 and part 2. I had implemented my solution from part 2, and it is working well. However, I am always on the look out for a better, cleaner solution. Just recently, an anonymous commenter pointed out something I had not noticed before: the UINavigationBarDelegate prototype has a method called navigationBar:shouldPopItem: It is called just before the item is popped. If it returns NO, then the pop won't actually happen. But this can be the perfect way to insert some logic before the pop happens (and return YES at the end). Here is how you would implement your back-button press event logic:
- (BOOL)
navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
   //insert your back button handling logic here

   // let the pop happen
   return YES;
}  
This is so much cleaner than my previous solution. I love it! Thanks to whomever pointed out the obvious.

May 12, 2009

Multiple Buttons on a Navigation Bar

Previously, I wrote about how to use standard icons in your navigation bar. This takes advantage of the nav bar's ability to put a button on the right side. But what if you want to put multiple buttons there? If your title is short enough, you can easily fit 2 or 3 buttons in that space. The nav bar allows you to stick content on the right hand side via the rightBarButtonItem property. On the surface, you might think you can add but one button to this property. But it's not so. There is an initializer for UIBarButtonItem that takes a view:
initWithCustomView:(UIView *)customView
With this, you can create multiple UIButton objects, add them to a UIView, and then create a UIBarButtonItem passing in this parent UIView. The following pseudo-code shows what I mean:
// create the container
UIView* container = [[UIView alloc] init];

// create a button and add it to the container
UIButton* button = [[UIButton alloc] init...];
[container addSubview:button];
[button release];

// add another button
button = [[UIButton alloc] init...];
[container addSubview:button];
[button release];

// now create a Bar button item
UIBarButtonItem* item = [[UIBarButtonItem alloc] initWithCustomView:container];

// set the nav bar's right button item
self.navigationItem.rightBarButtonItem = item;
[item release];

But, what if you want multiple *standard* buttons. Access to the standard buttons/icons is through the UIBarButtonItem class, not UIButton. You can't add UIBarButtonItem objects to a UIView. While there is support to create a UIBarButtonItem object from a UIView-derived object, there does not seem to be a way to do the opposite; there is no way to create a UIView object from a UIBarButtonItem. The only way I found how to deal with multiple UIBarButtonItem objects is to stick them in a toolbar. A UIToolBar is designed to hold multiple UIBarButtonItem objects. Think of a toolbar as your container, instead of the UIView object. And since a UIToolBar is UIView based, it can be added to the right side of a nav bar using the above trick. The following code shows how you can add two standard buttons to the right side:

// create a toolbar to have two buttons in the right
UIToolbar* tools = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, 133, 44.01)];

// create the array to hold the buttons, which then gets added to the toolbar
NSMutableArray* buttons = [[NSMutableArray alloc] initWithCapacity:3];

// create a standard "add" button
UIBarButtonItem* bi = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:NULL];
bi.style = UIBarButtonItemStyleBordered;
[buttons addObject:bi];
[bi release];

// create a spacer
bi = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
[buttons addObject:bi];
[bi release];

// create a standard "refresh" button
bi = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(refresh:)];
bi.style = UIBarButtonItemStyleBordered;
[buttons addObject:bi];
[bi release];

// stick the buttons in the toolbar
[tools setItems:buttons animated:NO];

[buttons release];

// and put the toolbar in the nav bar
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:tools];
[tools release];


May 4, 2009

Using Standard Toolbar and Navbar Buttons

In an article several months back, I discussed how to use standard Tab bar buttons. These are icons provided by Apple for you to use in the tab bar. But tab bars are not the only container that offers system-provided icons. There are standard icons available for toolbars and navigation bars. Recently I wanted to make use of a system icon, but it took me some time to find what icons were standard and how to use them. Even though it is fairly trivial, I would like to share this information, if nothing else but to save people a few minutes of needless searching. If you feel weird about using system-provided icons, don't. Apple is very keen on developers making use of the standard icons and encourages it in the HIG. However, Apple is also adamant about the consistent use of these icons. If your application uses a standard icon in a way unintended for that icon, then it is likely your app will get rejected from the app store. For example, if you are using the standard Contacts icon to bring up a list of contact lens providers, ding! Here is the section in the HIG that lists the standard icons and their intended uses. This gives you an idea to what is available, but doesn't help you coding one of them up. You have to know how to reference each of these in your code. For this, you have to look at the UIBarButtonItem class reference. If you go to the Constants section, you will see the UIBarButtonSystemItem enumerations which define the values for all the system icons.

For nav bars

To put a system icon on the navigation bar, you have several choices for the location. At the most basic level, you can put the icon on the left or the right. If you require more refinement, it's quite easy to do with subviews, but that technique is well-documented and outside the scope of this article. Here is the simple code for putting the standard refresh icon in the left position:
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]
                                      initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh
                                      target:self
                                      action:@selector(refresh:)];

The first parameter is the enum from the UIBarButtonItem reference. The target/action parameters specify the method to call when the button is hit.

For toolbars

Adding the standard icons to a toolbar is a little more involved, but still straight-forward. Toolbars also take UIBarButtonItem objects, but instead of assigning them to specific locations, you must add them to an NSArray object first (in the order you want them) and then add the array to the toolbar object.

// create the toolbar object
UIToolBar* toolbar = [[UIToolBar alloc] init];

// create an array to hold the buttons
NSMutableArray* buttons = [[NSMutableArray alloc] init];

// create a standard button
UIBarButtonItem* button = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh
target:self
action:@selector(refresh:)];

// stick it in the array
[buttons addObject:button];

// add more buttons
//...

// add the array to the toolbar
toolbar.items = buttons;

// done with the buttons array
[buttons release];

March 23, 2009

TableFormEditor Example

I have been asked to provide a more concrete example on using the TableFormEditor. I will provide an Xcode project with a little app that demonstrates the TableFormEditor in action. The project can be downloaded from here. What this example does is display three fields of data and a button. When the button is pressed, the user either is creating a new record or editing an existing record. This example also has a new version of the TableFormEditor which addresses a couple bugs. One bug was that if you have a translucent navigation bar, then the editor puts the first field under the nav bar. The other bug is that the alert that pops up when you hit the delete button assumes you have a tab bar. If you don't it crashes. So this version fixes that assumption.

March 11, 2009

Trapping the UINavigationBar Back Button (part 2)

After posting the first part of this article, it was pointed out to me that there is an alternative solution: handle the viewWillDisappear message. This is certainly a more elegant solution, but it does have its drawbacks, which is why I did not use it in the first place. However, it is a valid solution in some cases and can be of use to some people, so I will describe it here. There is a set of messages that get sent to your UIViewController subclass during various events:
- (void)viewWillDisappear:(BOOL)animated - (void)viewDidDisappear:(BOOL)animated - (void)viewWillAppear:(BOOL)animated - (void)viewDidAppear:(BOOL)animated
These are called for the event described in the method name. For example, the viewWillDisappear is called just before the view disappears. Now for my situation, I needed to trap the event of my controller being popped off. The problem with using viewWillDisappear (or viewDidDisappear) is that it is called in different scenarios. When a controller is pushed on top of yours, the viewWillDisappear method is called. And when your controller is popped off, the method is called again. The problem for me is the event is too general. Your view is disappearing, but the reason why it's disappearing is not provided. In my situation, my controller can push other controllers, but I only wanted to know when I was popped off, not pushed on top of. In order to differentiate between the two scenarios, I need additional intelligence in my code to determine which situation I am in. Basically, whenever I am about to push on a new controller, I have to set a flag to ignore the next viewWillDisappear message. Then I need to handle the viewWillAppear message to clear this flag. Not a big deal, but seems like a hassle to handle something fairly basic. Here is the framework needed to support this:
// ---------------------------------------------------------
//
// YourController.h
@interface YourController : UIViewController
{
 BOOL ignoreDisappear;
}
@end

// ---------------------------------------------------------
//
// YourController.m

// this helper method will push a new controller onto the navigation stack
- (void) myPushController:(UIViewController*) newController
{
  // first, set our flag to ignore the next viewWillDisappear message
  ignoreDisappear = YES;

  // now do the push
  [self.navigationController pushViewController:newController animate:YES];
  [newController release];
}

- (void) viewWillDisappear:(BOOL) animate
{
  // do we care about this event?
  if(ignoreDisappear == NO) {
     // this is your back button handle logic
     //...
  }

  [super viewWillDisappear:animate];
}

- (void) viewWillAppear:(BOOL) animate
{
  // clear the flag
  ignoreDisappear = NO;

  [super viewWillAppear:animate];
}
But that was my situation. If your controller is at the end of the UINavigationBar hiearchy, meaning you cannot have another controller pushed onto yours, then you are safe assuming that the viewWillDisappear is being called for the scenario where your controller is being popped off. Take caution, though. Should you later change the behavior of your code to allow additional controllers to be pushed, your viewWillDisappear will get invoked in what may be an inappropriate time. One other thing I should mention. There is this warning in the Apple reference in each of the four methods listed above:
Warning: If the view belonging to a view controller is added to a view hierarchy directly, the view controller will not receive this message. If you insert or add a view to the view hierarchy, and it has a view controller, you should send the associated view controller this message directly. Failing to send the view controller this message will prevent any associated animation from being displayed.
I'm not real clear on what the phrase "added to a view hierarchy directly" means, but there are numerous reported problems related to this. I don't have these problems myself, but they are reported here and here.

March 7, 2009

Trapping the UINavigationBar Back Button

I recently had a need to trap the back button tap on the navigation bar in order to do something before popping to the previous controller. However, I soon discovered that this was not possible. The standard back button is a "special" button that developers cannot override, at least not with the current public SDK.
The only thing you can change on the standard back button is the text. By default, this is the title of the controller. You can change it with code like this:
[self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"New Title" style:UIBarButtonItemStyleBordered target:nil action:nil]; 
The important but subtle thing about a controller's backBarButtonItem property is that it is not the back button displayed when that controller is on top; it is for other controllers that are pushed on top of it. In other words, if you have a "root" controller whose title is something very long, you set its backBarButtonItem title to something shorter. Then when other controllers are pushed on the stack, they will use this back button item as their back button. This is unintuitive at first glance, but makes sense when you think about it: you can redefine the back button title once, and all other controllers that are pushed on top will use this back button.
Even though there are four parameters to the initializer, only the title is used for this "special" back button. The style specified seems to be ignored, as are the target and action parameters. Because of this, you cannot insert your own event handler for the back button, which is what I needed to do.
After some digging and research, I have a solution. It is not a pretty solution, but it's good enough.
The navigationItem property of a UIViewController has another property called leftBarButtonItem. This, like the backBarButtonItem, is a UIBarButtonItem object. If you set this to something other than nil, then this new button will replace the standard back button item. So now, I can get my custom back button event handler to work, because this one honors the style and the target/action parameters:
- (void) viewDidLoad
{
   // change the back button and add an event handler
   self.navigationItem.leftBarButtonItem =
   [[UIBarButtonItem alloc] initWithTitle:@"Pages"
                                    style:UIBarButtonItemStyleBordered
                                   target:self
                                   action:@selector(handleBack:)];
}

- (void) handleBack:(id)sender
{
    // do your custom handler code here

    // make sure you do this!
   // pop the controller
    [self.navigationController popViewControllerAnimated:YES];
}

If you still want this button to behave like a back button, it is important to put the popViewControllerAnimated: call in your handler. I should also say that this leftBarButtonItem should be set on the controller who will actually show the back button, unlike the backBarButtonItem. Now the reason I stated this wasn't a perfect solution is the following: The "special" back button looks different. Here is what the standard backBarButtonItem looks like:
Yet, the leftBarButtonItem looks like this:
I have been unable to force my custom back button to have the different shape. This is another indication that this back button object is treated specially that is not exposed in the SDK. Other than this little aesthetic discrepency, this approach met my needs. Hopefully it will meet yours as well.