多页面混合开发利器之UIPageViewController
疑问
不知道大家发开时有没遇到这样一个需求,一个页面当中可以进行条件或者内容切换然后进行不同详情内容的显示,当内容样式差不多的时候我们还可以进行替换数据源和不同的 UITableViewCell 进行处理。但是,当我们遇到每个详情内容页面的风格差异很大的时候该如何处理呢?比如下面这组图(图片来源于手机京东iOS客户端)
1.jpg 2.png好吧,也许有些人看这样的界面说我可以做到,不就是简单的欢欢换换数据源和替换不同样式的 UITableViewCell 或者 UICollectionViewCell 吗?但是我弱弱地问下,如果这个界面包含了五种?六种或者更多的不同数据源呢?每个界面的业务逻辑稍有差异,在这样的情况下还采取这样的开发方式........................=.=(心中一万只草泥马跑过)这样既违反了软件编程本身的低耦合高内聚,同时也不便于日后的维护和扩展。
代替方案
思路
想一下,其实就算我们是一个单一的页面时,页面上还是不是依旧充满了许多控件吗?我们可以利用这种封装的思想,将我们不同要显示的具体子界面拆分成不同的 UIViewController.view 进行显示,既然思路有了那就该着手实现的具体方案了,目前我所想到的有两种方案,如果你还有什么更好的方案欢迎指出:
1.使用 UIPageViewController 进行管理
2.在 UIScrollView 的基础上自行加载各自 UIViewController.view 并进行相对应的管理
接下来我会在这种两种方案上进行对比分析,既然今天的主角是 UIPageViewController 那我们就先开始介绍它吧,不然各位看官看了那么多废话还没进入主题 ,Let's do IT ^ . ^
UIPageViewController
介绍
0.png此控件为使用者提供了在不同内容界面之间跳转功能,并且不同界面之间管理着自己独自的 UIViewController,同时该控件不仅已经为用户提供了跳转手势(滑动)也支持代码上的跳转管理,而且用户也可指定界面之间的跳转动画方式
听起来蛮符合我们的要求的嘛😄
1.初始化
3.png初始化系统提供了两种翻页动画:
UIPageViewControllerTransitionStylePageCurl = 0, 类书籍翻页效果
UIPageViewControllerTransitionStyleScroll = 1 滚动效果
[self addChildViewController:self.pageViewController];
[self.pageViewController didMoveToParentViewController:self];
[self.view addSubview:self.pageViewController.view];
- (UIPageViewController *)pageViewController {
if (!_pageViewController) {
_pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
options:nil];
_pageViewController.view.frame = CGRectMake(0, 64, SCREEN_WIDTH, SCREEN_HEIGHT - 64);
_pageViewController.dataSource = self;
_pageViewController.delegate = self;
[_pageViewController setViewControllers:@[self.childViewControllersArray[_currentIndex]] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
}
return _pageViewController;
}
初始化的时候需要设置一个当前显示 UIViewController,在这里我是利用一个数组管理所有的子页面 childViewControllersArray
- (NSArray<UIViewController *> *)childViewControllersArray {
if (!_childViewControllersArray) {
_childViewControllersArray = @[[[ViewControllerA alloc] init],
[[ViewControllerB alloc] init],
[[ViewControllerC alloc] init],
[[ViewControllerD alloc] init]];
}
return _childViewControllersArray;
}
2.设置代理
#pragma mark - UIPageViewControllerDataSource
//当前界面的上一个界面,该代理在手势操作时便触发(轻微滑动),并且应该是有某种缓存机制,同一界面的第二次手势操作不触发
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {
NSInteger beforeIndex = _currentIndex - 1;
//返回nil时禁止继续滑动
if (beforeIndex < 0) return nil;
return self.childViewControllersArray[beforeIndex];
}
//当前界面的下一个界面,机理同上
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
NSInteger afterIndex = _currentIndex + 1;
if (afterIndex > self.childViewControllersArray.count - 1) return nil;
return self.childViewControllersArray[afterIndex];
}
#pragma mark - UIPageViewControllerDelegate
//跳转动画开始时触发,利用该方法可以定位将要跳转的界面
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray<UIViewController *> *)pendingViewControllers {
//pendingViewControllers虽然是一个数组,但经测试证明该数组始终只包含一个对象
_pengdingViewController = pendingViewControllers.firstObject;
}
//跳转动画完成时触发,配合上面的代理方法可以定位到具体的跳转界面,此方法有利于定位具体的界面位置(childViewControllersArray),便于日后的管理
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray<UIViewController *> *)previousViewControllers transitionCompleted:(BOOL)completed {
//previousViewControllers虽然是一个数组,但经测试证明该数组始终只包含一个对象
if (completed) {
[self.childViewControllersArray enumerateObjectsUsingBlock:^(UIViewController * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (_pengdingViewController == obj) {
_currentIndex = idx;
*stop = YES;
}
}];
}
}
代理设置完成同时自定义了一个顶部选择栏来达到类似手机京东的效果,自定义控件的细节这里就不细讲了,具体可以看最后放出的 Demo
最后效果图
UIPageVIewController.gif运行Demo并验证相关疑问
1.各种的子界面是否会在 APP 加载完成时就完全加载呢?
为了验证这个问题,我们将对所有子 UIViewController 的 viewDidLoad 方法进行监控:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor colorWithRed:arc4random()%256/255.0 green:arc4random()%256/255.0 blue:arc4random()%256/255.0 alpha:1.0];
NSLog(@"%@ %s", NSStringFromClass([self class]), __func__);
}
结果:
4.png可以看到在 APP 完成初始化的时候只有第一个子UIViewController调用了 ** viewDidLoad** 进行了具体相关加载,我们再左右滑动看下结果如何 5.png
只有显示了的子 UIViewController, APP 才会进行相关内容的加载,这类似于懒加载机制,节约了对系统内存的开销
基于UIScrollerView实现
现在再来让我们看看第二种方案的结果如何
具体实现还是比较简单的知识利用了 UIScrollerView 加载各种子 UIViewController,具体代码实现如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
_currentIndex = 0;
#ifdef usingPageViewController
[self addChildViewController:self.pageViewController];
[self.pageViewController didMoveToParentViewController:self];
[self.view addSubview:self.pageViewController.view];
#else
[self.view addSubview:self.scrollerView];
#endif
[self.view addSubview:self.segmentView];
}
- (UIScrollView *)scrollerView {
if (!_scrollerView) {
_scrollerView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 64, SCREEN_WIDTH, SCREEN_HEIGHT - 64)];
_scrollerView.contentSize = CGSizeMake(SCREEN_WIDTH * self.childViewControllersArray.count, SCREEN_HEIGHT - 64);
_scrollerView.pagingEnabled = YES;
__weak typeof(self) weakSelf = self;
[self.childViewControllersArray enumerateObjectsUsingBlock:^(UIViewController * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
obj.view.frame = CGRectMake(SCREEN_WIDTH * idx, 0, SCREEN_WIDTH, SCREEN_HEIGHT - 64);
[weakSelf addChildViewController:obj];
[obj didMoveToParentViewController:weakSelf];
[_scrollerView addSubview:obj.view];
}];
}
return _scrollerView;
}
Demo 中已经利用预编译命令实现好了这两种布局方法,大家只要开放或者取消这个宏定义即可
ViewController.m
#define usingPageViewController
在布局方案变更后再回到我们之前的那个疑问中,看下此种方案的系统加载流程是如何的:
6.png可以看到在 APP 初始化完成时,各个子 UIViewController 也完成了其自身的加载,如果子 UIViewController 内容相对复杂的情况下,此种方案对于系统的各项开销还是比较大的
总结
1.无论利用 UIPageViewController 还是 UIScroller 都很好的降低了主界面的耦合性,各自子 UIViewController 都维护着自己的单一模块的功能, UI 等(在包含两个布局方案下,主视图 ViewController 代码行数只有 170 行)
2.但是由于 ** UIPageViewController** 在懒加载的加持下,很好的优化了系统对于内存等方面的压力
3.UIPageViewController 不仅默认提供了两种不同的翻页动画效果还对用户滑动手势有了很好的支持,而且通过其提供的代理回调方法我们也能很便利的对当前滑动页进行管理和监测,这大大提高了工作效率和降低了降低了研发成本
本人也是新手入门如果发现本文有什么错误的地方可以下方留言跟我反馈,哪怕是文章的排版问题还是其他影响到阅读体验的问题都可以反馈我,大家一起探讨如果我确实写的有问题我会欣然接受并改正。同时我的新浪微博是 KeepMoveingOn,有任何问题也都可以@我,我会尽快回复大家的,感谢各位看官花费宝贵的时间阅读此文🙏
Demo地址