作者:Bill,前滴滴 iOS,现就职于抖音商业化,偶尔写写 FE 和跨端。
本文发表于 2019/07/01 《WWDC19 内参》
前言
本文主要分三个方面进行讨论。首先,我们将概述为了实现多窗口支持,App 生命周期在iOS13中的变化。然后我们将深入探讨新的 UIScene Delegate,以及我们应该在这里构建怎样的代码。最后,我们将介绍 ArchitectureKit 的一些最佳实践,确保为用户提供一致,无感知的多任务处理体验。
App 生命周期的变化
App 生命周期的职责:
iOS 12 及之前:
处理 App 的生命周期
UI 的生命周期
iOS 13:
处理 App 生命周期和新的 Scene Session 生命周期
UI 的生命周期是由 Scene Delegate 来处理的
在iOS12及之前,App Delegate 有两个主要职责。
第一个是通知 App 进程级别相关事件。因此,系统会在您的进程启动或即将终止时通知 App Delegate 相关消息。
App Delegate 的第二个职责在于让我们的应用知道其 UI 的状态。因此,通过某些方法,例如重新载入前台 App 将重新激活,系统将由 App Delegate 通知对应的 UI 状态变化。
这种模式在 iOS 12 及之前完全运行正常,因为应用程序只有一个进程,而且只有一个用户界面实例相匹配。
iOS 12 及之前,我们在构建 App Delegate 代码时,大致如下:
在完成初始化的回调中,这里做了两件事, 首先执行了一次连接到数据库或初始化数据结构的非 UI 全局操作,然后立即进行了用户界面的配置操作。
Why?这是因为 App 现在虽然仍共享同一个进程,但可能有多个用户界面实例或 Scene Session。这就意味着 App Delegate 的职责需要为此做一些变更。
Scene Delegate
在iOS13中,App Delegate 仍然负责进程的事件以及生命周期,但它不再负责与 UI 生命周期相关的任何操作。UI生命周期相关事件都将由新的 UIScene Delegate 进行处理。
对于开发者,有哪些影响?
以前在 App Delegate 中执行的任何UI操作现在都需要迁移到 Scene Delegate 中的相应方法。
实际上,在 iOS 13 中,如果我们使用了新的 UIScene 生命周期进行代码构建,UIKit 将不会调用与 UI 状态相关的所有 App Delegate 方法;所有UI状态相关将通过 Scene Delegate 进行调用,大多数方法都有一对一的映射。
如果我们想在 iOS 13 上采用多窗口支持,并不需要太担心旧版本的适配问题,我们只需同时保留新旧两组代理方法即可,UIKit 将自动在运行时调用正确的方法集。
在我们深入研究具体的 Scene Delegate 相关代理方法之前,还需要介绍一下 App Delegate 的另外一个额外责任:系统会在将要创建及销毁 Scene Session 时通知 App Delegate。
配置新 Session
我们以下面的蓝色 App 为例,来分析具体的生命周期方法调用。
首先,第一次启动,我们来观察调用堆栈。App Delegate 中依旧调用了 didFinishLaunchingWithOptions
方法。
同时要说明的是,在这里构建一次性非 UI 相关初始化代码是合理的。在这之后,系统将创建一个 Scene Session。但在创建实际的 UIScene之前,系统会向我们的应用程序确认 UIScene 的相关配置。这些配置包括要创建 Scene 的 Scene Delegate,Storyboard 以及指定的 Scene 子类。
值得注意的是,我们也可以在代码中动态定义这些 Scene 的配置,或者最好在info.plist中静态定义这些 Scene 配置。
我们可以配置一个主 Scene 配置,同时也可能有一个辅助 Scene。因此,我们应该确认此处提供的相应 options 参数,以将其用作选择正确 Scene 配置的上下文。一旦我们定义了这些配置,例如在 info.plist 中,就非常简单了,只需按对应名称引用,同时确保传入 Session 角色的角色设定。
至此应用已启动,我们创建了一个 Scene Session。但是目前我们仍然不会看到任何 UI。
但是我们的 Scene Delegate 确实连接到了对应Scene Session,Why?那是因为我们需要在此使用新的UIWindow初始化方式设置我们的UIWindow。
我们传递了相应的 Session。但是,我们还需要检查任何相关的用户活动或状态恢复活动以配置我们的窗口。
完成相关配置后,现在我们可以看到我们的应用程序了。
Scene 断连
那么,当我们的用户上滑返回桌面时会发生什么?
这时,我们熟悉的 didEnterBackground
方法将会在 Scene Delegate 中被调用。但有趣的是,在之后的某个时间点,我们的 Scene 可能会断开连接。
那这是意味着什么呢?
为了进行资源回收,系统可能会在 Scene 进入后台后的某个时间点从内存中释放该 Scene。这也意味着我们的 Scene Delegate 将从内存中释放,并且Scene Delegate所持有的任何 Window 及 View 的实例也将被释放。
这让我们有机会销毁和释放内存中与此 Scene 相关的可能被应用中其他逻辑所 retain 的任何大型资源。但是,要注意我们不应该使用它来删除任何用户数据或永久状态,因为 Scene 可能会重新连接并返回数据。
清理废弃 Session
接下来让我们将讨论当用户在 Scene Session上滑并明确想要销毁它时实际发生了什么。
这时,系统将调用 AppDelegate 中的 didDiscardSceneSessions
。
这也让我们可以很方便地处理与Scene相关的任何用户状态或数据,例如在文本编辑类 App 中未保存的草稿。
同时,用户也可以通过上滑动来从 App 切换界面中删除一个或多个 UIScenes,即便实际应用进程并未实际运行。如果进程没有运行,系统将持续跟踪销毁的 Session,并在应用下次启动后立即调用。
架构
接下来让我们讨论一些可以考虑应用到项目中的一些架构模式。我们首先谈谈 State 的恢复。
在 iOS 13中,state 的恢复不再是精确的。对于我们来说,实现基于 Scene 的 State 恢复很重要。
让我们看一下为什么会这样。下面是我们的应用切换界面。例如这个文档应用,假设我们正在计划一次公路旅行,同时打开了四个不同的文件 Session。
但在这些 Session 中,我更常用打包清单和日程计划。在相应的时机,后台运行的公路旅行和会议 Session 将会被系统断开连接并释放。
如果我并没有在这里保存当时编辑的状态数据,当我回到公路旅行计划 Session 时,应用不会回到之前所处的状态。Scene 中并不会保有我之前编辑的文档, 相反,应用会重启,相当于一个全新的窗口,这种用户体验非常糟糕。
Pre-Scene 状态恢复
那么,我们该怎么解决这个问题呢?iOS 13 中有一个全新的基于 Scene 的 State 复原 API。它非常简单。
它的工作原理不再是通过视图分层结构来维护数据,而是通过 State 来进行管理,这也意味着可以通过 State 来重建我们之前的 Session,这也是基于 NSUser 的。
值得注意的是,在 iOS13 中,我们返回给系统的状态恢复归档将与应用其余部分的相同数据保护类相匹配。
在 Scene Delegate 中,我们为Scene实现了 State 复原的方法,调用方法,在当前窗口中查找活跃的最相关用户操作行为。
然后我们返回这个行为。一段时间后,当这个 Scene 重新进入前台并连接时,我们检查 Session 是否包含 State 复原行为。如果有,则执行相关行为进行页面恢复。如果没有,我们可以创建一个没有任何状态的全新窗口。这样,用户就怎么都不会注意到 Scene 在后台断开连接也不会遇到状态未保存的糟糕用户体验了。
多 Scene 状态同步
最后,让我们谈谈在采用多窗口支持时可能遇到的一个更重要的问题。这就是如何最好地保持应用的 Scene 数据同步。以下面这个聊天应用为例来详细讨论一下,在 iOS 13 上刚添加了对多个窗口的支持。所以我们同时可以在两个不同的 ViewController 和两个不同的 Scene 中查看同一段对话。
所以,假设我想给 Giovanni 发一条消息让他知道我已准备好吃午饭了。但是只有一个 Scene 更新了。
在iOS上,许多应用都是以这种方式构建的,其中 ViewController 通过按钮点击等接收事件。然后 ViewController 更新自己的 UI 。然后我们的 ViewController 会通知我们的模型或模型控制器。
但是现在如果我们要在显示相同数据的不同 Scene 中引入第二个 ViewController ,那么这个新的 ViewController 不会被通知用这个新数据来进行更新。
从架构上来说,现在如果我们的 ViewController 在收到事件后立即通知我们的模型控制器,那么我们可以让模型控制器实际通知任何相关订阅者或 ViewController ,告诉他们应该使用这些新数据进行更新。我们可以通过多种方式实现这一目标。
发送消息按下按钮时调用 didEnterMessage 方法。我们首先创建了一个消息模型对象,然后通知我们的 ViewController 更新自己的视图,最后更新了 Model 。
然而这种方式是不对的,首先我们要处理的就是当前 ViewController 不应该改变它自己的视图状态。
现在我们来优化这里的逻辑。
模型控制器的 Add 方法实际上进行了增加一条新信息的操作。但我们希望模型控制器现在通知任何其他 ViewController 或连接的 Scene 是否有更新。
那么如何发送此更新?我们想要一种结构化的方法来打包处理这个事件,这样它就是强类型的,并且易于调试和测试。
所以,iOS 13 中继续并创建了一个新类型,我们将其称为更新事件。这是一个带有相关值的 Swift 枚举。
我们添加一个新的消息类型。
这是我们的模型控制器在接收新消息时将创建的对象,然后将向下发送到任何相关的 ViewController 或 Scene 。我们使用 NSNotification Center 作为我们的数据 Store。
我们在 post 方法中创建一个新的更新事件,然后将其发布给任何订阅者。我们只是向新消息通知频道发通知。但这里要注意的是我们将更新事件对象本身包含在通知对象中。
现在,当我们的模型控制器被通知新消息已添加时,在我们处理相应逻辑之后,我们创建新事件并调用 post 方法。
后,如果我们观察 ViewController 的 UI 更新时,我们会观察到相应事件。
在这个 case 中,我们会收到新消息通知名称。然后我们创建一个处理方法,我们从参数中获取通知。
需要注意的是,当我们将更新事件作为通知对象传递时,我们可以从通知中撤回该事件。然后,我们可以轻松地切换事件的类型,因为我们创建了一个关联的枚举,我们可以将消息取出。现在,我们可以在这里更新UI。所以,在使用这个新架构后,当我们向 Giovanni 发送相同消息时两个 Scene 将会同步。
推荐阅读
Apple Silicon Mac 上的 iPad 与 iPhone App 运行
关注我们
我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。
这篇文章的内容来自于《WWDC19 内参》。关注【老司机技术周报】,回复「2020」, 即可领取。
本文分享自微信公众号 - 老司机技术周报(LSJCoding)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。