344bd1c874fe923e383ba976a46b90df
Modernizing Your UI for iOS13

前言

iOS 13将在2019年的秋天正式发版,每次新的 iOS 系统发布的时候,无数 iOS 开发者总是战战兢兢地祈祷新系统不会和之前的系统有太大的 UI 区别。

2013年发布的 iOS 7将 iOS1~6 写实化风格完全颠覆,几乎重绘了所有系统 App,推出了扁平化设计。当时无数 iOS 开发者和 UI 设计师们加班加点适配新系统的场景还历历在目,想必当初加班加点的开发者们和设计师们有不少都在心中呼唤:爸爸,请给一条生路吧!

在iOS 13即将发布之际,我们先来提前感受一下为了适配 iOS 13,我们的 App 都应该做些什么。好在这次苹果爸爸还算比较人道,不但没有太多额外的修改,反倒是开放了许多原本只在系统App中御用的控件,让普罗大众们也可以体验一把御用控件的快感!

好了,闲话不多扯,我们开始讲讲在iOS 13上UI相关的那些事。

Flexible UI

Launch Storyboards

我们可以通过修改 Launch Images 的方式进行来修改一个App在启动的时候用户看到的第一个画面。在 iOS 8之后,苹果引入了 LaunchScreen.storyboard 来处理这个事情。虽然两种方式都可以正常工作,但是苹果会更加希望开发者能够使用 LaunchScreen.storyboard 来进行这样的操作:Launch Images 需要在对应的aseets里面放入所有屏幕尺寸的 Launch Images,这样每当出现一个屏幕尺寸不同的设备的时候,都需要做对应的修改,显然就不够灵活。因此在2020年4月之后,所有App都必须使用 LaunchScreen.storyboard 的方式来操作启动画面,否则将无法提交到 AppStore 进行审批。(好的,爸爸!)

Be Resizable

每次在苹果发布新屏幕尺寸的设备的时候,线上的 App 总是会出现界面适配的问题,为了避免这样的事情再次发生,苹果要求凡是 link 了iOS 13的App都需要通过特定的 API 适配所有尺寸的屏幕(包括屏幕尺寸最小的 iPhone 以及屏幕最大的 iPad )。
另外,对于 iPad 的 App,苹果也希望每个 App 能够支持 Split Screen Multitasking。
和 Launch Storyboards 一样,这些特性的支持,也必须在2020年4月前完成。

iPhone 适配所有尺寸

iPhone 适配所有尺寸

iPad 适配所有尺寸

iPad 适配所有尺寸

Bars

在iOS 13上,默认的Navigation Bar的行为将会如下图gif所示。

iOS 13 Navigation Bar

iOS 13 Navigation Bar

当你的 App link 了iOS 13之后,你就自动拥有了这些行的能力。同时,苹果还提供了额外的 Appearance Customization 参数来定制对应的 Bar。

let appearance = UINavigationBarAppearance() 

appearance.configureWithOpaqueBackground()

appearance.titleTextAttributes = [.foregroundColor: myAppLabelColor] // 修改Navigation Bar上title的颜色
appearance.largeTitleTextAttributes = [.foregroundColor: myAppLabelColor] // 修改Navigation Bar上large title的颜色

navigationBar.standardAppearance = appearance

其中的 .standardAppearance 如下图所示,就是一个常规状态下的 Navigation Bar。

.standardAppearance // 常规状态
.compactAppearance // 小屏幕手机横屏时的状态
.scrollEdgeAppearance // 被ScrollView向下拉的状态

iOS13 Navigation Bar

iOS13 Navigation Bar

除了 Navigation Bar 以外,UIToolBarUITabBar 也能通过类似的方式进行自定义。其中 UIToolbar 对应的类为 UIToolbarAppearanceUITabBar 对应的类为 UITabBarAppearance

另外,在 iOS 13 中新的 Reminder 中,当你点击不同的类目的时候,详情页上的Navigation Bar 的 Title 颜色是会有不同的变化,如gif所示。

iOS13 系统 Reminder

iOS13 系统 Reminder

这种方式应该如何实现呢?直接看代码

let appearance = navigationBar.standardAppearance.copy()
// 改变颜色
// 其他任何修改
navigationItem.standardAppearance = appearance // navigationItem拥有和navigation bar一样的属性来对应不同的状态

Presentations

在iOS13中加入了新的 UIModalPresentationStyleUIModalPresentationStyle.pageSheetUIModalPresentationStyle.formSheet,他的样子如下图所示:

iOS13 Sheet Style

iOS13 Sheet Style

于是,用户就可以使用下拉来dismiss一个ModalViewController了,掌声响起来~

再于是,默认的 UIModalPresentationStyle 就变成了UIModalPresentationStyle.automatic,如果你使用了 automatic 作为UIModalPresentationStyle,当你试图弹出的是系统为你提供的ViewController的话,系统会根据那些 ViewController 的配置帮你智能选择一个弹出的样式,例如:当弹出一个 UIImagePickerController 的时候,如果 sourceType = .photoLibrary ,那么弹出的就是一个 sheet 样式的照片选择界面,但是如果 sourceType = .camera 的话,弹出的就是一个全屏样式的拍摄界面。至于我们自己写的 VC,如果使用 automatic 的话,默认会弹出sheet样式的界面。那么,我们如何手动指定弹出的样式呢?也非常简单,具体的代码如下:只需加上 vc.modalPresentationStyle = .fullScreen 就可以了

func showCustomCamera() {
    let cameraVC = MyCameraViewController() 
    cameraVC.modalPresentationStyle = .fullScreen 
    present(cameraVC, animated: true)
}

至于 iPad 上的 popover,如果你希望 popover 在 regular width 下是一个普通的 VC,在 compact width 下是一个 sheet 的话,也非常简单,只需要将 modalPresentationStyle 设置成.popover就可以了。

iPad regular width popover

iPad regular width popover

iPad compact width popover

iPad compact width popover

class MyViewController: UIViewController {
    func showOptions() {
        let optionsVC = MyOptionsViewController()   
        optionsVC.modalPresentationStyle = .popover present(optionsVC, animated: true)
    } 
}

关于下拉关闭,一般情况下,你不需要做额外的工作,一个以sheet类型弹出的 ModalVC 将会默认拥有拉下关闭的操作。但是假设如果用户正在进行一些文字编辑,这时候如果进行了下拉关闭,那么我们是需要有能力让用户选择是否对已经编辑的文字做保存,放弃还是其他的操作。于是,出现了一个叫做 UIAdaptivePresentationControllerDelegate 的代理,其中:

func presentationControllerDidAttemptToDismiss(_:UIPresentationController) {
    // Present action sheet
}

就可以在用户试图关闭一个VC的时候进行额外的操作。

对了,一个叫做 isModalInPresentation 的属性被加入了UIViewController中,只有当 isModalInPresentation 为true的时候,presentationControllerDidAttemptToDismiss 才会被调用。想要了解更信息的信息,可以参考代码:Disabling Pulling Down a Sheet

另外,对于在 Share Extensions 中的 Principal View Controller,如果我们将 isModalInPresentation 设置为 true,那么用户在试图关闭那个分享VC的时候,系统将会调用presentationControllerDidAttemptToDismiss,那么我们就可以在这里做一些额外的操作来确保用户编辑过的分享内容能够被更好地被处理。

敲黑板!!! 下面这部分是要考的!

对于一个以 Full Screen 形式弹出的VC,正如我们原先认知中了解到的一样,当一个 VC 被弹出的时候,将他弹出的那个 VC 会依次调用viewWillDisappearviewDidDisappear。然后在这个VC被dismiss的时候,将他弹出的那个VC的viewWillAppearviewDidAppear会被依次调用。如图:

FullScreen调用顺序

FullScreen调用顺序

但是对于一个以sheet形式弹出的VC,将他弹出的那个VC的willDisappear和DidDisappear将不会被调用! 同样的,在dismiss的时候viewWillAppearviewDidAppear 也不会被调用

Sheet调用顺序

Sheet调用顺序

(目测这里会是一个潜在的风险!原先写在viewWillDisappear等四个函数中的代码以后都有可能会存在问题。目前可以想到的一个做法就是,如果项目中的VC都能够继承自同一个VC,那么在那个RootVC中将他的modalPresentationStyle默认先设置成fullScreen的。如果没有这样一个VC,那就只能强行hook了😂)

另外,在iOS13中,系统偷偷在UIWindowrootViewController.view中插入了一些 UIKit 的view,当然了,这些 view 是 private的,苹果也表示请不要在这些view上做任何事情,因为我们不能保证这些view在不同的系统中会不会有不同的行为!

iOS13 private windows

iOS13 private windows

Search

对于 Search 而言,UISearchViewController在 iOS 13 中提供了更加多样的定制化功能,其中最为主要的就是把 SearchBar 开放出来了!(普大喜奔!!)。以后终于可以比较方便地自定义那个 SearchBar 了。

UISearchViewController

UISearchViewController

UISearchViewController

UISearchViewController

具体设置代码如下:
```Swift
func loadView() {
let searchController = UISearchController(searchResultsController: /.../)

// Don’t automatically show the cancel button or scope bar
top Created with Sketch.