在 Kickstarter-iOS 源码中学到的(二) - 代码

上一次我们简单介绍了一下 Kickstarter-iOS 项目工程方面的闪光点,这次我们探究一下,看看到底是哪些设计与细节让我推荐大家都来阅读这份代码。

Environment

Environment.swift

Environment 这个 sruct 这作为一个存储整个应用状态的存在。那这个应用可以随时获取的全局状态都有什么呢?

  • API 环境
    • apiService,包括了 appIDapiBaseUrl 等等。这里之所以把它算进 Environment 是因为在测试环境中,apiService 可能会发生改变。
  • 缓存
    • cache,一个 NSCache 缓存一些临时的数据
    • ubiquitousStore,背后是一个 NSUbiquitousKeyValueStore.default,用于放一些有没有出现引导相关的 flag,是 iCloud 同步的
    • userDefaults,其实就是 UserDefaults,用于持久化数据
  • 设备
    • devie,设备型号、系统版本等等
    • countryCode, language, locale, 一些设备国际化的信息
    • reachability,当前设备的网络环境
  • 当前登录用户
    • currentUser

… 等等

那是不是只要 AppDelegate 持有一个 Environment 的实例就可以完成任务了呢?

是也不是。

其实作为一个应用来说,一份 Environment 就足以完成任务了,但 Kickstarter 为了方便 test,搞出了一个 AppEnvironment

AppEnvironment

AppEnvironment.swift

如图,AppEnvironment 持有了一个 stack: [Environment],其实同时存这么多 Environment 是没有必要的。对于同一个应用来说,一个 Environment 就够了,那为什么它还要搞这么多出来呢?

搜索一下 AppEnvironmentpushEnvironmentpopEnvironment 方法的调用,几乎都会出现在 *Test.swift 中。主要作用是测试 Environment 发生变化时,保留旧的 Environment 做一些 state 的判断。

这个 struct 还提供了一个便捷的 replaceCurrentEnvironment 方法,根据你提供的想要更改的值,update 一下 current 的 environment(其实 push 也是同理,把 current 先 copy 一份,再改需要更新的部分)。这个方法就比较有价值了。比如在 debug 的时候,可以把 apiService 换成 mockService,测试的时候,直接更新 user 为一个 fake user,就直接登录成功了,非常方便。

关于 Environment 的设计,我是比较认可的。相比于全局的 state 满天飞,这种封装会让程序干净不少。AppEnvironment 中的 stack 如果只是用来测试的话其实完全没有必要存在,第一眼看上去有多个 environment 看上去有些 confusing,不过它是 fileprivate,其实也没啥,是吧?

Deep Linking

Navigation.swift

iOS 中的 deep linking 就是通过 URL Scheme、Universal Link、3D touch 或者 notification 等 URL 的方式跳转原生页面的方式。

objc.io 里也有一期关于 Kickstarter 是如何做 deep linking 的,感兴趣的可以看一下。链接点我

工程里的实现跟视频里的方案差不多。有一个 Navigation 的 enum,把所有支持通过 URL 跳转的都定义成一个 case,

public enum Navigation {
case checkout(Int, Navigation.Checkout)
case emailClick(qs: String)
case emailLink
...
}

Navigation 存在的意义,是是把一个通过 deep link 方式传入的 URL,解析成一个 Navigation 的 case

那它是怎么实现这个过程的呢?

allRoutes 中定义出所有支持 deep linking 的 URL 模板,以及它们对应的解析函数,构成一个 route 条目数组

暴露给外面解析 URL 的入口是 match(_ url: URL)

这个函数首先遍历 allRoutes 中的所有条目,尝试把这些 URL 与 allRoutes 中的 URL 模板匹配 (1)

模板匹配上了,根据模板中参数的部分把原有 URL 的参数取出来,构成一个参数名为 key,参数值为 value 的 [String:RouteParams](2)(3)

然后把这个字典传入 route 条目对应的解析函数(4),就可以顺利得到我们想要的 Navigation 枚举了(5),根据这些确定的枚举,就可以做出跳转了。

Deep link 写得特别出彩的地方是它的很多代码非常地函数式,其实解析的代码也非常适合写成函数式地。map reduce zip
结合操作符重载,代码可读性高,而且干净,阅读起来令人愉悦。

可以结合图看一下 deep link 的过程


现在再回头看看代码,应该是很清晰了。