由于我们已经在项目中使用了 SwiftUI,这使得泊学 App 只能在 iOS 13 及以后的版本中运行。因此,项目中为兼容更早版本 iOS 的代码就都没必要了。于是在继续之前,我们先清理下这部分代码。它们主要影响的是在各种交互条件下,对通知权限的申请。
整理 App 的启动流程
我们先通过一张图看下整理后的 App 主要启动流程:

其中这一节要修改的,就是图中橙色的部分。
Clear Keychain
当 App 第一次启动时,我们要清除之前保存在 Keychain 中的用户信息。这是因为当用户在登录状态下删除 App 并重新安装时,由于 iOS 并不会清除 Keychain 中的数据,导致了下次启动时,会误用之前保存的用户信息发起请求,这显然不是我们期望的。
那么,该如何进行清理呢?这要从 Keychain 中都保存了什么内容说起。当前,我们一共保存了三类数据。一类是App 启动时,向服务器申请的用于验证返回值的公钥,这部分代码,在 AppDelegate
里:
_ = attempt() {
KeychainBasedPublicKeyInfoStore
.default
.refresh()
.done { print($0) }
}
其中,KeychainBasedPublicKeyInfoStore
会在 Keychain 中保存当前使用的公钥版本(io.boxue.key.public.version
)以及公钥(io.boxue.key.public.pem
)。这两个信息是和用户个人数据无关的,并且每次 App 启动时会自动更新。因此,它们可以一直留在 Keychain 里。
第二类,是用户成功登录后,由服务器下发的标识用户的 RemoteUserSession
,它在 Keychain 中通过 io.boxue.key.remote.usersession
键保存,这个数据应该是要删掉的;
第三类,则是记录用户账号和个人信息的 UserProfile
,它在 Keychain 中通过 io.boxue.key.user.profile
键保存,这个数据也应该要删掉;
在泊学 App 里,RemoteUserSession
和 UserProfile
统一用 UserSession
这个 model 表示,而对这个 model 的操作,则由 UserSessionRepository
这个协议完成,因此,在这里,我们添加了一个清除用户数据的方法:
public protocol UserSessionRepository {
func clearUserSession()
}
然后,在 BoxueUserSessionRepository
的实现里,分别调用删除这两个数据的 delete
方法:
public func clearUserSession() {
_ = userProfileStore.delete()
_ = remoteUserSessionStore.delete()
}
最后,在 LaunchViewModel
的 init
方法里,判断如果是第一次启动,就清空记录用户信息的 Keychain 数据:
public init(userSessionRepository: UserSessionRepository,
guideResponder: GuideResponder,
browseResponder: BrowseResponder) {
/// ...
if isFirstLaunch {
userSessionRepository.clearUserSession()
}
}
Provisional notification 权限
第二处要修改的,是当用户在欢迎界面点击“随便看看”的时候,不再需要判断当前系统是否支持 provisional 通知了,直接申请这种默认的通知权限即可。于是,在 WelcomeViewModel
的 requestNotification
方法里,我们可以直接生成请求权限的事件:
func requestNotification() {
// If people choosed "Browse now", they might just wanna look around.
// So do not interfere them by a popup. The provisional permission can make
// our notifications in the notification center by default.
requestProvisionalNotificationSubject.onNext(())
browseResponder.browse()
}
将 SwiftUI 组件移动到 Boxue_iOS
最后一处要修改的,是 Boxue_iOS 中的 SwiftUI 组件。之前为了让这些组件使用 Xcode 的 UI 预览功能,我们把它放到了 Boxue_iOS target,并且在 Target Membership 中勾选了 Boxue:

但按照最初我们对这个工程结构的划分,可重用组件应该放到 BoxueUIKit 这个框架里。于是,我们把所有属于 UI 组件部分的 SwiftUI 代码都移动到了 BoxueUIKit / Reusable / SwiftUI group,并且去掉了这些文件的 Boxue Target Membership:

最后,在 Boxue_iOS 中保留的,只是和 App 具体页面内容相关的 SwiftUI 文件。例如上图中,保留下来的,都是在首页上要显示的滚动视图。这些视图也将通过访问 BoxueUIKit 这个框架对外提供的接口来实现相关的功能。
这样,和 SwiftUI 的部分就和整个项目最初的功能划分保持一致了。唯一的“缺陷”,就是我们不再能够使用 SwiftUI 的预览功能。不过这个问题就实际的开发体验而言,真的并没有想象中严重。一来,当前版本 Xcode 的预览功能并不好用,甚至当有些结果和想象不一样的时候,你还要怀疑预览是不是有 bug,与其这样,倒不如先不去用它;二来,当你真的熟悉了 SwiftUI 之后,在头脑中的自动布局,要比通过 Xcode 渲染出来,快多了 :)
What's next?
处理了这些不再使用的代码和配置之后,从下一节开始,我们就要着手准备内容浏览的首页了。这个过程比我们想象的复杂一些,因为它包含了一些在接下来构建起它界面时都会用到的机制,例如:
- 界面的部分功能要区分用户是否登录;

- 接收到服务器的返回结果之后,要在本地进行缓存,并实现本地缓存显示优先,通过异步方式自动更新界面的效果。这样,当网络不好,或处于飞行模式时,只要曾经读取过的内容,对应的界面就可以正常浏览。对于一个以内容为主的 App 来说,这样做用户体验会更好;
因此,接下来,我们要做的,就是获取网络数据,自动编码解码到 model,自动缓存到 Core Data,在得到数据后自动更新到界面,以及重新加载界面时自动识别缓存,这一系列的功能。这个机制,将为泊学 App 的绝大多数界面提供服务。