Tuesday, 6 May 2014

Creating a Page View Controller [There and Back Again]

You have arrived at the third, and final, tab of your Wonderland app. This tab will display the text of the book, one page at a time. This tab uses a page view controller (UIPageViewController) object. Its a container view controllerthat manages a (potentially huge) collection of content view controllers. Each “page” consists of one or two content view controllers. The page view controller provides gesture recognizers that perform, and animate, thenavigation between pages in the collection.

Adding a page view controller to your design is simple enough. Getting it to work is another matter.
Page view controllers are typically code intensive, and this app is no exception. To make the
situation even more exciting, you have to get the bulk of your code working before the page view will do anything at all. So settle in, this is going to be a long trip.
You’re going to need a number of new classes, so create them all now. Use the New File... command to create new Objective-C class files in your projects Wonderland group. Table 12-2 lists the new classes you need tocreate, the superclass of each, and what its role will be. When creating new view controller classes, do not create an XIB file.

Table 12-2. Page ViewClasses


Class
Superclass
Description
WLBookViewController
UIPageViewController
Your custom version of the page view controller thatwill manage the page view
WLBookDataSource
NSObject
The data source object that provides the page view controller with the content view controllers it contains
WLPaginator
NSObject
A utility object that encapsulates the logic thatdetermines out how much text will fit on a page
WLOnePageViewController
UIViewController
The content view controller(s)
WLOnePageView
UIView
A custom view object used to display the text

Just as table and picker views need a data source object, so does the page view controller. But instead of providing a value on a wheel or one row of a table, the page view controllers data source provides it with the view controllers it wants to display, on demand.


Adding the Page View Controllers

Its not all code. Use Interface Builder to create the two view controllers. Select the Main_Phone.
storyboard (or _iPad) file. Drag a new Page  View Controller from the object library and add it to
your design. Also add a new View Controller object as shown in Figure 12-22. 
Arrange them below the other scenes.


Figure 12-22.  Adding the page view controller  and single page view controller


Add the page view controller to the tab bar by right/control-dragging from the tab bar controller to the new page view controller, as shown in Figure 12-23. Select the view  controllers relationship.


Figure 12-23.  Adding the page view controller  to the tab bar

As you did with the other tabs, select the tab bar item in the page view controllers scene. Use the attributes inspector to set its title to “Book” and its tab icon to tab-book.

Now configure the page view controller itself. Select the page view controller object and use the identity inspector to change its class to WLBookViewController. Switch to the attributes inspector and double-check that thefollowing properties are set:

n     Navigation: Horizontal

n     Transition Style: Page  Curl

n     Spine Location: Min

These settings define a “book-like”  interface where the user moves horizontally through a collection of view controllers, one per page. (If you set the Spine location to the Mid, you’d get two view controllers per page.) A transitionbetween controllers emulates the turning of a paper page.


Designing a Prototype Page

Now move over to the plain view controller you just added. Use the identity inspector to change its class to WLOnePageViewController. Also change its Storyboard ID to “OnePage”. This last step is important. This controllerwont be connected in Interface Builder; you’re going to create instances
of it programmatically. To do that, you need a way to refer to it, and you’ll use its storyboard ID to do that. (Its equivalent to UIViews tag property.)

With the preliminaries out of the way, create the interface for the one page view controller. From the object library, add three view objects as follows:

1.       Label

a.       Font: System  15.0

b.       Text: Alice's Adventures in  Wonderland

c.       Position it top center (iPhone) or top right (iPad)

2.       Label

a.       Font: System  11.0  (iPhone) or 13.0  (iPad)

b.       Alignment: center (middle button)

c.       Position at bottom center

3.       View

a.       Position between the two labels to fill in the available space

4.       Constraints

a.       Select both label objects

b.       Fix their height (Editor  Pin  Height)

c.       Select both label objects

d.       Center them (Editor  Align  Horizontal Center in Container)

e.       Add a constraint from the top label to the top layout guide (refer to Figure 12-16)

f.         Select the newly created constraint and use the attributes inspector to check the
Standard option

g.       Add a constraint from the bottom label to the bottom layout guide

h.       Select the newly added constraint (see Figure 12-24) and set its Constant to 60
(allowing for the tab bar at the bottom of the screen)

i.         Fill in the remaining constraints (Editor  Resolve Auto Layout Issues  
Add Missing Constraints in One Page View Controller)

Select the UIView object and use the identity inspector to change its class to WLOnePageView. The finished interface should look like the one in Figure 12-24.


Figure 12-24. One page view controller  interface

Switch to the assistant editor and, with the WLOnePageViewController.h  file in the right pane, add two #import statements:

#import "WLOnePageView.h"
#import  "WLPaginator.h"

And then add four properties to the @interface:

@property  (nonatomic) NSUInteger  pageNumber;
@property  (strong,nonatomic) WLPaginator  *paginator;
@property  (weak,nonatomic) IBOutlet  WLOnePageView  *textView;
@property  (weak,nonatomic) IBOutlet  UILabel  *pageLabel;

Connect the outlet sockets of the last two properties to the WLOnePageView object in the middle of the screen and the small label object at the bottom, as shown in Figure 12-25.
The latter will display the page number.


Figure 12-25. Connecting the outlets in the page view

The other two properties let this view controller know which page of the book it is displaying and reference to the “paginator” object that determines what text is on that page. Taken together, this is all the information this view controller needs to determine the text and page number to display.


Coding the One Page View

You’ve now done just about all you can do in Interface Builder. Its time to roll up your sleeves and start coding. This time, start from the “tail” end of this design and work back up to the page view controller, filling in thedetails as you go. The last object in the chain is WLOnePageView, the custom view that display the text of a page. Select the WLOnePageView.h file. Add two properties to the
@interface:

@property  (strong,nonatomic) NSString   *text;
@property  (strong,nonatomic)  NSDictionary *fontAttrs;

Switch to the WLOnePageView.m implementation file and write its -drawRect: method:

-  (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
[_text  drawInRect:self.bounds withAttributes:_fontAttrs];
}

You should be able to decipher this, having read Chapter 11. When its time to draw itself, it fills the view with the background color ([super  drawRect:rect] does that for you), and then draws its text using the attributes stored in its fontAttrs property.

You’re done with this class. Lets move on to the view controller. You’ve already defined the interface for the WLOnePageViewController class (in WLOnePageViewController.h). Select its implementation file(WLOnePageViewController.m) and fill in the missing code.

At the top of the file, find the private @interface WLOnePageViewController  () section and add a prototype for the -loadPageContent method (new code in bold):

@interface WLOnePageViewController  ()
-  (void)loadPageContent;
@end

The –loadPageContent method prepares the WLOnePageView object to display the text for this controllers page. Add that method now:

-  (void)loadPageContent
{
_paginator.viewSize = _textView.bounds.size; if (![_paginator availablePage:_pageNumber])
_pageNumber = _paginator.lastKnownPage;
_textView.fontAttrs = _paginator.fontAttrs;
_textView.text = [_paginator textForPage:_pageNumber]; [_textView setNeedsDisplay];
_pageLabel.text = [NSString  stringWithFormat:@"Page %u",  
(unsigned  int)_pageNumber];
}

This method loads the content of the view. It configures the paginator object with the size of its text view object. It configures the text view to use the same font attributes the paginator is using, and then asks the paginator forthe text that appears on this page. It also updates the page number label at the bottom of the view.

Theres also a little logic to handle the case where the page to display doesnt exist anymore. This can happen if you rotate the device; the dimensions of the view change, altering the text in each page, and changing thenumber of pages in the book. In this situation, the view changes to the last page available and displays that.

So when does -loadPageContent get sent? Under most circumstances, this kind of first-time-view- setup code would be invoked from your -viewDidLoad method. But -loadPageContent needs to be sent whenever the size of textview changes, and that can happen at any time, most notably when the user changes the display orientation. Solve that by adding a -viewDidLayoutSubviews method and sending -loadPageContent whenever the controllers view layout is adjusted:

-  (void)viewDidLayoutSubviews
{
[super  viewDidLayoutSubviews]; [self  loadPageContent];
}

You’re seeing a number of compiler errors because you havent implemented the paginator object yet. Do that now.

The Paginator

The code for WLPaginator.h is in Listing 12-1 and the code for WLPaginator.m is in Listing 12-2.
If you want to copy and paste the solution, you’ll find the source files for the finished code in the
Learn  iOS Development  Projects  Ch 12  Wonderland project folder.

Listing 12-1. WLPaginator.h

#import <Foundation/Foundation.h>

@interface WLPaginator  : NSObject

@property  (strong,nonatomic) NSString   *bookText;
@property  (strong,nonatomic) UIFont  *font;
@property  (readonly,nonatomic)  NSDictionary *fontAttrs;
@property  (nonatomic) CGSize viewSize;
@property  (readonly,nonatomic) NSUInteger  lastKnownPage;

-  (BOOL)availablePage:(NSUInteger)page;
-  (NSString*)textForPage:(NSUInteger)page;

@end


Listing 12-2. WLPaginator.m

#import  "WLPaginator.h"

@interface WLPaginator  ()
{
NSMutableArray   *ranges;
NSUInteger             lastPageWithContent; NSDictionary                                *fontAttrs;
}
-  (NSRange)rangeOfTextForPage:(NSUInteger)page;
@end

@implementation   WLPaginator

-  (void)resetPageData
{
ranges = [NSMutableArray  array]; lastPageWithContent = 1;
}

-  (void)setBookText:(NSString  *)bookData
{
_bookText  = bookData; [self resetPageData];
}

-  (void)setFont:(UIFont *)font
{
if ([_font isEqual:font]) return;
_font = font;
_fontAttrs = nil; [self resetPageData];
}

-  (NSDictionary*)fontAttrs
{
if (fontAttrs==nil)
{
NSMutableParagraphStyle *style = [NSMutableParagraphStyle new]; style.lineBreakMode = NSLineBreakByWordWrapping;
fontAttrs = @{
NSFontAttributeName: self.font, NSParagraphStyleAttributeName: style
};
}
return fontAttrs;
}

-  (void)setViewSize:(CGSize)viewSize
{
if (CGSizeEqualToSize(_viewSize,viewSize)) return;
_viewSize = viewSize; [self resetPageData];
}

-  (NSUInteger)lastKnownPage
{
return  lastPageWithContent;
}

#define SpanRange(LOCATION,LENGTH)  \
({  NSUInteger  loc_=(LOCATION);  NSMakeRange(loc_,(LENGTH)-loc_);  })

-  (NSRange)rangeOfTextForPage:(NSUInteger)page
{
if (ranges.count>=page)
return  [ranges[page-1] rangeValue];

CGSize constraintSize = _viewSize;
CGFloat  targetHeight = constraintSize.height; constraintSize.height = 32000;

NSRange textRange = NSMakeRange(0,0); if (page!=1)
textRange.location = NSMaxRange([self  rangeOfTextForPage:page-1]);
NSCharacterSet  *wordBreakCharSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];



while   (textRange.location<_bookText.length &&
[wordBreakCharSet characterIsMember:[_bookText  characterAtIndex:textRange.location]])
{
textRange.location += 1;
}

CGSize textSize = CGSizeMake(0,0); CGRect textBounds;
NSCharacterSet *paraCharSet  = [NSCharacterSet characterSetWithCharactersInString:@"\r"]; while   (textSize.height<targetHeight)
{
NSRange paraRange  = [_bookText rangeOfCharacterFromSet:paraCharSet options:NSLiteralSearch
range:SpanRange(NSMaxRange(textRange),_bookText.length)]; if (paraRange.location==NSNotFound)
break;

textRange.length = NSMaxRange(paraRange)-textRange.location; NSString   *testText = [_bookText substringWithRange:textRange]; textBounds = [testText  boundingRectWithSize:constraintSize
options:NSStringDrawingUsesLineFragmentOrigin attributes:self.fontAttrs context:[NSStringDrawingContext new]];
textSize = textBounds.size;
}

while   (textSize.height>targetHeight)
{
NSRange wordRange = [_bookText rangeOfCharacterFromSet:wordBreakCharSet options:NSBackwardsSearch
range:textRange];
if (wordRange.location==NSNotFound) break;
textRange.length = wordRange.location-textRange.location; NSString   *testText = [_bookText substringWithRange:textRange]; textBounds = [testText  boundingRectWithSize:constraintSize
options:NSStringDrawingUsesLineFragmentOrigin attributes:self.fontAttrs context:[NSStringDrawingContext new]];
textSize = textBounds.size;
}

if (textRange.length!=0) lastPageWithContent = page;

[ranges  addObject:[NSValue valueWithRange:textRange]]; return  textRange;
}

-  (BOOL)availablePage:(NSUInteger)page
{
if (page==1) return YES;
NSRange textRange = [self rangeOfTextForPage:page]; return (textRange.length!=0);
}

-  (NSString*)textForPage:(NSUInteger)page
{
return  [_bookText substringWithRange:[self  rangeOfTextForPage:page]];
}

@end

The details of how WLPaginator works isnt important to this chapter, but if you’re curious read the comments in the finished project. Conceptually, its straightforward. The paginator object is configured with three pieces ofinformation: the complete text of the book, the font it will be drawn in, and the size of the text view that displays a page. The object then splits up the text of the book into ranges, each range filling one page. Any view controller object can then ask the paginator for the text that fits on its page.
Coding the Page View Data Source

You finally get to the heart of the page view controller: the page view data source. A page view controller data source must conform to the UIPageViewControllerDataSource protocol and implement these two requiredmethods:

-  pageViewController:viewControllerBeforeViewController:
-  pageViewController:viewControllerAfterViewController:

The page view starts out with an initial view controller to display. When the user “flips” the page to the right or left, the page view controller sends the data source object one of these messages, depending on the direction of thepage turn. The data source, using the current view controller as reference, retrieves or creates the view controller that will display the next (or previous) page. If there is no page, it returns nil.

Your data source must implement these methods. It also needs a readonly property that returns the single paginator object used by all of the individual view controllers and a method to create the view controller for an arbitrary page. Your WLBookDataSource.h file, therefore, looks like this:

#import  "WLPaginator.h"
#import  "WLOnePageViewController.h"

@interface WLBookDataSource  : NSObject  <UIPageViewControllerDataSource>



@property  (readonly,nonatomic) WLPaginator  *paginator;

-  (WLOnePageViewController*)pageViewController:pageViewController loadPage:(NSUInteger)page;

@end

Now switch to the WLBookDataSource.m implementation file. You need an instance variable to store the single paginator object, so add this before the @implementation  section:

@interface WLBookDataSource  ()
{


}
@end

WLPaginator  *paginator;


In the @implementation  section, write a getter method for the paginator property that lazily creates the object:

-  (WLPaginator*)paginator
{
if (paginator==nil)
{
paginator = [WLPaginator  new];
paginator.font = [UIFont   fontWithName:@"Times New  Roman" size:18];
}
return paginator;
}

The -pageViewController:loadPage: method is the workhorse of this data source. Add it now:

-  (WLOnePageViewController*)pageViewController:(UIPageViewController*)pageViewController loadPage:(NSUInteger)page
{
if (page<1  || ![paginator availablePage:page]) return nil;

WLOnePageViewController  *controller;
controller = [pageViewController.storyboard 
instantiateViewControllerWithIdentifier:@"OnePage"]; controller.paginator = self.paginator;
controller.pageNumber = page; return controller;
}

This method returns a configured WLOnePageViewController for any page in the book.
It works by checking to see if the requested page number is in the book. If not, it returns nil.

It then asks the storyboard object to create the controller and views contained in the scene with the identifier “OnePage.” This is done because segues and actions arent used to navigate between view controllers in a page view. Its up to the data source to create them when requested.

Once it has a new one page view controller object, it connects it to the paginator and sets the page number it should display.

All thats left to do is to implement the two required data source protocol methods. These also go in your WLBookDataSource.m file:

-  (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
NSUInteger  currentPageNumber = ((WLOnePageViewController*)viewController).pageNumber; return [self pageViewController:pageViewController  loadPage:currentPageNumber+1];
}

-  (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
NSUInteger  currentPageNumber = ((WLOnePageViewController*)viewController).pageNumber; return [self pageViewController:pageViewController loadPage:currentPageNumber-1];
}

Since each one page view controller stores the page number it displays, all these two methods have to do is request the page after, or before, the current one.


Initializing a Page View Controller

Your book implementation is almost complete. The only thing left to do is perform some initial setup of the page view controller and data model when the page view controller is created. Switch to the WLBookViewController.mimplementation file. Begin by creating an instance variable to store the data source object, by adding this #import statement and instance variable to the private @interface WLBookViewController  () section at the beginning of thefile (new code in bold):

#import  "WLBookDataSource.h"

@interface WLBookViewController  ()
{
WLBookDataSource  *bookSource;
}
@end
Find the -viewDidLoad method, and add the rest of the code in this section, starting with:

self.dataSource = bookSource  = [WLBookDataSource new];

This creates, and retains, the data source object and makes it the data source for this page view controller.

NSURL *bookURL   = [[NSBundle  mainBundle]   URLForResource:@"Alice" withExtension:@"txt"];
NSString   *text = [NSString  stringWithContentsOfURL:bookURL encoding:NSUTF8StringEncoding
error:NULL];
bookSource.paginator.bookText = text;

This next block of code reads in the text of the book, stored in the Alice.txt file that was one of the resource files you added at the beginning. The file is a UTF-8 encoded text file with each line separated by a single carriagereturn (U+000d) character. This format is what the paginator expects. The entire text is read into a single string and stored in the paginator, which will use it to assign text to individual pages.

[self setViewControllers:@[[bookSource pageViewController:self loadPage:1]] direction:UIPageViewControllerNavigationDirectionForward animated:NO
completion:nil];

This last statement is probably the most important. It creates the initial view controller that the page view controller will present. This must be done, programmatically, before the page view controller appears.

That was a lot of code, but you’re done! Run your app and test out the third tab, as shown in Figure 12-26.


Figure 12-26.  Working page view interface

You’ve created a truly complex app. You were aided, in part, by storyboards that allowed you to map out and define much of your apps navigation in a single file. But you also learned how to load storyboard scenesprogrammatically when you need to.

I encourage you to take a moment and review the scenes in your storyboard file and the classes
you created to support them. Once you’re comfortable that you understand the organization of your view controllers, how they work together, and the roles of the individual classes you created, you can consider yourself an iOSnavigation engineer, first class.


Using Pop-Over Controllers

Theres one oddball navigation class, the pop-over controller (UIPopoverController). The
pop-over controller is not a view controller. Its a utility class that presents a view controller in a floating window, on top of an existing view controller. The first view controller never goes away, but is disabled while the pop-oversview controller is active. In this sense, its work like any other modal view controller.

Coding a pop-over isnt difficult. You already did it in Chapter 7. Now that you know a lot more about view controllers, you might want to skip back and review the code you added to present the media picker interface on theiPad.

You can also create pop-over transitions using storyboard segues. When you create a modal segue in an iPad storyboard, you get the added option of popover. Just choose this segue type and the storyboard will wrap your view controller in a pop-over controller to present it.


Advanced Navigation

You have dived deep into the sea of iOS navigation, but you havent covered everything. To go further, I suggest starting with two key documents: View Controller Programming Guide for iOS and the View Controller Catalogfor iOS in the Xcode Documentation and API Reference. The first explains the nitty-gritty details about view controllers and navigation in general. The second describes each of the individual view controller subclasses, andhow to use them.

Summary

You’ve traveled far in your quest to master iOS app development. In all but the simplest apps, navigation is an important part of your design, every bit as important as what your app does.

In this chapter you gained experience using all of the major view controller classes: UIViewController, UITableViewController, UINavigationController, UITabBarController, and UIPageViewController. More importantly, you learned thedifference between content and container view controllers, and how to assemble and connect them using storyboards. You also learned the fundamentals of presenting modal view controllers. You added some new tricks to your table view skills; you learned how to create a table view using a storyboard, configure its cell object, create row selection segue, and use the storyboard methods for preparing the details view controller. You created view controllers stored in a storyboard file programmatically, and used that to create a page view controller data source.

This is a huge accomplishment. Its so exciting that you should share it with your friends. The next chapter will show you how to do just that.

2 comments: