User Tools

Site Tools


ios-labs-s14:class-06

GGT Tutorial 3

This is the final part of the GGT tutorial.

In this section you will finally graph some grants using another imported class. All of the graphing is done Google's CorePlot graphing library. After importing the app and setting up the third-party frameworks, we'll cover rotation triggers, programmatic views, and programmatic view controllers.

You may not finish this tutorial in class, but some of the content here will be on the midterm, so I encourage you to finish it in your own time.


Import Graph Controller

The code that actually does the graphing is built around CorePlot, Google's graphing library. Thankfully, you won't have to write any of the code that actually makes the magic happen. Download the files here.

Along with the actual controller, you'll need to add the static library files and headers. The first is a precompiled file that contains the CorePlot implementation, the second is the set of header files that allows us to access the library within our code.

Theres a bit of a glitch here, however. In order to load the library, you'll have to set a linking flag within Xcode's project files. You should only have to do this for projects rarely, and we won't discuss the project files in detail in this tutorial.

Add New Files

  1. Download the archive and unzip it
  2. Drag and drop the new files into your project (Note: framework files are conventionally placed in the “Frameworks” folder
  3. Import “LandscapeMainGraphViewController.h” into RootViewController

  1. Click on the blue Project file, first in your project navigator
  2. Select “Build Settings” on the top of the page that appears
  3. Select “All” instead of “Basic” on the top left
  4. Using the search bar near the top of the screen, enter “linker”
  5. Find “Other Linker Flags” in the search results, double-click to the right of it
  6. Press the plus sign, enter “-ObjC -allLoad”

So what do they flags you just set do? They force the linker to load all statically referenced libraries before compile time so the compiler has access to the symbols stored within. Don't spend too much time thinking on it.


Rotation Presentation

Our new graphing controller looks a lot better in landscape view. Although we could lay it out in InterfaceBuilder as we've done with all the previous controllers, its faster to create it programatically.

Read up on UINavigationControllers in the Building Blocks if you've forgotten their function. To this point we've only really interacted with the controller from IB and let it act behind the scenes, save for one example in the previous week.

All view controllers organized into a nav stack (refers to the navigation controller's active stack) have access to the navigation controller in memory through self.navigationController. This allows them to push or pop view controllers in code without having to establish a segue in IB. The two methods used are:

pushViewController:animated
popViewControllerAnimated:

where …animated: takes a boolean (YES or NO) to toggle a transitional animation. You will use both of these to present and remove the landscape controller.

The iOS device can be in one of four orientations: landscape left and right, portrait up and down. Thankfully, all view controllers are notified of changes in orientation through an optional delegate method called willRotateToInterfaceOrientation. By implementing this method, your controllers can react to changes appropriately.

Add Rotation Callback

  1. Copy the following method into the body of RootViewController
- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
    //double check the destination orientation
    if(UIInterfaceOrientationIsLandscape(toInterfaceOrientation)) {
    
    }
}

Note that UIInterfaceOrientationIsLandscape is orange when pasted into Xcode. The only other orange text you've seen so far is #pragma mark. These are both preprocessor directives. They are not Objective-C and are not compiled as code, instead they are replaced before the compiler runs. In the case of UIInterfaceOrientationIsLandscape, the ability to check a passed orientation comes from a clever macro. Note that toInterfaceOrientation is passed into the delegate method, and is how we check what the resulting orientation will be.

Now that we can recognize an orientation transition, its time to push the new view controller. Within the conditional above, you must instantiate a new Landscape controller, call init on it with the grants array, and finally call pushViewController on self.navigationController, passing the Landscape controller.

Instantiate and Push

  1. Instantiate the new Landscape controller within the newly created rotation method (standard alloc/init)
  2. Call initWithGrantArray on the resulting object, passing it the array of grant objects
  3. Push the view controller onto the nav stack

Test the app and ensure that the Landscape controller appears when the device is rotated (tip: command + arrowKeys rotate the simulator). You'll undoubtedly be quick to note that although Landscape appears, Root never reappears on rotation shifts. Implement the rotation callback in Landscape, popping self, to return to Root.

Add Another Rotation Callback

  1. Copy the same rotation method into the Landscape controller
  2. Change the orientation double-check from landscape to portrait: UIInterfaceOrientationIsPortrait instead of UIInterfaceOrientationIsLandscape
  3. Pop this view controller by calling popViewController on the navigation controller

Since all transitions are orientation-based we don't really need that top white bar on Landscape, it in fact occludes the graph. This is called a UINavigationBar and is autogenerated for a controller that belongs to a nav stack. Lets get rid of it.

Remove Navigation Bar

  1. In viewDidLoad, call setNavigationBarHidden: on the navigation controller

Adding Views Programatically

This section could alternatively be titled “why we like InterfaceBuilder.”

Here you'll add control buttons that allow the user to change the grants displayed on the graph. As the number of grants that could be passed into Landscape is variable, you'll need a variable number of buttons. Though this is certainly possible in InterfaceBuilder (and is how the issue was originally solved) its much cleaner to add the views to the screen programatically.

In this section you will instantiate UIButtons in Landscape and add them as subviews to Landscape's view. The tricky part will be understanding how iOS handles placement of views.

Tidbits on Views:

  1. Each controller has a UIView property called view, accessable through self.view. It is a reference to the whole screen the view controller owns, and all views shown on the screen (labels, buttons, etc) are subviews of this view
  2. Subviews occlude each other: a subview in front of another subview will cover it up
  3. Views have a property called frame that dictates where they should be drawn on the screen
  4. Frame is of type CGRect (a struct, not an Objective-C object) which has 4 variables: x coordinate, y corodinate, width, and height
  5. The origin of the screen's coordinate plane is in the top left corner. Y values increase as you move down the screen
  6. You cannot change members of a views's frame individually, you must replace the whole frame

Lets get started.

Adding A Button Programatically

  1. Create two integers to represent the dimensions of the button. Set the width variable to 120 and the height to 25
  2. Instantiate a new UIButton
  3. Add the following code below (substituting the name of your button and integers):
  button.frame = CGRectMake(100, 100, widthVariable, heightVariable);
  [self.view addSubview:button];
  [self.view bringSubviewToFront:button];
  [button setTitle:@"Test" forState:UIControlStateNormal];
  
  [buttonReferences addObject:button];

Test to make sure the button appears.

Instantiating a button doesn't do much, you have to explicitly add it as a subview using addSubview. Note how we refer to the controller's view property using self.view. Note also the usage of bringSubviewToFront; without this line the button would be stuck behind the graph.

CGRectMake is a C convenience function, as obvious by the parenthesis. It produces a CGRect struct, which we assign to the button's frame property, thereby changing its location and size.

Aright, here comes the fun part. You'll next need to make a loop that iterates over each grant stored in “grantObjects,” creating a button for each one with the appropriate title.

Dynamic Quantity of Buttons

  1. Create a for loop iterating over each Grant (do not use a for-each loop, use the basic (int i =0; i< … as we need an index). Make this loop wrap around all the code you just made EXCEPT for the integer declarations! Each iteration of the loop should create new button, assign it a frame, add it as a subivew, and add it to the array of references. Note: the array you need is called “grantObjects,” not “grants.”
  2. Copy the following code to set the text of the button into the loop (where “grant” is the grant object at the appropriate index of grantObjects– you have to fetch it first!):
[button setTitle:[[grant getMetadata] objectForKey:@"title"] forState:UIControlStateNormal];

Test to make sure you see the buttons on the screen.

Obviously we'd like the buttons to not overlap. The solution here is to dynamically set each frame property so they sit hugging the right edge of the screen, one on top of each other. To do this you'll need the following:

  1. Width of the screen
  2. Width of the button
  3. Height of the button

The x coordinate should be easy, use the loop variable to set the y coordinates of each button to a variable amount. Pad the y-value with some constant value so the buttons dont creep to the top and get occluded by the status bar (the battery, reception, etc indicators).

Dynamically Placing Buttons

  1. Create two integers within the loop. These represent the x and y coordinates of the buttons. Set them appropriately
  2. Set the button's frame within the loop to reflect the new, dynamic values

The buttons are all in the right place, but they're fairly ugly. Lets make them pretty.

Customizing Buttons

  1. Copy before loop:
    //Create an array of color objects that are removed in order for each grant, ensuring unique colors
    NSArray *plotColors = [NSArray arrayWithObjects:[UIColor redColor], [UIColor greenColor], [UIColor blueColor], [UIColor cyanColor], [UIColor yellowColor],[UIColor magentaColor],[UIColor orangeColor],[UIColor purpleColor], [UIColor grayColor], [UIColor whiteColor], nil];
  1. Copy inside loop (Note: replace “button” with whatever you named your button):
        //coloring
        UIColor *temp = [plotColors objectAtIndex:i];
        
        const CGFloat* components = CGColorGetComponents(temp.CGColor);
        
        button.backgroundColor = [UIColor colorWithRed:components[0] green:components[1] blue:components[2] alpha:.4];
        [[button layer] setCornerRadius:8.0f];
        [[button layer] setMasksToBounds:YES];
        [[button layer] setBorderWidth:1.0f];
        [button.titleLabel setFont:[UIFont systemFontOfSize:8]];

Ok, buttons in place, pretty, everything is going well, with the notable exception that touching a button does… nothing.

If we were laying these onto the controller's view using InterfaceBuilder, the next step would be connecting an IBAction. This would have the effect of creating the callback method within the controller and alerting the button it should call that method when touched.

The method required to realize this in code is called addTarget:withSelector:forControlEvents:.

  1. target: the object to call the desired method on
  2. selector: there's a little more going on here, but selector is essentially the name of a method
  3. controlEvents: the condition which should trigger the passed method call to the passed target

The method buttonPress has already been created in Landscape. Check it out if you'd like.

Test to make sure the connections work. When you press a button, the graph of the matching color should disappear from the screen, the screen should resize itself, and the SUM graph should change.

Dynamic Delegates

  1. Call addTarget on the buttons as created within the loop, passing self as a target, @selector(buttonPress:) as the action, and UIControlEventTouchUpInside as the control event

—-

Conclusion

Hey, you survived the intermediate material! If you made it here, then you're plenty comfortable with Xcode and Objective-C at this point (or exhausted and frustrated from hours of brute-forcing solutions). Congratulations!

Next week we will cover a smattering of advanced iOS topics. Don't feel like everything we've covered so far is just the tip of the iceberg- once we finish up next week you'll have seen most of what iOS has to throw at you, confidence comes with repetition.

ios-labs-s14/class-06.txt · Last modified: 2014/02/19 14:23 by mbarboi