In part one I wrote about planning a custom iPad interface using UIScrollView and UIGestureRecognizer. Depending on the elements you choose, developing a non-standard interface will probably involve creating a view hierarchy dependent on multiple views as well as Core Graphics. While detailing each aspect would involve many articles, I've decided to highlight the most important components that make the interface work.
Tip: In this article I make several references to the view hierarchy for MuseumApp. You can see a graphical representation of this object model in the previous article.
Working with Multiple Views
To keep the pieces organized, my subviews are added into MuseumCollectionView.xib (as objects) then manually configured through code. The code configuration was a two-step process of declaring IBOutlets then adding them to the appropriate views. When adding the drawing view (CollectionDrawView) I declared an instance of this directly from my UIViewController (MuseumCollectionView) class. Once the declaration is established I refresh the draw view by calling setNeedsDisplay.
1: //
2: // MuseumCollectionView.h
3: // MuseumApp
4: //
5: // Created by Wayne Bishop on 8/13/10.
6: // Copyright 2010 Arbutus Software. All rights reserved.
7: //
8: #import <UIKit/UIKit.h>
9: #import <QuartzCore/QuartzCore.h>
10: #import "CollectionDrawView.h"
11: #import "MuseumDetailView.h"
12: @interface MuseumCollectionView : UIViewController <UIScrollViewDelegate> {
13: IBOutlet CollectionDrawView *ColDrawView;
14: IBOutlet UIScrollView *scrollView;
15: IBOutlet MuseumDetailView *detailView;
16: }
17: @property(nonatomic, retain) CollectionDrawView *ColDrawView;
18: @property(nonatomic, retain) UIScrollView *scrollView;
19: @property(nonatomic, retain) MuseumDetailView *detailView;
20: //the user selection for the selected collections
21: - (void)selectDetailCollection;
22: - (void)processCollectionFadeAnimation;
23: - (void)setTapGesture;
24: - (void)setPinchGesture;
25: //manage standard input gestures
26: - (IBAction)handleTapGesture:(UIGestureRecognizer *)recognizer;
27: - (IBAction)handlePinchGesture:(UIGestureRecognizer *)recognizer;
28: @end
Configuring UIScrollView
With everything hooked up in interface builder (IB) the next step is to configure UIScrollView. For my project an important property to consider is contentSize. Since I want to enable vertical scrolling I extend the length of the vertical area to a multiple of my drawing view. This multiple will set the sliding plane for view. The width should match the size of the target view.
1: //sets the scrollable area for the interface
2: scrollView.contentSize = CGSizeMake(ColDrawView.frame.size.width,
3: ColDrawView.frame.size.height * 1.35);
4: scrollView.clipsToBounds = YES;
5: scrollView.delegate = self;
6: scrollView.pagingEnabled = YES;
On line 5 the delegate is set to self. Even though I am not handling any specific UIScrollView events I keep this code as a placeholder. On line 6 I set the pageEnabled property so the interface will "glide" to the next set of collections when the user invokes a minor scrolling action. Adding this property improved the usability of the window and allowed gestures to work more seamlessly. While hard to visualize, this creates the functional equivalent of paging through apps on the iPad.
Handling Changing Orientations
MuseumApp is my first iOS application to support multiple orientations. I have to admit this process was cumbersome to get up and running. There are a handful of things you need to consider that may not be obvious. The first step is to enable views contained within your tab bar controller to change orientation.
If you create a new iPad application based on a single view, that view is configured with the correct boilerplate code. However if you add a UITabBarController to your default UIWindow you must override the default UITabBarController class and provide your own implementation. Here's an example of that code.
1: @implementation MuseumTabController
2: - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
3: return YES;
4: }
5: @end
Processing Orientation Messages
Once the view is capable of receiving orientation messages the next step involves redrawing the display to support images using a 3X3 or 4X2 format. To make this seamless I need to consider the default orientation (once the app is started) as well as changes in orientation. For MuseumApp the logic to detect orientation is handled at the UIViewController level. Once I've confirmed the orientation I set instance properties (e.g. ColDrawView.isPortrait) exposed from my UIView drawRect class and eventually call setNeedsDisplay to refresh the view.
When detecting changes in orientation there are three delegates you can work with. The one I settled on was (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation duration:(NSTimeInterval)duration. This function is called when the system detects that a change is about to occur. In my testing this delegate provided the most natural experience.
1: //gets called before the animation starts. This is a one step animation procedure.
2: - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
3: duration:(NSTimeInterval)duration {
4: UIInterfaceOrientation nowOrientation = self.interfaceOrientation;
5: if (nowOrientation == UIInterfaceOrientationPortrait || nowOrientation == UIInterfaceOrientationPortraitUpsideDown) {
6: //moving towards portrait orientation
7: ColDrawView.isPortrait = YES;
8: }
9: else {
10: //moving to landscape orientation
11: ColDrawView.isPortrait = NO;
12: }
13: //process the fade animaton for the rotation
14: [self processCollectionFadeAnimation];
15: }
Hopefully everything makes sense with the exception of line #14. This line ensures that an animation is fired each time the orientation changes. For MuseumApp this is a simple UIView Fade Transition. The code (not shown here) fades out the interface while setNeedsDisplay refreshes the drawRect view. The animation sequence then fades in the view once the animation completes. Applying this technique allows the interface to reorient itself without users really noticing. I learned this trick in a WWDC presentation and I plan to use it in other areas of the app.
Handling Interface Guestures
The final piece involves adding gesture recognition to the images. When I first started iPhone development I learned a technique of applying hidden UIButtons behind images to enable this type of functionality. Another technique is to apply the standard touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event. The problem with this method is that it can only detect touches on the outer most view. For MuseuemApp this is CollectionDrawView which won't give me the functionality I need. For it to work I want to detect touches at the UIViewController level. As a result I changed my implementation to support UIGuestureRecognizer.
1: //register the tap gesture
2: - (void)setTapGesture {
3: UITapGestureRecognizer *singleFingerTap = [[UITapGestureRecognizer alloc]
4: initWithTarget:self action:@selector(handleTapGesture:)];
5: singleFingerTap.numberOfTapsRequired = 1;
6: [self.view addGestureRecognizer:singleFingerTap];
7: [singleFingerTap release];
8: }
This method is called when my UIControllerView is first loaded. As you can see the code is pretty straightforward. On line 3 I set the selector for handling the tap gesture. Then I set the gesture to support a single tap then add the object to the UIViewController. Like the iPad photos app I also want images to invoke functionality when they are pinched. This was an easy process of creating a similar function that supported an instance of UIPinchGuestureRecognizer.
Recognizing Gestures with Images
Now that my UIViewController can detect pinch and tap gestures the final step involves associating gestures with specific images. For example, if someone taps on the African Collection image they should be taken to a view that displays more information about that specific collection. Keep in mind that my images are not UIButtons but I need for them to act as buttons. The following shows the implementation of the tap gesture selector.
1: //handle the tap guesture
2: - (IBAction)handleTapGesture:(UIGestureRecognizer *)recognizer{
3: CGPoint currentLocation;
4: currentLocation = [recognizer locationInView:self.view];
5: //process the african collection
6: if (CGRectContainsPoint(ColDrawView.AfricanRect, currentLocation)) {
7: //the first collection was selected
8: NSLog(@"The african collection was selected");
9: //process the transition animation to the detail view.
10: [self selectDetailCollection];
11: }
12: }
On line 5 I receive the CGPoint (e.g. x and y coordinates) of where the user tapped. The secret sauce occurs on line 6 where I compare the current tap coordinate with the CGRect of a specific image. In this case I am detecting if someone tapped the African Collection image.
Conclusion
Building a custom iPad interface involves a fair amount tinkering but the payoff is worth it. As you move forward with your own solution be sure to test often, and be sure to test all of your animation sequences directly on your iPad (it just works better). Feel free to post your thoughts about my code and don't be shy about providing additional tips or feedback. Happy coding!