UIPageViewController类可以构建出一种类似书籍的界面,并且分别用多个视图控制器来表示书中的每一页。用户可以通过滑动手势来翻页,也可以点击页面边缘,将书翻到下一页或者上一页。也可以创建一种平坦的滚动界面。
我们要用代码来定制页面视图控制器的样貌和行为。其中有一些关键属性可以指定多个页面同时显示时的方式以及每个页面背后的内容等。下面简单介绍一下由苹果公司所提供的属性:
1、transitionStyle属性用来指定两个视图控制器之间的切换效果。页面视图控制器只支持翻页和滚动显示两种风格
2、控制器的doubleSided属性决定了内容是出现在同一页的正反两面还是只出现在其中一面。假如要并排显示两页内容,那么就应该开启该属性。否则,有一半的页面都看不见,因为页面的背面无法出现在主视图空间内。书的排版由书脊位置来控制。
3、通过spineLocation属性,可以把书脊设置到屏幕左右两侧、上下两端或页面正中。三个相关的常数值分别是UIPageViewControllerSpineLocationMin(对应于顶端或左侧)、UIPageViewControllerSpineLocationMax(对应于底端或右侧)及UIPageViewControllerSpineLocationMid(对应于中心)。前两种常量会产生单页展示风格,后一种用来同时显示两页内容。设备方向改变时,系统会调动名为pageViewController:spineLocationForInterfaceOrientation:的委托方法,开发者应该在该方法中返回所选的spineLocation属性,令控制器可以根据当前设备方向来更新其视图。
4、navigationOrientation属性用来指定这本书是左右翻页还是上下翻页。UIPageViewControllerNavigationOrientationHorizontal表示左右翻页UIPageVIewControllerNavigationOrientationVertical表示上下翻页。对于上下翻页的书来说,页面应该是上下翻动的,而不是像平常那样左右翻动。
@protocol BookControllerDelegate
- (id)viewControllerForPage:(NSInteger)pageNumber;
- (NSInteger)numberOfPages;
- (void)bookControllerDidTurnToPage:(NSNumber *)pageNumber;
@end
@interface BookController : UIPageViewController
+ (instancetype)bookWithDelegate:(id)theDelegate style:(BookLayoutStyle)style;
- (void)moveToPage:(NSUInteger)requestedPage;
- (NSInteger)currentPage;
@property (nonatomic,weak) id bookDelegate;
@property (nonatomic,assign) NSUInteger pageNumber;
@property (nonatomic,assign) BookLayoutStyle layoutStyle;
@end
@implementation BookController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (NSInteger)currentPage{
NSInteger pageCheck = ((UIViewController *)[self.viewControllers objectAtIndex:0]).view.tag;
return pageCheck;
}
- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController
{
return 0;
}
- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController
{
if (_bookDelegate && [_bookDelegate respondsToSelector:@selector(numberOfPages)]) {
return [_bookDelegate numberOfPages];
}
return 0;
}
- (BOOL)useSideBySide:(UIInterfaceOrientation)orientation
{
BOOL isLandscape = UIInterfaceOrientationIsLandscape(orientation);
switch (_layoutStyle) {
case BookLayoutStyleHorizontalScoll:
case BookLayoutStyleVerticalScoll: return NO;
case BookLayoutStyleFlipBook: return isLandscape;
default:
return isLandscape;
}
}
- (void)updatePageTo:(NSUInteger)newPageNumber
{
_pageNumber = newPageNumber;
[[NSUserDefaults standardUserDefaults] setInteger:_pageNumber forKey:@"bookpage"];
[[NSUserDefaults standardUserDefaults] synchronize];
if (_bookDelegate && [_bookDelegate respondsToSelector:@selector(bookControllerDidTurnToPage:)]) {
[_bookDelegate bookControllerDidTurnToPage:@(_pageNumber)];
}
}
- (UIViewController *)controllerAtPage:(NSInteger)pageNumber
{
if (_bookDelegate && [_bookDelegate respondsToSelector:@selector(viewControllerForPage:)]) {
UIViewController *controller = [_bookDelegate viewControllerForPage:pageNumber];
controller.view.tag = pageNumber;
return controller;
}
return nil;
}
- (void)fetchControllerForPage:(NSUInteger)requestPage orientation:(UIInterfaceOrientation)orientation
{
BOOL sideByside = [self useSideBySide:orientation];
NSInteger numberOfPageNeeded = sideByside ? 2 : 1;
NSInteger currentCount = self.viewControllers.count;
NSUInteger leftPage = requestPage;
if (sideByside && (leftPage % 2)) {
leftPage = floor(leftPage / 2) * 2;
}
if (currentCount && (currentCount == numberOfPageNeeded)) {
if (_pageNumber == requestPage) {
return;
}
if (_pageNumber == leftPage) {
return;
}
}
UIPageViewControllerNavigationDirection direction = (requestPage > _pageNumber) ? UIPageViewControllerNavigationDirectionForward : UIPageViewControllerNavigationDirectionReverse;
NSMutableArray *pageControllers = [NSMutableArray array];
[pageControllers addObject:[self controllerAtPage:leftPage]];
if (sideByside) {
[pageControllers addObject:[self controllerAtPage:leftPage + 1]];
}
[self setViewControllers:pageControllers direction:direction animated:YES completion:nil];
[self updatePageTo:leftPage];
}
- (void)moveToPage:(NSUInteger)requestedPage
{
[self fetchControllerForPage:requestedPage orientation:(UIInterfaceOrientation)self.interfaceOrientation];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
[self updatePageTo:_pageNumber + 1];
return [self controllerAtPage:(viewController.view.tag + 1)];
}
- (UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation
{
NSUInteger indexOfCurrentViewController = 0;
if (self.viewControllers.count) {
indexOfCurrentViewController = ((UIViewController *)[self.viewControllers objectAtIndex:0]).view.tag;
}
[self fetchControllerForPage:indexOfCurrentViewController orientation:orientation];
BOOL sideBySide = [self useSideBySide:orientation];
self.doubleSided = sideBySide;
UIPageViewControllerSpineLocation spineLocation = sideBySide ? UIPageViewControllerSpineLocationMid : UIPageViewControllerSpineLocationMin;
return spineLocation;
}
+ (instancetype)bookWithDelegate:(id)theDelegate style:(BookLayoutStyle)style
{
UIPageViewControllerNavigationOrientation orientation = UIPageViewControllerNavigationOrientationHorizontal;
if (style == BookLayoutStyleFlipBook || style == BookLayoutStyleVerticalScoll) {
orientation = UIPageViewControllerNavigationOrientationVertical;
}
UIPageViewControllerTransitionStyle transitionStyle = UIPageViewControllerTransitionStylePageCurl;
if (style == BookLayoutStyleHorizontalScoll || style == BookLayoutStyleVerticalScoll) {
transitionStyle = UIPageViewControllerTransitionStylePageCurl;
}
BookController *bc = [[BookController alloc] initWithTransitionStyle:transitionStyle navigationOrientation:orientation options:nil];
bc.layoutStyle = style;
bc.dataSource = bc;
bc.delegate = bc;
bc.bookDelegate = theDelegate;
return bc;
}
@end
该类会给每个页面编号,而且会把上一页下一页这种实现细节以及处理屏幕方向变更事件所用的代码封装起来。所以自定义了名叫BookControllerDelegate的委托协议,系统向实现了该协议的对象发送viewControllerForPage:消息时,对象负责返回与给定页码相对应的控制器。这就简化了实现方式,使得调用BookController类的应用程序只需处理这一个方法就好,在该方法里,我们可以手工构建控制器,也可以从故事板中加载控制器。
在使用BookController类时,需要创建控制器,并将BookController对象声明成它的子视图控制器,然后把BookController的view添加成它的子视图。把BookController添加为子控制器之后,他就能收到与屏幕方向及内存状态有关的事件了。下一个解决方案将会详细讨论视图控制器之间的这种关系。最后把初始的页码设置好就行了。下面这段就是BookController的用法:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
if (!_bookController) {
_bookController = [BookController bookWithDelegate:self style:BookLayoutStyleBook];
}
_bookController.view.frame = self.view.bounds;
[self addChildViewController:_bookController];
[self.view addSubview:_bookController.view];
[_bookController didMoveToParentViewController:self];
[_bookController moveToPage:0];
}
创建BookController的那个便捷方法还可以接受第二个参数,提供了四种构建电子书所用的样式:一种是左右翻页的传统样式,还有一种是上下翻页的书:
typedef NS_ENUM(NSUInteger, BookLayoutStyle) {
BookLayoutStyleBook,
BookLayoutStyleFlipBook,
BookLayoutStyleHorizontalScoll,
BookLayoutStyleVerticalScoll,
};
在竖屏模式下,对于标准样式的书,其书脊是竖着摆放在屏幕左侧的,而在横屏模式下,书脊则会竖着摆放屏幕正中,这样就会并排显示出两页内容。这是西方书籍的标准翻页方式,用户可以从左向右或者从右向左翻页。
而翻转形式的书,其书脊则是横着摆放的,在横屏模式下,书脊横放于屏幕顶端,每次只能显示一页内容。而在竖屏模式下,书脊则横着摆在屏幕中央,这样就能显示出上下两页内容了。
剩下的两种滚动展示风格可以令用户通过水平滚动或垂直滚动 的方式来查看不同的页面。如果使用了滚动风格的版式,那么就不能同时显示两页内容了。
我们可以在viewWillDisappear方法中清理BookController,将其从上级视图中移除:
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[_bookController willMoveToParentViewController:nil];
[_bookController.view removeFromSuperview];
[_bookController removeFromParentViewController];
}
为了处理与委托及数据源有关的任务,给每个视图控制器都编了页码,并将其设为tag。通过这个页码,我们可以知道当前显示出来的是哪一页,而且还能够命令BookControllerDelegate去制作接下来要显示的视图控制器。
页面控制器本身的viewControllers数组里总是存有0个,1个或者两个页面。0个页面表明控制器还没有配置好。一个页面只用于书脊在屏幕边缘时的情况,而两个页面则适用于书脊在屏幕正中的情况。假如页面数量与书脊的位置不相匹配,那么程序在运行的时候就会出错。
展示在各个页面里的控制器是由两个数据源方法提供的,BookController类分别实现了这两个方法表示上一页和下一页的回调方法。在页面控制器的传统实现方式中,我们用控制器之间的前后关系来描述它们,而不采用页码来描述。但这条解决方案所编写的BookController类会把这种关系替换成简单的数字,并且会向BookControllerDelegate查询与某个页面相对应的页面。
useSideBySide:方法会根据屏幕方向来决定书脊的位置,并决定屏幕上同时可以显示几个控制器。这个方案解决了实现的代码会在横屏模式下并排 显示两页,而在竖屏模式下只显示一页。可以根据自己的程序来修改这个段代码。比方说,可能觉得在iPhone上面无论横屏还是竖屏,都只应该显示一页才对,这样可以令文本看起来更加清晰。
用户可以用滑动手势翻页,也可以通过点击屏幕来翻页,而应用程序则可以发送moveToPage:请求。这样一来,我们就能在UIPageViewController的手势识别器之外,多添加一种控制翻页的方式。