App 多窗口支持架构

Stella981
• 阅读 695

作者: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 多窗口支持架构

App Delegate 的第二个职责在于让我们的应用知道其 UI 的状态。因此,通过某些方法,例如重新载入前台 App 将重新激活,系统将由 App Delegate 通知对应的 UI 状态变化。

App 多窗口支持架构

这种模式在 iOS 12 及之前完全运行正常,因为应用程序只有一个进程,而且只有一个用户界面实例相匹配。

iOS 12 及之前,我们在构建 App Delegate 代码时,大致如下:

App 多窗口支持架构

在完成初始化的回调中,这里做了两件事, 首先执行了一次连接到数据库或初始化数据结构的非 UI 全局操作,然后立即进行了用户界面的配置操作。

Why?这是因为 App 现在虽然仍共享同一个进程,但可能有多个用户界面实例或 Scene Session。这就意味着 App Delegate 的职责需要为此做一些变更。

Scene Delegate

在iOS13中,App Delegate 仍然负责进程的事件以及生命周期,但它不再负责与 UI 生命周期相关的任何操作。UI生命周期相关事件都将由新的 UIScene Delegate 进行处理。

对于开发者,有哪些影响?

以前在 App Delegate 中执行的任何UI操作现在都需要迁移到 Scene Delegate 中的相应方法。

App 多窗口支持架构

实际上,在 iOS 13 中,如果我们使用了新的 UIScene 生命周期进行代码构建,UIKit 将不会调用与 UI 状态相关的所有 App Delegate 方法;所有UI状态相关将通过 Scene Delegate 进行调用,大多数方法都有一对一的映射。

App 多窗口支持架构

如果我们想在 iOS 13 上采用多窗口支持,并不需要太担心旧版本的适配问题,我们只需同时保留新旧两组代理方法即可,UIKit 将自动在运行时调用正确的方法集。

App 多窗口支持架构

在我们深入研究具体的 Scene Delegate 相关代理方法之前,还需要介绍一下 App Delegate 的另外一个额外责任:系统会在将要创建及销毁 Scene Session 时通知 App Delegate。

App 多窗口支持架构

配置新 Session

我们以下面的蓝色 App 为例,来分析具体的生命周期方法调用。

App 多窗口支持架构

首先,第一次启动,我们来观察调用堆栈。App Delegate 中依旧调用了 didFinishLaunchingWithOptions 方法。

App 多窗口支持架构

同时要说明的是,在这里构建一次性非 UI 相关初始化代码是合理的。在这之后,系统将创建一个 Scene Session。但在创建实际的 UIScene之前,系统会向我们的应用程序确认 UIScene 的相关配置。这些配置包括要创建 Scene 的 Scene Delegate,Storyboard 以及指定的 Scene 子类。

App 多窗口支持架构

值得注意的是,我们也可以在代码中动态定义这些 Scene 的配置,或者最好在info.plist中静态定义这些 Scene 配置。

App 多窗口支持架构

我们可以配置一个主 Scene 配置,同时也可能有一个辅助 Scene。因此,我们应该确认此处提供的相应 options 参数,以将其用作选择正确 Scene 配置的上下文。一旦我们定义了这些配置,例如在 info.plist 中,就非常简单了,只需按对应名称引用,同时确保传入 Session 角色的角色设定。

App 多窗口支持架构

至此应用已启动,我们创建了一个 Scene Session。但是目前我们仍然不会看到任何 UI。

App 多窗口支持架构

但是我们的 Scene Delegate 确实连接到了对应Scene Session,Why?那是因为我们需要在此使用新的UIWindow初始化方式设置我们的UIWindow。

App 多窗口支持架构

我们传递了相应的 Session。但是,我们还需要检查任何相关的用户活动或状态恢复活动以配置我们的窗口。

App 多窗口支持架构

完成相关配置后,现在我们可以看到我们的应用程序了。

App 多窗口支持架构

Scene 断连

那么,当我们的用户上滑返回桌面时会发生什么?

这时,我们熟悉的 didEnterBackground 方法将会在 Scene Delegate 中被调用。但有趣的是,在之后的某个时间点,我们的 Scene 可能会断开连接。

App 多窗口支持架构

那这是意味着什么呢?

为了进行资源回收,系统可能会在 Scene 进入后台后的某个时间点从内存中释放该 Scene。这也意味着我们的 Scene Delegate 将从内存中释放,并且Scene Delegate所持有的任何 Window 及 View 的实例也将被释放。

App 多窗口支持架构

这让我们有机会销毁和释放内存中与此 Scene 相关的可能被应用中其他逻辑所 retain 的任何大型资源。但是,要注意我们不应该使用它来删除任何用户数据或永久状态,因为 Scene 可能会重新连接并返回数据。

App 多窗口支持架构

清理废弃 Session

接下来让我们将讨论当用户在 Scene Session上滑并明确想要销毁它时实际发生了什么。

这时,系统将调用 AppDelegate 中的 didDiscardSceneSessions

App 多窗口支持架构

这也让我们可以很方便地处理与Scene相关的任何用户状态或数据,例如在文本编辑类 App 中未保存的草稿。

同时,用户也可以通过上滑动来从 App 切换界面中删除一个或多个 UIScenes,即便实际应用进程并未实际运行。如果进程没有运行,系统将持续跟踪销毁的 Session,并在应用下次启动后立即调用。

App 多窗口支持架构

架构

接下来让我们讨论一些可以考虑应用到项目中的一些架构模式。我们首先谈谈 State 的恢复。

在 iOS 13中,state 的恢复不再是精确的。对于我们来说,实现基于 Scene 的 State 恢复很重要。

让我们看一下为什么会这样。下面是我们的应用切换界面。例如这个文档应用,假设我们正在计划一次公路旅行,同时打开了四个不同的文件 Session。

App 多窗口支持架构

但在这些 Session 中,我更常用打包清单和日程计划。在相应的时机,后台运行的公路旅行和会议 Session 将会被系统断开连接并释放。

如果我并没有在这里保存当时编辑的状态数据,当我回到公路旅行计划 Session 时,应用不会回到之前所处的状态。Scene 中并不会保有我之前编辑的文档, 相反,应用会重启,相当于一个全新的窗口,这种用户体验非常糟糕。

Pre-Scene 状态恢复

那么,我们该怎么解决这个问题呢?iOS 13 中有一个全新的基于 Scene 的 State 复原 API。它非常简单。

App 多窗口支持架构

它的工作原理不再是通过视图分层结构来维护数据,而是通过 State 来进行管理,这也意味着可以通过 State 来重建我们之前的 Session,这也是基于 NSUser 的。

值得注意的是,在 iOS13 中,我们返回给系统的状态恢复归档将与应用其余部分的相同数据保护类相匹配。

在 Scene Delegate 中,我们为Scene实现了 State 复原的方法,调用方法,在当前窗口中查找活跃的最相关用户操作行为。

App 多窗口支持架构

然后我们返回这个行为。一段时间后,当这个 Scene 重新进入前台并连接时,我们检查 Session 是否包含 State 复原行为。如果有,则执行相关行为进行页面恢复。如果没有,我们可以创建一个没有任何状态的全新窗口。这样,用户就怎么都不会注意到 Scene 在后台断开连接也不会遇到状态未保存的糟糕用户体验了。

App 多窗口支持架构

多 Scene 状态同步

最后,让我们谈谈在采用多窗口支持时可能遇到的一个更重要的问题。这就是如何最好地保持应用的 Scene 数据同步。以下面这个聊天应用为例来详细讨论一下,在 iOS 13 上刚添加了对多个窗口的支持。所以我们同时可以在两个不同的 ViewController 和两个不同的 Scene 中查看同一段对话。

App 多窗口支持架构

所以,假设我想给 Giovanni 发一条消息让他知道我已准备好吃午饭了。但是只有一个 Scene 更新了。

在iOS上,许多应用都是以这种方式构建的,其中 ViewController 通过按钮点击等接收事件。然后 ViewController 更新自己的 UI 。然后我们的 ViewController 会通知我们的模型或模型控制器。

App 多窗口支持架构

但是现在如果我们要在显示相同数据的不同 Scene 中引入第二个 ViewController ,那么这个新的 ViewController 不会被通知用这个新数据来进行更新。

App 多窗口支持架构

从架构上来说,现在如果我们的 ViewController 在收到事件后立即通知我们的模型控制器,那么我们可以让模型控制器实际通知任何相关订阅者或 ViewController ,告诉他们应该使用这些新数据进行更新。我们可以通过多种方式实现这一目标。

App 多窗口支持架构

发送消息按下按钮时调用 didEnterMessage 方法。我们首先创建了一个消息模型对象,然后通知我们的 ViewController 更新自己的视图,最后更新了 Model 。

App 多窗口支持架构

然而这种方式是不对的,首先我们要处理的就是当前 ViewController 不应该改变它自己的视图状态。

现在我们来优化这里的逻辑。

App 多窗口支持架构

模型控制器的 Add 方法实际上进行了增加一条新信息的操作。但我们希望模型控制器现在通知任何其他 ViewController 或连接的 Scene 是否有更新。

App 多窗口支持架构

那么如何发送此更新?我们想要一种结构化的方法来打包处理这个事件,这样它就是强类型的,并且易于调试和测试。

所以,iOS 13 中继续并创建了一个新类型,我们将其称为更新事件。这是一个带有相关值的 Swift 枚举。

App 多窗口支持架构

我们添加一个新的消息类型。

App 多窗口支持架构

这是我们的模型控制器在接收新消息时将创建的对象,然后将向下发送到任何相关的 ViewController 或 Scene 。我们使用 NSNotification Center 作为我们的数据 Store。

App 多窗口支持架构

我们在 post 方法中创建一个新的更新事件,然后将其发布给任何订阅者。我们只是向新消息通知频道发通知。但这里要注意的是我们将更新事件对象本身包含在通知对象中。

App 多窗口支持架构

现在,当我们的模型控制器被通知新消息已添加时,在我们处理相应逻辑之后,我们创建新事件并调用 post 方法。

App 多窗口支持架构

后,如果我们观察 ViewController 的 UI 更新时,我们会观察到相应事件。

App 多窗口支持架构

在这个 case 中,我们会收到新消息通知名称。然后我们创建一个处理方法,我们从参数中获取通知。

App 多窗口支持架构

需要注意的是,当我们将更新事件作为通知对象传递时,我们可以从通知中撤回该事件。然后,我们可以轻松地切换事件的类型,因为我们创建了一个关联的枚举,我们可以将消息取出。现在,我们可以在这里更新UI。所以,在使用这个新架构后,当我们向 Giovanni 发送相同消息时两个 Scene 将会同步。

推荐阅读

Apple Silicon Mac 上的 iPad 与 iPhone App 运行

关注我们

我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。

App 多窗口支持架构

这篇文章的内容来自于《WWDC19 内参》。关注【老司机技术周报】,回复「2020」, 即可领取。

本文分享自微信公众号 - 老司机技术周报(LSJCoding)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Stella981 Stella981
3年前
Docker 部署SpringBoot项目不香吗?
  公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为★“星标”!这样才不会错过每日进阶架构文章呀。  !(http://dingyue.ws.126.net/2020/0920/b00fbfc7j00qgy5xy002kd200qo00hsg00it00cj.jpg)  2
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这