iPad 多任务 Spilt View & Size Class

iPad 多任务 Spilt View & Size Class


一、多任务简介

iOS 9 以后iPad新增了多任务的支持,主要形式有三种:

  • Slide Over (侧边快捷打开)
  • Spilt View (多任务分屏)
  • Picture in Picture (画中画)

1. Picture in Picture

使用系统AVKit或者AVFoundation库提供的新的API替换掉老的 MPMoviePlayerViewController, MPMoviePlayerViewController,做一些调整即可。

2. Slide Over

Slide Over支持起来比较简单,程序基本上不需要做任何代码级别的适配。

Tips: App交互设计的时候,应该避免采用屏幕右侧向左滑这一类的交互。因为iOS9以后系统拦截了该事件用来触发Slide Over & Spilt View。
例如网易新闻iPad端的评论划出功能

3. Spilt View

多任务分屏功能,我们今天主要探讨的内容,后续章节有详细描述。


## 二、是否要支持多任务分屏?

1. 是否需要适配

引用官方的说法

Adopt Slide Over and Split View unless you have a specific reason not to. From a customer’s perspective, an iOS 9 app that doesn’t adopt Slide Over and Split View feels out of place.

简单翻译一下:“最好添加Slide Over & Spilt View的适配,除非你有足够的原因。不做新特性的适配会让用户觉得你的程序out了”

当然也有例外的情况,如果你的程序属于以下两种,那么不需要去做多任务的适配:

  • 以拍照为主要功能的程序
  • 需要全屏交互的程序,例如需要使用到传感器的游戏类应用

2. 如何禁用多任务分屏

要禁掉App的多任务分屏支持,只需要在App的info.plist中添加一个“UIRequiresFullScreen”的key,设置value为“YES”即可。

到这儿了,如果你的不需要支持多任务分屏的话那么就后面的内容就可以跳过了。


## 三. 适配前需要明确的几个点

1. 转变观念

首先有一个观念我们得转变过来,同一时刻运行在前台的App不再限制为一个。如果用户进入了多任务分屏模式的话,那么就会有两个App同时运行在前台。

Both Apps in Spilt View are running in the foreground.

虽然两个App同时运行在前台时,地位却不一样,Primary(老大,通常在左边)和Secondary(老二,通常指的是右边的应用)。

只有老大:

  • 拥有状态栏的控制权限
  • 可以处理外接屏幕的显示(通常使用UIScreen实现)
  • 可以显示画中画窗口
  • 可以占据2/3宽的屏幕,而老二最多只能占据屏幕宽度的一半

避免使用UIScreen的bounds来处理App应该的展示区域,最好使用UIWindow的bounds来代替。

因为在多任务分屏下UIScreen还是那个Screen,只不过App的keyWindow不再时时刻刻充满UIScreen了,显示在什么位置,显示多大面积,全部取决于用户使用设备的姿势。

2. 面临的问题

前面提到过了,当进入多任务分屏模式以后,将会有两个App同时运行在前台。试想有两个App同时需要使用CPU,GPU,内存,I/O及其它的硬件资源,要想保持良好的用户体验,就需要我们对自己的App做很多性能调优的方面的工作。

关于性能调优方面的知识,可以参考:Adopting Multitasking Enhancements on iPad

Every iOS app—even one that opts out of using multitasking features—needs to operate as a good citizen in iOS 9.
Now, even full-screen apps don’t have exclusive use of screen real estate, the CPU, memory, or other resources.

同时苹果基于保证用户体验的角度,对于支持做了硬件层面的限制如下:

前面提到了,Primary App比Secondary App拥有诸多优势,但是有一点是一样的:

当系统收到内存警告时,无论是Primary App还是Secondary App均可能被Kill掉


## 四. 如何适配Spilt View

如果遵循iOS 8引入的新的UI最佳实践,那么适配多任务适配将会是一件很容易的实情。可是问题关键问题就出在了这个“如果”上。

iOS 8推出以后苹果提的最多的“Adaptivity”,以及新引入了Size Class体系,并提出了让我们忘记设备方向的概念,所以的这一切都是在为了我们能够方便的实现App的布局。

通常开发App的UI框架的陈旧加上交互设计只考虑横竖屏(甚至只考虑一个方向)导致了我们适配Spilt View的难度比较大。

1. 适配Spilt View的几点要求

  • Xcode 7 及之后编译

  • 使用iOS 9 及之后的SDK

  • 使用"LaunchScreen.storyboard"代替launch.png之类的图片,完成启动画面定制。Xcode7之后新建工程会自动帮忙创建该文件并设置Info.plist,对于已存在的老工程需要我们手动创建该文件,并在Info.plist中做相应配置。

    PS. 苹果要求LaunchScrenn.storyboard中必须使用Autolayout布局,还在用全手写布局的朋友们,该考虑切换到Auto Layout布局了。

  • 支持四个方向

2. 多任务分屏模式切换时发生了什么

在多任务分屏模式时,App展示的尺寸完全取决于用户,可能会占屏幕的3/10, 5/10, 7/10等,再加上竖屏时候那两个奇葩的比例。一个App要完全支持iPad的多任务分屏,如果使用硬编码的方式,那么需要考虑5套布局,有没有想死的感觉。

先别着急,接着往下看。

当多任务分屏模式下,用户操作应用之间的分割区改变两个应用的显示比例时,系统会同时调用两个App的“applicationWillResignActive”方法。然用户完成操作的时候系统会再同时调用连个App的“applicationDidEnterBackground”方法。

关于如何相应App状态变换,请看文档:Strategies for Handling App State Transitions

与此同时,系统会通过以下两个方面告知我们分屏状态的改变:

  • Window尺寸的变化
  • RootVC 的Size Class的变化

这两个点配合起来才能完成多任务分屏显示模式切换的响应。

因为多任务分屏显示模式的变化并不总是伴随着Size Class的变化,例如从3/10 --> 5/10的变换的时候水平方向的Size Class一直都是Compac模式,但是尺寸(宽度)却发生了变化。

3. 如何响应Window尺寸变化

当Window尺寸变化时通常伴随着VC的viewDidLayoutSubViews活着View的layOutSubViews方法的调用,我们可以在这些方法里面重新计算位置,完成UI布局的刷新。

还有一个更好的方法就是Auto Layout,我们在程序一开始布局的时候通过指定元素之间的约束来描述布局,这样在View的尺寸变化的时候系统会根据我们制定的约束条件自动完成布局的刷新。

4. 如何响应Size Class的变化

我是代码党,界面布局大部分都是靠代码实现,Storyboard几乎不用。在一开始我查找资料的时候一搜Size Class出来的都是教人怎么在Storyboard中完成界面布局的文章。还以为Size Class就是专门为Storyboard设计的。

后面在搜索终于被我发现了一些端倪,iOS 8中 引入了两个Protocol:UITraitEnvironmentUIContentContainer,以及一个类UITraitCollection

其中UITraitEnvironment协议的定义如下:

/*! Trait environments expose a trait collection that describes their environment. */
@protocol UITraitEnvironment <NSObject>
@property (nonatomic, readonly) UITraitCollection *traitCollection NS_AVAILABLE_IOS(8_0);

/*! To be overridden as needed to provide custom behavior when the environment's traits change. */
- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection NS_AVAILABLE_IOS(8_0);
@end

可以看到该协议定义了一个traitCollection的属性,还有一个用来通知traitCollection改变的方法。系统中我们可以想到的UI类都实现了这个协议,包括: UIScreen, UIWindow, UIViewController, UIPresentationController, 以及UIView.

UIContentContainer协议则定义了几个VC级别的用来响应TraitCollection变化的方法,UIViewControllerUIPresentationController都实现了该协议。通过该协议定义的方法我们可以在Size Class变化的时候做一些动画。

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator NS_AVAILABLE_IOS(8_0);

- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator NS_AVAILABLE_IOS(8_0);

UITraitCollection类则定义了一些属性用来描述设备特性,如下所示:

	horizontalSizeClass
	verticalSizeClass
	displayScale
	userInterfaceIdiom
	forceTouchCapability

到这儿我们终于看见了SizeClass的身影了,而Size Class的定义如下:

typedef NS_ENUM(NSInteger, UIUserInterfaceSizeClass) {
    UIUserInterfaceSizeClassUnspecified = 0,
    UIUserInterfaceSizeClassCompact     = 1,
    UIUserInterfaceSizeClassRegular     = 2,
} NS_ENUM_AVAILABLE_IOS(8_0);

iOS 系统把UI的显示模式抽象为三种:Unspecified(对应StoryBoard中的any),CompactRegular

非多任务分屏下,常见设备的Size Class如下:

可以看出在非多任务分屏模式下,iPad 无论在横屏和竖屏下宽,高都是Regular。
进入多任务分屏模式以后Size Class如下:

搞懂了Size Class的知识以后,我们再看看Size Class变换的时候我们可以做些什么呢?

  • 改变SubViews的尺寸和位置
  • 添加或者移除subView
  • 添加,移除或者修改约束(注:约束只能修改constant)
  • 改变UILabel,TextField,Text view等的字体大小

## 五. Demo

我写了一个适陪Spilt View多任务分屏的Demo,地址:SpiltViewDemo,App的内容很简单,界面上只包含两个元素一个ImageView用来展示图片,一个UITextView用来展示文字描述。
截图如下:

在Regular模式下图文结构为左右结构,当进入到Compact模式时切换为上下结构。

核心的代码如下:

#pragma mark -
#pragma mark Size Class Related

- (void)updateConstraintsForSizeClass:(UIUserInterfaceSizeClass)newSizeClass
{
    NSArray *currentConstraints = [self constraintsForSizeClass:self.traitCollection.horizontalSizeClass];
    NSArray *newConstraints = [self constraintsForSizeClass:newSizeClass];
    [self.view removeConstraints:currentConstraints];
    [self.view addConstraints:newConstraints];
    
    if (newSizeClass == UIUserInterfaceSizeClassRegular) {
        _imageIV.image = [UIImage imageNamed:@"aodi.jpg"];
        _textView.font = [UIFont systemFontOfSize:24];
        _textView.text = _aodiDes;
    }
    else {
        _imageIV.image = [UIImage imageNamed:@"aotuo.jpg"];
        _textView.font = [UIFont systemFontOfSize:16];
        _textView.text = _aodiDes;
    }
    
    [self.view updateConstraintsIfNeeded];
}

- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
    [super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator];
        
    [self updateConstraintsForSizeClass:newCollection.horizontalSizeClass];
    
}

可以看到只是简单的在willTransitionToTraitCollection:withTransitionCoordinator:方法中检测Size Class的变化,然后跟新约束系统即可。

当然实际应用在适配Spilt View的时候工作量可能远比这个多,但是原理都是一样的。

实际应用适配Spilt View除了技术上的支持,我想更多地是交互和视觉的调整,在多任务模式切换的时候如何更合理的调整元素的摆放位置以及交互的方式。


## 六. 总结 & 思考

1. 向下兼容

Size Class是iOS 8才引入的概念,如果你也像我一样使用手写的方式写界面,且你的应用还需要支持iOS 7,那么需要小心行事。

如果你的应用使用Storyboard的方式使用Size Class,那么恭喜你只要你符合以下几点要求,那么系统会自动帮你做向下兼容。

  • 使用Xcode6及以后编译
  • 竖直方向的Size Class不是Compact模式

Important: Compatibility occurs at build time, not at run time.

2. 关于性能

在最后还是要提一下App的性能,作为一个iOS上的好公民,我们需要多花一些精力来做App的性能调优,用好Profile工具,仔细查找并优化App在CPU,GPU,memory,I/O等方面的占用。

3. 及时跟进iOS的新技术

及时跟进iOS的新技术,这样在出现新特性的时候才能快速方便的接入。仔细想想从iOS 6的Auto Layout, iOS8 的Size Class,再到iOS 9推出的Spilt View,整个发展的递进式的。

4. 参考资料

Adopting Multitasking Enhancements on iPad
Size Classes Design Help
Strategies for Handling App State Transitions
UITraitEnvironment
UIContentContainer
UITraitCollection
Building Adaptive Apps with UIKit


注:smileEvday保留本文的一切权利,转载请著名原文出处
  本文所有内容仅代表个人观点,如有有不对的地方,欢迎指出。

posted on 2015-11-23 23:09  一片-枫叶  阅读(4250)  评论(4编辑  收藏  举报