原文地址:http://www.ioslearner.com/implementing-uipageviewcontroller-programatically-without-storyboarding/
Recently I tried the new feature introduced in iOS 5.0 – The UIPageViewController. Apple has provided an already built in template for this – Page-Based Application. But the default template uses storyboarding. So, in this tutorial, we will see how to implement a UIPageViewController without storyboarding.
Ok so lets get started!
Initial Project setup:
Bump up your Xcode and create a new Project, and choose Single View Application from the template. Make sure that the device family you choose is iPad and the Use Storyboard option is unchecked as shown below.
We will now make our view controller class which will have the content. In this simple application, we will just have a page with a label denoting the number of that page, and finally our application will look like this:
Make a new UIViewController subclass and name it ContentViewController. Open the ContentViewController.xib and do the following:
(a) Change the background color of the view to gray color.
(b) Add a view from the objects window, and change its color as in the screenshot after point (c). Also set the autoresizing mask of the view as follows:
(c) Add a label to the ContentViewController.xib. Set the autoresizing mask for the label as follows:
Now, Create an outlet for the label by opening the Assistant Editor and Ctrl+Drag from the label onto the ContentViewController.h as follows:
(d) Create an NSString property in ContentViewController.h as follows:
@property (strong, nonatomic) NSString *labelContents; |
Also synthesize the property in the implementation. This NSString property will hold the contents of our label. And so in our viewDidLoad, we write the following:
- (void)viewDidLoad { [super viewDidLoad]; self.myLabel.text = self.labelContents; } |
Ok. So now our project setup is complete. We now start with the UIPageViewController stuff!
UIPageViewController Implementation:
We create two properties in our ViewController.h file as follows:
@property (nonatomic, strong) UIPageViewController *pageViewController; @property (nonatomic, strong) NSMutableArray *modelArray; |
and synthesize them in the implementation.
We will also instantiate our modelArray, which will store the contents that will be displayed in our ContentViewController label.
self.modelArray = [[NSMutableArray alloc] init]; for (int index = 1; index |
To add the UIPageViewController into our app, we will follow these simple steps:
Step 1: Instantiate the UIPageViewController
self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStylePageCurl navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil]; |
Here, for the transitionStyle, we give UIPageViewControllerTransitionStylePageCurl. This is the transition style when we transition between our views.
We give navigationOrientation as UIPageViewControllerNavigationOrientationHorizontal. This specifies that we want Left <-> Right direction in which the navigation of our pages will happen. If we give UIPageViewControllerNavigationOrientationVertical, the navigation will happen Top <-> Bottom, as in a wall calendar.
Last is the options dictionary. Here, we pass in as nil. We can give the default spine location here, if required.
Step 2: Assign the delegate and datasource as self. Also conform to UIPageViewControllerDelegate protocol in the ViewController.h file.
self.pageViewController.delegate = self; self.pageViewController.dataSource = self; |
Step 3: Next, we set the initial view controller as follows:
ContentViewController *contentViewController = [[ContentViewController alloc] initWithNibName:@"ContentViewController" bundle:nil]; contentViewController.labelContents = [self.modelArray objectAtIndex:0]; NSArray *viewControllers = [NSArray arrayWithObject:contentViewController]; [self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil]; |
Here, we instantiate our ContentViewController and add it to an array. Then we assign that array using this UIPageViewController instance method:
- (void)setViewControllers:(NSArray *)viewControllers direction:(UIPageViewControllerNavigationDirection)direction animated:(BOOL)animated completion:(void (^)(BOOL finished))completion; |
We pass in the direction as UIPageViewControllerNavigationDirectionForward, which is again for Left <-> Right direction. We specify No for animated, because we are setting our initial viewControllers, so we don’t want to animate it. And then we specify nil for the completion block.
Step 4: We now do the ViewController containment stuff.
(a) Add the pageViewController as the childViewController of our ViewController:
[self addChildViewController:self.pageViewController];<span class="Apple-style-span" style="font-family: Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif; font-size: 13px; line-height: 19px; white-space: normal;"> </span> |
(b) Add the view of the pageViewController to the current view:
[self.view addSubview:self.pageViewController.view]; |
(c) Call didMoveToParentViewController: of the childViewController, the UIPageViewController instance in our case:
[self.pageViewController didMoveToParentViewController:self]; |
Step 5: Though this is not mandatory, we set the pageViewController’s frame as an inset rect, so that we are able to distinguish between our view and the pageViewController’s view.
CGRect pageViewRect = self.view.bounds; pageViewRect = CGRectInset(pageViewRect, 40.0, 40.0); self.pageViewController.view.frame = pageViewRect; |
Step 6:Assign the gestureRecognizers property of our pageViewController to our view’s gestureRecognizers property. This will enable touch gestures to transition between the pages:
self.view.gestureRecognizers = self.pageViewController.gestureRecognizers; |
Now, if we run the application, we can see our view controller with But, if we swipe left to right, the views aren’t changing i.e. the gesture recognizers aren’t working.
So to make that happen, we have to conform our class to the UIPageViewControllerDataSource protocol and implement the following two methods:
(1) - (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
viewControllerBeforeViewController:(UIViewController *)viewController
(2) - (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
viewControllerAfterViewController:(UIViewController *)viewController
We write this code in our ViewController.m file:
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController { NSUInteger currentIndex = [self.modelArray indexOfObject:[(ContentViewController *)viewController labelContents]]; if(currentIndex == 0) { return nil; } ContentViewController *contentViewController = [[ContentViewController alloc] init]; contentViewController.labelContents = [self.modelArray objectAtIndex:currentIndex - 1]; return contentViewController; } - (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController { NSUInteger currentIndex = [self.modelArray indexOfObject:[(ContentViewController *)viewController labelContents]]; if(currentIndex == self.modelArray.count-1) { return nil; } ContentViewController *contentViewController = [[ContentViewController alloc] init]; contentViewController.labelContents = [self.modelArray objectAtIndex:currentIndex + 1]; return contentViewController; } |
The above two methods will be called every time the user swipes left or right, and so we have to return the suitable view controller that will be presented. For demo purposes, here we just create an instance of our ContentViewController, see the currentIndex of the contents, and set the labelContents appropriately.
In a real life scenario, this part would differ, and instead of allocating a new instance of our ContentViewController, we could have a better logic, like having an array of say 3 controllers, and reusing them. But to avoid complexity, and with the intent of just demonstrating on how the UIPageViewController works, I have just allocated a new instance every time.
I we run the application now, we see that the transition is now happening between the pages!
But if we rotate to landscape orientation, we don’t see a partition, right. For that, we have to implement the UIPageViewControllerDelegate method:
- (UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation
and return the UIPageViewControllerSpineLocation according to the interface orientation.
The spine is actually a kind of binding we see in a notebook. It can be at three positions:
(a) UIPageViewControllerSpineLocationMin – The binding is on the left side.
(b) UIPageViewControllerSpineLocationMid – The binding is on the mid. This would be foe Landscape orientations, with like pages on both sides
and (c) UIPageViewControllerSpineLocationMax – The binding is on the right side.
We just write this code in our ViewController implementation:
- (UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation { if(UIInterfaceOrientationIsPortrait(orientation)) { // In portrait orientation: Set the spine position to "min" and the page view controller's view controllers array to contain just one view controller. Setting the spine position to 'UIPageViewControllerSpineLocationMid' in landscape orientation sets the doubleSided property to YES, so set it to NO here. UIViewController *currentViewController = [self.pageViewController.viewControllers objectAtIndex:0]; NSArray *viewControllers = [NSArray arrayWithObject:currentViewController]; [self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL]; self.pageViewController.doubleSided = NO; return UIPageViewControllerSpineLocationMin; } else { // In landscape orientation: Set set the spine location to "mid" and the page view controller's view controllers array to contain two view controllers. If the current page is even, set it to contain the current and next view controllers; if it is odd, set the array to contain the previous and current view controllers. NSArray *viewControllers = nil; ContentViewController *currentViewController = [self.pageViewController.viewControllers objectAtIndex:0]; NSUInteger currentIndex = [self.modelArray indexOfObject:[(ContentViewController *)currentViewController labelContents]]; if(currentIndex == 0 || currentIndex %2 == 0) { UIViewController *nextViewController = [self pageViewController:self.pageViewController viewControllerAfterViewController:currentViewController]; viewControllers = [NSArray arrayWithObjects:currentViewController, nextViewController, nil]; } else { UIViewController *previousViewController = [self pageViewController:self.pageViewController viewControllerBeforeViewController:currentViewController]; viewControllers = [NSArray arrayWithObjects:previousViewController, currentViewController, nil]; } //Now, set the viewControllers property of UIPageViewController [self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL]; return UIPageViewControllerSpineLocationMid; } } |
and Bingo! When we run the app, in landscape orientation, we see a partition.
So lets scrutinize what we did in the delegate method.
We first set the viewControllers property of our pageViewController according to the interface orientation. We return an array of just one viewController if the orientation is portrait. In case of landscape, we return an array of two view controllers. If the current page is even, we set the array to contain the current and next view controllers; if it is odd, then we set the array to contain the previous and current view controllers.
We then set the spine location appropriately. We set it as UIPageViewControllerSpineLocationMin in portrait and UIPageViewControllerSpineLocationMid in landscape.
An important thing to note: When we set the spine location to UIPageViewControllerSpineLocationMid,the doubleSided property of the pageViewController is automatically set to YES. This means that the content on page front will not partially show through back. But when this property is set to NO, the content on page front will partially show through back, giving the page a translucent kind of effect. So, in the portrait orientation, we have to set the value to NO, otherwise it would result in an exception.
We can also zoom in and out in a UIPageViewController. Please comment and let me know if you need a tutorial on that.
As usual, you can download all the sample code of this tutorial here: