SPStackedNav 是全球最大的流音乐服务商 Spotify 开源的一个 iPad 分屏框架,用于 Spotify 的 iPad 版 App 中,网易云音乐 iPad 版 App 也是采用相似的分屏交互方案,该框架的交互表现如下图所示:
使用
根据 GitHub 上面的说明完成项目导入之后,那么就可以开始搭建UI框架了。
创建 SPSideTabController, SPSideTabController 的用法和UITabController的用法没有什么大的区别。
分别创建 SPSideTabController 的 RootViewController,设置 UITabBarItem 属性。
给 SPSideTabController 的 viewControllers 属性赋值对应的 RootViewController 数组。
Demo 的 AppDelegate 代码如下:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor = [UIColor whiteColor];
// 步骤 1 创建 SPSideTabController
self.tabs = [[SPSideTabController alloc] init];
// 步骤 2 分别创建 SPSideTabController 的 RootViewController,设置 UITabBarItem 属性
RootTestViewController *root1 = [RootTestViewController new];
root1.title = @"Root 1";
root1.tabBarItem.image = [UIImage imageNamed:@"114-balloon"];
RootTestViewController *root2 = [RootTestViewController new];
root2.title = @"Root 2";
root2.tabBarItem.image = [UIImage imageNamed:@"185-printer"];
root2.tabBarItem.badgeValue = @"5";
root2.tabBarItem.badgeColor = [UIColor redColor];
RootTestViewController *root3 = [RootTestViewController new];
root3.title = @"Root 3";
root3.tabBarItem.image = [UIImage imageNamed:@"114-balloon"];
// 步骤 3 给 SPSideTabController 的 viewControllers 属性赋值对应的 RootViewController 数组
self.tabs.viewControllers = @[
[[SPStackedNavigationController alloc] initWithRootViewController:root1],
[[SPStackedNavigationController alloc] initWithRootViewController:root2],
[[SPStackedNavigationController alloc] initWithRootViewController:root3]
];
self.window.rootViewController = self.tabs;
[self.window makeKeyAndVisible];
return YES;
}```
5.效果图
![效果图1](http://upload-images.jianshu.io/upload_images/656644-868252e5afa69fbd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
![效果图2](http://upload-images.jianshu.io/upload_images/656644-c413a01681ef5ed1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
### 设计
![View的层次结构](http://upload-images.jianshu.io/upload_images/656644-3ca6c07d584f5a61.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
从图中的 View 层次结构图可以看到,左边的侧边栏 View 是一个 SPSideTabBar,该 SPSideTabBar 包含若干个 SPSideTabItemButton 。右边的容器 View 是一个 SPStackedNavigationScrollView ,该 SPStackedNavigationScrollView 里面包含了若干个 SPStackedPageContainer , 一个 SPStackedPageContainer 可以简单的看做一个ViewController。
当我们在 Demo 项目中的 RootTestViewController 里面 push 一个 ViewController 的时候。其实就相当于往 SPStackedNavigationScrollView 添加一个 SPStackedPageContainer 子 view。SPStackedPageContainer的显示内容来自于 ViewController 的 view 属性。
```CPP
ChildTestViewController *vc = [ChildTestViewController new];
[self.stackedNavigationController pushViewController:vc animated:YES];
SPSideTabBar 和 SPSideTabItemButton 解析
RootTestViewController *root2 = [RootTestViewController new];
root2.title = @"Root 2";
root2.tabBarItem.image = [UIImage imageNamed:@"185-printer"];
root2.tabBarItem.badgeValue = @"5";
root2.tabBarItem.badgeColor = [UIColor redColor];
Demo 代码里面的 AppDelegate 设置的明明是 UITabBarItem 的各类属性, 但是为什么在 SPSideTabBar 里面没有看到关于 UITabBarItem 的信息呢?
再来看看 SPSideTabBar 这个 View 的层级结构图,可以猜出 SPSideTabBar 将 UITabBarItem 的属性设置映射成 SPSideTabItemButton 的属性设置了。
查看 SPSideTabController.m 文件的 viewDidLoad 方法,我们可以看到 _tabBar.items = validItems 这个属性设置方法将 SPSideTabController 的 tabBarItem 的对象数组传给SPSideTabBar 的 items属性。
来到 SPSideTabBar.m 实现文件查看 - (void)setItems:(NSArray*)items 方法
//将 UITabBarItem 数组转成 SPSideTabItemButton 数组
- (void)setItems:(NSArray*)items
{
if ([items isEqual:_items]) return;
self.selectedItem = nil;
_items = [items copy];
for(UIView *b in _itemButtons) [b removeFromSuperview];
self.itemButtons = nil;
if (_items) {
NSMutableArray *itemButtons = [NSMutableArray array];
CGRect pen = CGRectMake(0, 10, 80, 70);
for(UITabBarItem *item in _items) {
//关键步骤 将 UITabBarItem 转成 SPSideTabItemButton
UIView *b = [self buttonForItem:item withFrame:pen];
[itemButtons addObject:b];
[self addSubview:b];
pen.origin.y += pen.size.height + 10;
}
self.itemButtons = itemButtons;
}
}
继续跟踪查看方法
UIView *b = [self buttonForItem:item withFrame:pen];
// 设置 SPTabBarItem 的 frame,并返回 SPTabBarItem 的 View
- (UIView*)buttonForItem:(UITabBarItem*)item withFrame:(CGRect)pen
{
if ([item isKindOfClass:[SPTabBarItem class]] && [(SPTabBarItem*)item view]) {
UIView *view = [(SPTabBarItem*)item view];
[view setFrame:pen];
return view;
}
SPSideTabItemButton *b = [[SPSideTabItemButton alloc] initWithFrame:pen];
// 省略 UITabBarItem 的属性转成 SPSideTabItemButton 的属性过程,
// 具体细节可以详看源码
return b;
}
使用 SPSideTabBar 自定义 View 来替代系统的 UITabBar, 使用 SPTabBarItem 自定义 View 来替代系统的 UITabBarItem,SPSideTabBar 将 UITabBarItem 的属性设置映射到 SPTabBarItem。这个就是常见的自定义 TabBar 的思路。
SPStackedNavigationController 解析
SPStackedNavigationController 继承与 UIViewController,并定义和实现了一系列和 NavigationController 相关的方法,简而言之就是自己实现一个 NavigationController,这里做重讲解2个主要的方法.
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated activate:(BOOL)activate
- (UIViewController *)popViewControllerAnimated:(BOOL)animated;
当 SPStackedNavigationController 做 push 操作的时候,就是往 SPStackedNavigationScrollView 这个仿 ScrollView 的 View 添加一个 SPStackedPageContainer 子View。从上图中的左边的 View 层次结构中可以看到SPStackedNavigationScrollView 里面有2个 SPStackedPageContainer 子 View。而上图中右边的 View 表现正好印证了这个结构。
查看 SPStackedNavigationController.m 文件的 - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated activate:(BOOL)activate 实现方法
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated activate:(BOOL)activate
{
// 省略代码
// 添加 viewController 到 viewControllers 的数组
[self willChangeValueForKey:@"viewControllers"];
[self addChildViewController:viewController];
//将 viewController 添加到 self,
if ([self isViewLoaded])
// 关键步骤 SPStackedNavigationScrollView 添加一个 SPStackedPageContainer 子 View
[self pushPageContainerWithViewController:viewController];
if (activate)
[self setActiveViewController:viewController position:activePosition animated:animated];
// 调用 viewController 生命周期方法
[viewController didMoveToParentViewController:self];
[self didChangeValueForKey:@"viewControllers"];
}
接下来看看 SPStackedNavigationController.m 文件 - (void)pushPageContainerWithViewController:(UIViewController*)viewController 的方法
- (void)pushPageContainerWithViewController:(UIViewController*)viewController
{
CGSize size = self.view.frame.size;
CGRect frame = CGRectMake(self.view.bounds.size.width, 0, 0, size.height);
frame.size.width = (viewController.stackedNavigationPageSize == kStackedPageHalfSize ?
kSPStackedNavigationHalfPageWidth :
size.width);
SPStackedPageContainer *pageC = [[SPStackedPageContainer alloc] initWithFrame:frame VC:viewController];
//SPStackedNavigationScrollView 添加一个 SPStackedPageContainer 子 View
[_scroll addSubview:pageC];
}
从代码中可以验证我们上文所述,当 SPStackedNavigationController 做 push 操作的时候,就是往 SPStackedNavigationScrollView 这个 View 添加一个 SPStackedPageContainer 子 View。
我们现在是否可以这样猜测,当 SPStackedNavigationController 做 pop 操作的时候,就是在 SPStackedNavigationScrollView 这个View 移除一个 SPStackedPageContainer View。
接下来查看 SPStackedNavigationController.m 文件的 - (UIViewController *)popViewControllerAnimated:(BOOL)animated 方法来验证一下我们的猜测。
- (UIViewController *)popViewControllerAnimated:(BOOL)animated
{
UIViewController *viewController = [[self childViewControllers] lastObject];
if (!viewController)
return nil;
[self willChangeValueForKey:@"viewControllers"];
[viewController willMoveToParentViewController:nil];
if ([self isViewLoaded])
{
// 关键步骤 ,将 SPStackedPageContainer 标记为移除状态,后续 SPStackedNavigationScrollView 会将它移除
SPStackedPageContainer *pageC = [_scroll containerForViewController:viewController];
pageC.markedForSuperviewRemoval = YES;
}
//关键步骤,移除 viewController
[viewController removeFromParentViewController];
[self didChangeValueForKey:@"viewControllers"];
[self setActiveViewController:[self.childViewControllers lastObject]
position:SPStackedNavigationPagePositionRight
animated:animated];
return viewController;
}
如我们猜测 SPStackedNavigationController 做 pop 操作的时候,就是在 SPStackedNavigationScrollView 这个View 移除一个 SPStackedPageContainer View。并让 SPStackedPageContainer 对应的 ViewController 发一个 removeFromParentViewController 的消息。
SPStackedPageContainer 解析
SPStackedPageContainer 的作用是承载 ViewController 的 View,并对一些手势动作进行处理,在这里 SPStackedPageContainer 这个概念在这里等同于一个分屏 View。
打开 SPStackedPageContainer.m 查看 - (void)setVCVisible:(BOOL)VCVisible 方法。
//将VC的View加到Container里面
- (void)setVCVisible:(BOOL)VCVisible
{
if (VCVisible == self.VCVisible) return;
if (VCVisible) {
[self.screenshot removeFromSuperview];
self.screenshot = nil;
if (!self.markedForSuperviewRemoval || [_vc isViewLoaded])
{
_vcContainer.backgroundColor = _vc.view.backgroundColor;
_vc.view.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
if (!_vc.view.superview)
// 关键步骤 添加 View
[_vcContainer insertSubview:_vc.view atIndex:0];
}
} else {
if ([_vc isViewLoaded])
// 关键步骤 移除 View
[_vc.view removeFromSuperview];
}
}
SPStackedNavigationScrollView 解析
SPStackedNavigationScrollView 是一个模仿 UIScrollView 实现的 View。关于 UIScrollView 的深入理解,推荐 ObjC 中国的文章 理解 Scroll Views, 这里就不再详述,默认大家都是能理解 UIScrollView 的相关概念。
当使用 SPStackedNavigationController 做3次 Push 操作的时候, SPStackedNavigationScrollView 的 View 层次结构是这样的。
SPStackedNavigationController 的 rootView 就是 Container0 这个 View。而 Push 的 View 分别是 Container1,Container2,Container3。左边的半屏 View 的位置从底往上分别是 Container1 --> Container2。右边的半屏 View 则是 Container3。若是 SPStackedNavigationController 再 Push 一个 View 的话,那么 左边的半屏 View 的位置从底往上分别是 Container1 --> Container2 --> Container3 。右边的半屏 View 则是 Container4,Container 这个概念在这里等同于一个分屏 View。 在这个时候 SPStackedNavigationScrollView 的View 的简单示意图如下
从上面的 View 结构示意图中可以看出,SPStackedNavigationScrollView 对 UIScrollView 的模仿主要体现在 UIScrollView 的滑动机制上。
当 SPStackedNavigationController 做 push 操作的时候,SPStackedNavigationScrollView 右边半屏的 View 会从右向左滑动到左边半屏的位置,而右边半屏则从右向左显示一个新的 push 进来的 View。
当 SPStackedNavigationController 做 pop 操作的时候,SPStackedNavigationScrollView 右边半屏的 View 会从左向右滑动出屏幕显示范围,而左边半屏的 View 则会从左向右滑动到右边半屏。
讲完了 SPStackedNavigationScrollView 的大概表现之后,若是大家还是不怎么了解的话,可以运行 Demo 详细体会SPStackedNavigationScrollView 的UI变化。
我们接下来查看 SPStackedNavigationScrollView.h 文件,寻找和 UIScrollView 相关的代码。
@interface SPStackedNavigationScrollView : UIView
// ...... 省略代码
@property(nonatomic) CGPoint contentOffset;
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated;
- (NSRange)scrollRange;
// ...... 省略代码
@end
从 SPStackedNavigationScrollView 的头文件中,我们可以看到 SPStackedNavigationScrollView 继承于 UIView。和 UIScrollView 相关的概念有 contentOffset 和 scrollRange。关于 UIScrollView 的深入理解,推荐 查看 ObjC 中国的文章 理解 Scroll Views ,这里就不再详述,默认大家都是能理解 UIScrollView 的相关概念。
接下来开始讲解 SPStackedNavigationScrollView 的具体实现。
看下面的图,当屏幕上只有 rootView 没有分屏的 View 的时候 SPStackedNavigationScrollView 的 frame 的坐标原点是在 rootView 的左上角,这个时候SPStackedNavigationScrollView 的 contentOffset = 0。
接着看图,当屏幕上出现一个分屏的 View 的时候,我们叫这个 View 为 Container1。 SPStackedNavigationScrollView 的 frame 的坐标原点是在 Container1 的左上角,这个时候SPStackedNavigationScrollView 的 contentOffset = rootView.width / 2。
接着看图,当屏幕上出现二个分屏的 View 的时候,我们分别叫这二个 View 为 Container1 和 Container2。 SPStackedNavigationScrollView 的 frame 的坐标原点是在 Container1 的左上角,这个时候SPStackedNavigationScrollView 的 contentOffset = rootView.width。
从上面的示意图中不难看出理解 SPStackedNavigationScrollView 的重点在于理解 SPStackedNavigationScrollView 不断变化的 frame 原点 和 contentOffset。只要 contentOffset 发生了变化,那么 SPStackedNavigationScrollView 就会发生滚动。
查看 SPStackedNavigationScrollView.m 文件,看到了2个和contentOffset相关的变量 _actualOffset 和 _targetOffset,接下来跟踪这2个变量的变化。
@implementation SPStackedNavigationScrollView {
CGPoint _actualOffset; //模拟 ScrollView 当前的 contentOffset
CGPoint _targetOffset;// 模拟 ScrollView 将要滚动到的 contentOffset
}
查看 SPStackedNavigationScrollView 的 - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated 方法,作用是赋值 _targetOffset 和 _actualOffset 。
// 模仿 UIScrollView 滚动到指定位置
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
// 给 _targetOffset 赋值
_targetOffset = contentOffset;
if (animated)
[self animateToTargetScrollOffset];
else {
// 给 _actualOffset 赋值
_actualOffset = _targetOffset;
if (_onScrollDone)
{
self.onScrollDone();
self.onScrollDone = nil;
}
// 关键步骤
[self setNeedsLayout];
}
}
UIView 在调用 setNeedsLayout 方法之后,会调用 layoutSubviews 方法。接下看查看该方法。
- (void)layoutSubviews
{
// pen 的作用是stretch scroll at start and end
// 用于在第一屏从左向右拉扯和最后一屏从右向左拉扯,
// 让手势拖动的距离2倍于View移动的距离。
// _actualOffset 改变之后,通过特定的规则计算 pen 的 frame,然后将 frame 赋值给 View ,
// 总之作用就是调整 View 的 frame 位置
// 可以说 pen 就是对应的每个分屏的 frame
CGRect pen = CGRectZero;
// 为什么需要 - _actualOffset.x ?
// 为了得到每个分屏 View 的坐标的 X 值 (坐标原点是 SPStackedNavigationScrollView 的坐标原点,即在屏幕范围内的最左边的分屏 View 的左上角位置)
// 详见 ContentOffset 的计算方法
pen.origin.x = -_actualOffset.x;
// stretch scroll at start and end
if (_actualOffset.x < 0){
// 第一页从左向右拉扯 _actualOffset.x < 0 才成立,
// _actualOffset 就是当前模仿的 UIScrollView 的 contentOffset
// 手势拖动的距离2倍于 View 移动的距离
pen.origin.x = -_actualOffset.x/2;
}
CGFloat maxScroll = [self scrollOffsetForAligningPageWithRightEdge:self.subviews.lastObject];
if (_actualOffset.x > maxScroll){
pen.origin.x = -(maxScroll + (_actualOffset.x-maxScroll)/2);
}
int i = 0;
// markedForSuperviewRemovalOffset 标记 pageC 自己的 offset 坐标
// 用来给 superview 把 pageC 从当前位置移动到 markedForSuperviewRemovalOffset 指定的坐标
// 可以让自己的 View 对边缘层叠效果做出对应的位置
// 也可以让 pageC 自己全屏或者半屏,
CGFloat markedForSuperviewRemovalOffset = pen.origin.x;// View 的坐标位置x
NSMutableArray *stackedViews = [NSMutableArray array];
for(SPStackedPageContainer *pageC in self.subviews) {
pen.size = pageC.bounds.size;
pen.size.height = self.frame.size.height;
if (pageC.vc.stackedNavigationPageSize == kStackedPageFullSize)
pen.size.width = self.frame.size.width;
CGRect actualPen = pen;
if (pageC.markedForSuperviewRemoval)
actualPen.origin.x = markedForSuperviewRemovalOffset;
// Stack on the left
// 小于 (0,1,2,3)*3
// 左边是一个 stackedViews,最多有3层边缘层叠效果
if (actualPen.origin.x < (MIN(i, 3))*3){
// 如果actualPen.origin.x 小于 (MIN(i, 3))*3 那么说明该 pageC 的位置不是在 stackedViews 最顶部的三个以内
[stackedViews addObject:pageC];
}else{
pageC.hidden = NO;
}
if (self.scrollAnimationTimer == nil)
// floorf取整操作
actualPen.origin.x = floorf(actualPen.origin.x);
// 改变pageC.frame,那么pageC就会动了
pageC.frame = actualPen;
markedForSuperviewRemovalOffset += pen.size.width;
// NavVC 做 POP 操作的时候会将 markedForSuperviewRemoval 置为 YES
// 前面 pen.origin.x = -_actualOffset.x;
// 这里计算下一个屏幕的位置 frame 的 x 值
// 所以需要加上 pen.size.width
if (!pageC.markedForSuperviewRemoval)
pen.origin.x += pen.size.width;
// 覆盖不透明度
if (actualPen.origin.x <= 0 && pageC != [self.subviews lastObject]) {
// abs()绝对值函数
pageC.overlayOpacity = 0.3/actualPen.size.width*abs(actualPen.origin.x);
} else {
pageC.overlayOpacity = 0.0;
}
i++;
}
i = 0;
for (NSInteger index = 0; index < [stackedViews count]; index++)
{
SPStackedPageContainer *pageC = stackedViews[index];
// stackedViews 包括 RootVC 的 View;
// stackedViews 里面的最后3个 View 显示
if ([stackedViews count] > 3 && index < ([stackedViews count]-3))
pageC.hidden = YES;
else
{
// 左边是一个 stackedViews,最多有3层边缘层叠效果
pageC.hidden = NO;
CGRect frame = pageC.frame;
// 调整坐标,显示层叠效果
frame.origin.x = 0 + MIN(i, 3)*3;
pageC.frame = frame;
i++;
}
}
// Only make sure we show what we need to, don't unload stuff until we're done animating
[self updateContainerVisibilityByShowing:YES byHiding:NO];
}
在 layoutSubviews 方法里面 根据 _actualOffset 计算好每个分屏的 frame ,以及哪些分屏是可以显示在屏幕上的,哪些分屏是需要移除的,哪些分屏的位置是在屏幕显示的分屏的左边,哪些分屏的位置是在屏幕显示的分屏的右边。
在layoutSubviews 方法里面调用了一个方法用于控制分屏 View 的显示与隐藏,在这里分屏 View的概念可以等同于SPStackedPageContainer。这个方法是 - (void)updateContainerVisibilityByShowing:(BOOL)doShow byHiding:(BOOL)doHide 。
- (void)updateContainerVisibilityByShowing:(BOOL)doShow byHiding:(BOOL)doHide
{
// fabsf 浮点数的绝对值
// 分屏 View 是否需要弹跳效果
BOOL bouncing = self.scrollAnimationTimer && fabsf(_targetOffset.x - _actualOffset.x) < 30;
// layoutSubViews的 pen 是一个 frame、
// 这里的 pen 是一个 frame 的 x 坐标
// 但是用法和 layoutSubViews 的 pen 没什么区别
CGFloat pen = -_actualOffset.x;
// stretch scroll at start and end
if (_actualOffset.x < 0)
pen = -_actualOffset.x/2;
CGFloat maxScroll = [self scrollOffsetForAligningPageWithRightEdge:self.subviews.lastObject];
if (_actualOffset.x > maxScroll)
pen = -(maxScroll + (_actualOffset.x-maxScroll)/2);
// 用来让 SuperView 移动 pageC 的 x 坐标,原点是屏幕显示的最左边的分屏的 X 坐标
CGFloat markedForSuperviewRemovalOffset = pen;
NSMutableArray *viewsToDelete = [NSMutableArray array];
for(SPStackedPageContainer *pageC in self.subviews) {
CGFloat currentPen = pen;
// 该 pageC 被做了 POP 操作,需要被 SuperView移除
if (pageC.markedForSuperviewRemoval)
currentPen = markedForSuperviewRemovalOffset;
// 该分屏是否是在屏幕可见的分屏的右边同时无法看见该分屏
BOOL isOffScreenToTheRight = currentPen >= self.bounds.size.width;
NSRange scrollRange = [self scrollRangeForPageContainer:pageC];
// View 是否被其他 View 覆盖了
BOOL isCovered = currentPen + scrollRange.length <= 0;
// View 现在是否可见
BOOL isVisible = !isOffScreenToTheRight && !isCovered;
// pageC 的可见性发生变化 && ( (isVisible == NO && doHide == Yes) || isVisible == Yes && doShow ==Yes)
// 只要 pageC 的可见性发生变化,不管是隐藏还是显示都执行下面的if条件分支
if (pageC.VCVisible != isVisible && ((!isVisible && doHide) || (isVisible && doShow)))
{
// pageC分屏将出现
// pageC分屏将离开屏幕
//(isVisible == No || bouncing == No || (isVisible ==Yes && needsInitialPresentation == Yes))
if (!isVisible || !bouncing || (isVisible && pageC.needsInitialPresentation)) {
pageC.needsInitialPresentation = NO;
pageC.VCVisible = isVisible;
}
}
// 要隐藏 pageC 并且该 pageC 被标记为销毁的
//(doHide ==Yes && pageC.markedForSuperviewRemoval ==Yes)
// 将 pageC 加入销毁数组 viewsToDelete
if (doHide && pageC.markedForSuperviewRemoval)
[viewsToDelete addObject:pageC];
//经过 Demo 验证 pen 和 markedForSuperviewRemovalOffset 的值一样
markedForSuperviewRemovalOffset += pageC.frame.size.width;
// markedForSuperviewRemoval = No
// 计算 pen 的值,该值为下一个分屏的 X 坐标
if (!pageC.markedForSuperviewRemoval)
pen += pageC.frame.size.width;
}
// 对viewsToDelete数组里面的View执行销毁操作
[viewsToDelete makeObjectsPerformSelector:@selector(removeFromSuperview)];
}
限于篇幅关系无法一一介绍SPStackedNavigationScrollView 的各种实现。
未介绍的细节知识点包括但不限于 NSRunLoop,用于 SPStackedNavigationScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。SPStackedNavigationScrollView 的 scrollRange 的计算细节,SPStackedNavigationScrollView 的手势处理等等,大家若是有兴趣可以在我的 GitHub 上下载对应注释版本源码,地址 https://github.com/junbinchencn/SPStackedNav-Note 。
总结
SPStackedNav 项目是一个用于 iPad 分屏的 UI 解决方案。该方案的核心在于 SPStackedNavigationScrollView 这个类。SPStackedNavigationScrollView 模仿了 UIScrollView 的实现。SPStackedNav 的分屏方案的设计非常精巧,实现思路清晰明确,实现过程中的很多细节还是非常具有参考和学习价值的,一些 contentOffset 的计算方法还是非常巧妙的。本人能力有限,文章难免有不足之处,若是您有发现,请在评论中指出,确认之后马上修改,谢谢!
参考
理解 Scroll Views https://www.objccn.io/issue-3-2/
SPStackedNav https://github.com/spotify/SPStackedNav
SPStackedNav-Note https://github.com/junbinchencn/SPStackedNav-Note