Programmatically adding a button to a view


#1

Subtitle: locating the origin of a button

I have completed the map challenge of Chapter 7. I create the map view controller in the app delegate after creating the other two controllers, following the pattern. Then in the map view controller’s -loadView method, I create the map view programmatically, following the pattern of -loadView in HypnosisViewController.

Now, I want to add an info button that brings up an action sheet with four buttons to let the user select the map type. Actually, I have done just that; works great! But I have used hard-coded values to locate the button, which is a bad idea for long term compatibility and portability.

  • (void)loadView
    {
    MKMapView *mv = [[MKMapView alloc] initWithFrame:CGRectZero];
    mv.delegate = self;
    [self setView:mv];
    [mv release];

    /*
    My first intention was to set up the info button at the bottom right of the map view,
    just above the tab bar. But with the frame of the map view set to CGRectZero, locating the button
    becomes a challenge. When -loadView is invoked, the map view controller creates the map
    view, but the view is not yet added to the window’s view, so mv.window returns nil and
    window.bounds is also unavailable.

    My second approach was to query the tab bar for its bounds and work from there. The tab bar
    claims the whole window within its bounds.

    So now, I will query the tab bar for the window, get the window’s bounds, and either place the button
    with hard coded offsets
    */

    // set up map options info button

    // first try: calculate button origin from mv.bounds or from the window’s bounds
    CGRect mvBounds = mv.bounds; // returns CGRectZero
    UIWindow *mvWindow = mv.window; // returns nil
    CGRect windowBounds = mvWindow.bounds; // messages to nil are ignored

    // try getting the bounds of the tab bar and calculating the button origin from
    // the tab bar bounds
    UIViewController *tbvc = self.parentViewController;
    UIView *tb = tbvc.view;
    CGRect tbBounds = tb.bounds; // tab bar claims whole window within its bounds (0.0, 0.0, 768.0, 1024.0)

    CGRect screenRect = [UIScreen mainScreen].bounds; // also returns 0.0, 0.0, 768.0, 1024.0

    // so get the window, use window.bounds and assume the height of either the status bar
    // or the tab bar

    UIWindow *window = tb.window;
    CGRect wb = window.bounds;

    CGPoint buttonOrigin;
    buttonOrigin.x = wb.size.width - 40.0; // 40 units from right side of screen
    buttonOrigin.y = wb.size.height - 100.0; // ? units above top of tab bar

    UIButton *infoButton = [UIButton buttonWithType:UIButtonTypeInfoLight];
    CGRect buttonFrame = infoButton.frame;
    buttonFrame.origin = buttonOrigin;
    infoButton.frame = buttonFrame;

    [infoButton addTarget:self
    action:@selector(presentMapOptions)
    forControlEvents:UIControlEventTouchUpInside];

    [mv addSubview:infoButton];

}

What is the correct way to programmatically query the OS regarding the size of existing objects so I can place the button relative to these objects?

Thanks for the help.

BTW, at the time I started the challenge, I saw only one view and chose the programmatic route. Now, I can see how using Interface Builder might make this easier. But I still would like to be able to do it either way.


#2

Followup:

I also looked at -viewDidLoad as a possibility. The view still returns CGRectZero bounds and a nil window, so at this point it is loaded, but not added to the window.


#3

You can get the amount of screen real-estate for your application by using the following:

CGRect availableArea = [[UIScreen mainScreen] applicationFrame];

You might want to create your view controller’s views with this rectangle initially and layout your interface as though it was going to be this size.

Then, the best way to make sure things resize properly is to use autoresizing masks. When the view is created, you won’t really know how big it will end up being on the screen even if you do give it an initial rectangle (it may go directly on the window or it may get resized by a nav or tab controller). If you set up the autoresizing masks correctly, no matter how the parent view gets resized, the button will be positioned the same relative to its parent’s bounds.


#4

Thanks, Joe. I will work with this.

When I said, “it works great”, that is true of my code above if the user starts the app in portrait orientation and does not rotate the screen. Maps beg to be rotated! The autoresizing masks should fix that. Btw, in the book, why do you use CGRectZero for the initial view instead of applicationFrame as returned by the UIScreen class? Thanks again.


#5

In this case, we are using CGRectZero because we only really have one view with no subviews that will automatically be resized by the tab bar controller.