Slug javi soto cover

使用 Swift 来构建 Fabric.app

当 Twitter 的 Fabric 团队在编写他们新的 iOS 应用时,他们转向了 Swift。为什么呐?Javi Soto 讲述了他们的决定,并且谈论了为什么他们要挑选这样一款强调稳定性和可维护性,并且支持依赖注入、代码生成、MVVM 以及错误处理的语言。


概述 (00:00)

我喜欢 Realm 为 Swift 社区所举办的这些大会。我能到这里进行演讲真是莫大的荣幸。(编者按:感谢 Javi!我们很荣幸能够请到这样杰出的演讲者!😊

我是 Javi。我来自西班牙的马德里,但是我已经在湾区工作了 4 年。之前我在 Pebble 工作,也就是 Kickstarter 资助的智能手表公司,我在那儿负责 iOS 应用的开发。当 Swift 首次发布的时候我就开始尝试使用 Swift 了,并且去年我的兄弟(就是那位坐在台下的 Nacho)和我一同发布了我们自己的首款应用,完全是用 Swift 编写而成。那个时候是使用的 Swift 1.1,同样它还包含了一个追更 Chad 广播的 Apple Watch 应用。现在我在旧金山,为 Twitter 的 Fabric 团队工作。

我非常❤️谈论 Swift 这玩意儿。编写 Fabric.app(这款应用即将上线!)是一件很有意思的事情。我只是想向大家分享一下,我在编写这款应用的时候所做的一些“特异”的事情(与我此前编写过的所有应用相比)。

何为 Fabric.app? (02:01)

Fabric.app 是一套专门为移动开发者编写的工具集。其中最主要的两个分别是 Crashlytics 和 Answers。我们的目标是构建一款移动应用,因为我们意识到有些时候当您不在办公桌面前的时候,总会有一些重大的问题出现,我们中的绝大多数人都需要能够对我们应用的运行情况保持关注,而无论我们身在何方。

这就是 Fabric 控制台在网页上看起来的样子。(参见上方的幻灯片。👆我们会向您展示(海量的)关于应用使用量和稳定性的实时和历史数据。但是对于移动应用来说,我们不仅仅只想把您能够在这个移动控制台当中所看到的所有内容搬到移动端上面:我们希望这款应用能够在移动端大放异彩。我们能够在移动端所做的一项最棒的事情就是通知了。Fabric.app 可以在您的应用出错的时候向您发出警告(例如,在线用户数量突然飞速下降)。我们同样也关注到大量的通知所可能带来的信息冗余问题:我们提供了细粒度控制,您可以控制您能够接收到的消息数量,并且您甚至可以设定几个小时或者几天之内忽略通知信息。(当人们正在度假的时候是不会想接收这些嘈杂的应用崩溃通知的!

工具 (03:35)

依赖管理 (03:39)

最近在 iOS 社区中,有很多关于您应该在应用中使用多少个第三方库的讨论帖,甚至还有一些是否应该完全不使用第三方库的讨论。在大型团队当中,为了用必需的人力资源来解决建立一个应用程序所面临的问题,或许您会想到要减少外部依赖的数目。但是 Fabric 应用只有一名工程师在开发(绝大多数开发时间下),并且单人开发也开始成为一种深入人心的观念。一开始我们希望能够证明 Fabric.app 是很有价值的,然后我们又希望能够进行敏捷开发。我们并不想要在证明过程中自行构建一个网络库。我们的初始目标就是避免重新造轮子。有一些问题,不管它简单还是不简单,我都不想花费额外的时间去解决它们。尤其是很多大神已经为解决这些问题做了很多很赞的工作。iOS 开源社区是非常棒的,这里有很多精妙绝伦的代码。

Tabric.app 同时使用了 CocoaPods 和 Carthage。我们并不建议大家使用多个依赖管理系统。这样就不能够看见各个系统的依赖结构图,并且您很可能会无意同时使用它们来解决同一个依赖(从而导致依赖库再一次被添加到您的应用当中)。然而(如果您对您的做法有清晰的意识的话)两者都有各自的优点。我之所以喜欢它们,分别有不同的原因。

CocoaPods (05:27)

CocoaPods 是一款被广泛采用的依赖管理工具。它所管理的库非常易于集成,因为它会自行为您修改 Xcode 项目。但是更为重要的是,如果您只打算在调试构建版本当中添加依赖的话,那么借助它会是一件非常简单的事情。我们可以在开发阶段当中使用 CocoaPods 来添加相关的工具,但是我们在最终的二进制版本中并不想要将这些工具一同分发出去。您或许会想了:CocoaPods 为您完成了所有操作,并且绝大部分库都支持 CocoaPods,我们完全可以到此为止了。但是为什么我们还是决定在 CocoaPods 的基础上仍然添加 Carthage 的支持呢?原因在于:编译速度。Swift 编译器的速度比 Objective-C 编译器的要慢得多。虽然 Swift 编译的速度会逐步优化,但是我觉得这将会是亘古不变的一个事实。将那些您很少进行改变的代码进行预编译或许总是一个非常好的想法。通过将某些库进行预编译操作可以加快我们应用的整体编译时间。

不幸的是,使用预编译库的过程中,仍然有一些和 Xcode 和 LLDB 相关的问题存在,也就是这些预编译库会编译到您 Xcode 项目之外,停留时间甚至会超过一年以上。因此这就涉及到取舍的情况了。无论怎样,只要 Swift 包管理器 (Swift Package Manager) 推出,那么我会迫不及待地切换过去。我已经等不及看到 Xcode 集成是如何出现的了。

Interface Builder (07:01)

我是 Interface Builder 的忠实粉丝。在 Fabric.app 中存在的布局代码是非常少的。只有某些视图是必须要动态创建约束的,因此我们使用 Cartography 库来实现这个功能,因为它极大地简化了语法。

Auto Layout (07:22)

可以在 Interface Buidler 当中使用 Auto Layout 约束极大地加少了 UI 代码的数量。除了复杂度方面的优势之外,Auto Layout 的另一个优势在于实现自适应表视图单元格,这各功能最终在 iOS 9 当中工作得很好。在没有 Auto Layout 之前,如果应用包含了多个表视图或者多个不同类型、动态高度的单元格的话,就需要手动管理大量的尺寸、布局代码。这对于快速迭代来说是非常糟糕的,尤其是当您只是想要添加个新视图并将其它视图移动位置的时候。

这并不意味着 Auto Layout 是非常完美的,也不意味着它易于使用(例如,您可以看到这个截图:如果您将手机连接到您的 Mac 上,您可以看一下 Springboard 当中正在输入的日志内容)。就连 Apple 都很可能会犯错。这是一个非常难的问题。但是我相信,在绝大多数情况下使用它是非常值得的。

Storyboards (08:28)

Fabric.app 的第一个正式版本:1.0,基本上是将大部分 UI 放到了一个故事板当中。说实话,我之所以这样做的原因是:我之前从未这样做过,因此我决定尝试一下。当我的同事 Josh 加入到团队当中之后,我们很快发现故事板对于多个代码贡献者来说是非常不友好的。因此在 1.1 版本当中我们将所有东西都移到了单独的 XIB 文件里面。

Receive news and updates from Realm straight to your inbox

UIStoryboard (09:10)

除了将所有 UI 放到同一个文件当中而导致的众所周知的问题之外,我还有一些不去使用故事板的理由。

UIStoryboard 是唯一一个可以用来从故事板中实例化视图控制器的 API。即使您使用了 segues,您仍然还是在使用这一类的 API。假设如果您想要实例化一个视图控制器的话,那么代码会是这个样子的:

class UIStoryboard {
    func instantiateViewControllerWithIdentifier(_ identifier: String) -> UIViewController
}

我们将其实例化之后,然后将相关的值传递进去。然后我们为其设置了一系列属性。

那么对于视图控制器本身来说,这段代码实际上看起来会是怎样的呢?您会发现您最终将不得不使用隐式解析可选值 (Implicitly Unwrapped Optional)(我也保证您并不喜欢这么做)——这就意味着您不能进行依赖注入操作。例如,在这个方法中我们并不能保证其他所必需的参数(例如:userSession)能否在运行时进行设置。

let vc = mainStoryboard.instantiateViewControllerWithIdentifier("ApplicationOverviewID") as! ApplicationOverviewVC
vc.userSession = ...
vc.applicationID = ...

也就是说,当此视图控制器出现在屏幕之上的时候,如果我们读取了为 nil 的隐式解析可选值的话,或者我们检查了空值但是却没做任何操作的时候(因为在绝大多数情况下我们并不能够做有用的事情),应用就很有可能发生崩溃。

final class ApplicationOverviewVC {
    // I hope you like "!"s...
    var userSession: UserSession!
    var applicationID: String!
    func viewDidLoad() {
        super.viewDidLoad()
        self.userSession.foo()
    }
}

在发布的 1.0 版本中,我用 Objective-C 编写了一个小小的黑科技,从而来解决这个问题。有趣的是,我必须要使用 Objective-C 才能完成此操作,因为 Swift 并不允许这样做(会告诉您这玩意儿是一个黑科技)。但是它同样也涉及到了隐式解析可选值的问题,因此这同样也不够安全。如果您对其感兴趣的话,我会将 gist 分享给大家。

这就是我所认为的视图控制器的理想状态。此视图控制器当中的 API 能够向用户明确它所需要的参数。并且如果没有提供必须的参数的话,就会导致编译时错误的发生。另一点好处是,成员属性现在可以是 let 敞亮了——这同样也简化了实现因为我们不必担心初始化之后值会发生改变。

final class ApplicationOverviewVC {
    let userSession: UserSession
    let applicationID: String

    init(userSession: UserSession, applicationID: String) {
            self.userSession = userSession
            self.applicationID = applicationID
            super.init(nibName: ..., bundle: ...)
    }

    func viewDidLoad() {
        super.viewDidLoad()

        // 保证编译时此对象能够完全被初始化
        self.userSession.foo()
    }
}

总而言之,没有故事板 == 美好的生活。

fastlane (11:19)

我非常喜欢 Fastlane (译者注:Fastlane是一组工具套件,旨在实现iOS应用发布流程的自动化,并且提供“一个运行良好的持续部署流程”,只需要运行一个简单的命令就可以触发这个流程。)💖我们将其应用在我们的应用当中,从而对许多任务进行了自动化操作:例如自动增长内部 build 版本号,自动创建标签,自动为 App Store 构建测试版本,自动存储快照以及自动更新快照等。我强烈推荐大家使用这个工具,它能够节省我们开发的时间,甚至当我们发布第一个版本之后,仍然还在帮我们节省时间、加快工作效率。

Fastfile 根据要进行自动化任务的不同而需要不同的配置,我们在我们构建的这个 Fastlane 示例当中将我们的配置开源了出来,以供大家参考。

ReactiveCocoa (06:46)

不要害怕,我并不是想讲解 ReactiveCocoa 的有关知识。我只是鼓励您,如果您感兴趣的话,可以去看一看我此前在 Swift Summit 大会当中关于该主题进行的演讲。我之所以提到它是因为它在 Fabric.app 的架构组成中占据了很大一块位置。

我想向大家展示一个小小的示例,关于我们是如何通过 ReactiveCocoa,从而以一种简单的方式来重用和撰写逻辑。在这个示例当中,我们已经创建了对 ReactiveCocoa 的扩展方法,这段代码搭建了一个异步任务,从而可以消除您账户当中的所有通知。通过调用我们在这个任务当中实现的这个方法,它允许我们确保请求仍在继续执行,即时应用位于后台运行状态。

self.accountService.muteAllNotifications(untilTime: date)

一旦实现此功能的代码写完之后,它就可以在应用的所有异步任务当中进行重用了。这就是 ReactiveCocoa 的一个简单调用示例。

self.accountService.muteAllNotifications(untilTime: date)
    .continueWhenApplicationIsBackgrounded(taskName: "Muting notifications")

架构 (13:25)

我们很难在幻灯片中对应用的整体架构进行描述展示,但是我准备重点提及两个方面。根据我以往的经验来看,我决定采取实用点的方法,也就是以 Xcode 项目配置开始来进行介绍。理想情况下,我应该可以将我的代码库分解为各个小型、孤立的框架。但是这会给您 Xcode 项目配置带来很大的复杂度。出于此原因考虑,我们将绝大部分业务逻辑分解到一个单一的 Fabric API.framework 当中。其背后的想法是让这个框架不必基于 UIKit 进行。在大多数情况下,Fabric API.framework 主要是完成所有网络操作以及与 Fabric 后端进行交互,从而将应用所必须的数据模型进行交互。从某种方面来看,它可以在别的架构或者平台上面进行重用。

Fabric.app 当中的多屏幕是使用 UITableViews 来完成的,我想要介绍一下我是如何处理它们从而能够减少样板代码 (boilerplate) 数量的。这里没有什么新鲜的事情,最主要的目的就是避免臃肿视图控制器的出现。我所使用的模式是一种称之为 MVVM 的模式。虽然我并没有严格遵循这个模式,不过我还是从线上源代码当中获取到了许多灵感。我们将所有感兴趣的逻辑放到视图模型 (view model) 当中,从而来控制各个屏幕的显示。这使得视图控制器变得非常简化。此外,视图模型执行必要的网络请求操作,将特定试图所需要的数据拉取下来,然后将所有东西都结合在一个结构体当中。暴露在外的 API 是一个位于结构体当中的可视视图,当请求结束并且可以被视图控制器看到的时候就会发生改变和变化。

GenericTableViewDataSource (15:43)

不过我总是一遍又一遍地做同样的事情:我需要对结构体进行观察,然后更新表视图当中的内容。我从中提取出了一个抽象对象。绝大多数表视图都是异步更新的,并且这些请求都是在后台结束的,此外它们在同一个屏幕当中还展示了不同类型的数据。

protocol TableSectionType {
    associatedtype AssociatedTableRowType: TableRowType

    var rows: [AssociatedTableRowType] { get }

    var title: String? { get }
}

protocol TableRowType { }

我就是我所撰写的通用代码的一部分。首先,我用了两个协议,分别表示节 (section) 的类型和行 (Row) 的类型。Section 包含一组 Row,以及一个可选的标题。

创建完此数据源之后,我就可以通过表视图所需要的内容对其进行初始化了。

final class GenericTableViewDataSource<Elements,
    SectionType: TableSectionType,
    RowType: TableRowType
    where SectionType.AssociatedTableRowType == RowType,
        SectionType: Equatable,
        RowType: Equatable>: NSObject, UITableViewDataSource

在这个 GenericTableViewDataSource 的实现当中,我可以实现相关的逻辑,比如说比较视图中两个数据快照的不同之处,以及使用动画来更新表视图,或者只更新发生变化的 Row 或者 Section。当您实现其中一个表视图的时候,代码如下所示:

enum ProjectIssueRow: TableRowType, Equatable {
    case Loading
    case NoIssues
    case ErrorLoadingIssues
    case ProjectIssue(Issue)
}

return GenericTableViewDataSource(
        tableView: tableView,
        tableViewData: observableProperty, // Observable<Elements>
        computeSections: { elements in ... }, /// Pure function from `Elements` to `[SectionType]`
        configureRow: { row, indexPath in ... } /// Function from `RowType` to `UITableViewCell`
        )

您可以自行实现 Row 的类型,并且我们还有一些不同的情况。例如,我们有一个表示正在加载过程的 Row。这个是一个在 1.2 版本当中所推出的新特性,从中我们可以向用户展示应用当中的所有问题。您需要使用 ReactiveCocoa 来传递表视图、表视图数据这两个对外公开的属性。然后接下来再来处理两个重要的闭包。第一个是一个纯函数,用于通过给定当前数据的状态来构建必须展示出来 Section 和 Rows。如果请求仍在加载,那么它就会用一个加载指示器来构建 Row,如果加载失败,那么就会展示对应的错误信息。最后一个闭包是通过给定一个 Row 来构建一个单元格。我会在下面展示我们是如何轻松地以一种类型安全的方式来实现表视图单元格的异步加载的。如果您想要了解更多的代码的话,我会很高兴在网上与大家分享出来。

Swift 让生活更美好 (18:08)

关于我为什么喜欢 Swift,我想要提及的有三点:可空性 (Nullability)、类型安全的 JSON 解析以及代码生成 (Code Generation)。虽然我有了多年的 Objective-C 编写应用的经验,但是这三点在我使用 Swift 制作第一个大型应用期间仍然给了我极大的帮助。

可空性 (18:26)

Ayala她就坐在观众席当中)曾经发过这么一段著名的 Tweet如果您还没有关注她的话那么我强烈建议您赶紧去,她对 Swift 研究颇深,并且发表了许多精彩的讲演):

可空性是非常棒的,因为我们可以使用不可空特性。

这不仅仅是一个哲学上的论点。通过借助可空性,我们在编写 Fabric.app 的时候就可以证明这些事情。同时,可空性还能够避免添加了冗余的需求,同时还减少了测试的数量(与此同时还可以让我在晚上睡个好觉)。

我想要展示两个例子。第一个例子是关于用户认证方面的。我想向大家展示这些用以填充屏幕的代码,它们非常地简明扼要。展示的内容是关于您账户当中的应用清单。下面让我们来看一看这个视图控制器是长什么样的:

final class ApplicationListViewController: BaseFabricTableViewController

这个界面是您登录系统之后展示的第一个界面,其中包含了一个用以展示您账户当中相关应用的列表。为了实现这个功能,我们需要去访问一些能够封装此用户会话的对象。这里展示的这段代码是一段非常精简的版本,不过和它原本的样子也是很类似的。根据我的经验来看,使用全局访问器 (global accessor) 这个方法来解决这个问题是非常常见的。我们现在有了用户会话类 (UserSession),并且我们还有了一个静态方法或者变量,以作为可变的“单例”模式存在。但是问题在于(如果您使用 Swift 的话您会更快地意识到这一点):UserSession.currentUserSession 属性必须要返回一个可选值,因为在用户登录之前,是没有任何用户会话存在的。

final class ApplicationListViewController: BaseFabricTableViewController {
 override viewDidLoad() {
     super.viewDidLoad()

     let session = UserSession.currentUserSession

     if let session = session {
         session.requestApplications()...
     }
     // or...
     session!.requestApplications()...
}

当我们触发了这个 viewDidLoad 方法之后,我们就能够获取到用户会话,我们应该要做些什么呢?我们是有条件地将这个用户会话进行解包,还是直接使用感叹号?这两种选项都不是最优解。其中一个可能会导致用户会话被损坏,因为在这个 else 语句当中我们并不能做任何有意义的操作。如果代码卡在其中的话,那么就意味着屏幕很可能就不能够显示。此外这往往还可能会导致应用崩溃。对于另一个选项来说,它同样也会导致崩溃。显然这两种方法并不都是好办法。因此您或许会想了,在用户登录之前,在视图控制器外部的代码当中并不能够展示,因此这没有任何问题。这真的没有任何问题吗?通过将 currentUserSession 展现给视图控制器,这里的代码可能会造成危险情况的发生。这很有可能会造成极其严重的错误。

final class ApplicationListViewController: BaseFabricTableViewController {
 init(viewModel: ApplicationListViewModel)
}

final class ApplicationListViewModel {
 init(fabricAPI: AuthenticatedFabricAPI)
}

public final class AuthenticatedFabricAPI {
    public init(authResponse: AuthResponse)
}

public final class AuthResponse {
     let accessToken: String
}

这就是为何我极力推崇使用此方法来解决上述的问题。在这些代码当中,视图控制器需要一个视图模型,用以负责发送网络请求。这个视图模型必须使用一个 非空的 (这是非常重要的)AuthenticatedFabricAPI 进行初始化。AuthenticatedFabricAPI 自身可以无需使用 AuthResponse 结构体就可以进行初始化操作,只需要换成 accessToken 就可以的,同样这也要求需要是非空的。这代表着我们可以证明,在编译过程中,此视图控制器(用以显示用户数据的)如果没有获取用户会话以及访问令牌以便能够执行认证请求的话,那么它将无法展示在屏幕上。这是一个很棒的设计。

让我再来引用一遍 Ayaka 所说的话:

可空性是非常棒的,因为我们可以使用不可空特性。

在上面的代码之中,没有任何属性是可空的。这同时也给了我们一个保证:这些必需的东西一定会在在运行的时候展示出来。

让我们来看一个稍微不同的例子。这个例子来自于我最近的开发工作(在多次重现错误之后,我提议改进的东西)。让我们再次回到同样的视图模型当中来:

final class ApplicationListViewModel {
    var applications: [Application]?
}

显然,applications 属性一直会发生变化,同时它是一系列 Application 值得数组,用以预测我们很可能会处于请求仍未结束时候的状态。这是一个可空的数组。如果它为 nil,就代表着这个网络请求仍未结束。那么视图控制器该怎么表示请求结束但是请求失败的状态呢?这里是否有可能会出现一个空数组,或者同样也会出现一个为 nil 的值?我十分肯定绝大多数人都会觉得 nil 是一个很好的解决方案,不过基于过去遗留的某些原因,我对此有着不同的看法。不过我对此也存在一些困惑。我觉得导致这个问题的基本原因在于缺乏明确性;我们试图只有一个可空类型来表示多种可能出现的状况,而这往往会导致错漏百出的情况发生。这就是我接下来所要说的:

enum DataLoadState<T> {
    case Loading
    case Failed
    case Loaded(T)
}

final class ApplicationListViewModel {
    var applications: DataLoadState<[Application]> = .Loading
}

这玩意可不是火箭这一类的黑科技。 🚀我利用了 Swift 枚举类型的优点,这对于值的表示来说是非常神奇的,因为只需要一些有限的形式就可以实现。在这种情况下,我们可以选择是在加载中,加载失败亦或是已经加载数据了。可空类型只能有两种情况,然而我们想要三种;UI 可以让使用者看到我们是否在加载,我们如果失败了我们会想显示一个错误,或者我们可以已经成功的加载了,不过我们有了一个空的应用序列(我们可以负责任的说这是一定会有的)。我们读取这个应用变量的时候,可以让我们知道它是否在加载中。我不敢保证说这是一个很赞的 API 然后大家都应该使用它,不过这简化了一些我写的代码,让解决这个问题更清晰,更安全,更难以犯错误。

类型安全的 JSON 解析 (24:47)

不用担心:我不是在编写一个新的 JSON 解析库(因为现在已经够多了!)。让我们来看看几个关于 JSON 解析的糟糕例子:

public struct Application {
    public var ID: String?
    public var name: String?
    public var bundleIdentifier: String?

    public mutating func decode(j: [String: AnyObject]) {
            self.ID = j["id"] as? String
            self.name = j["name"] as? String
            self.bundleIdentifier = j["identifier"] as? String
    }
}

这是一种实现 JSON 解析的最简单方法。我们让结构体当中的所有成员变为可空,同时我们使用正确的形式来解析 JSON 字典,然后将值赋给所有的成员。这不会出现冲突的情况,因为我们进行了类型检查,但是我们结束完 JSON 解析之后,结构体当中的某些值仍然还是为空。这并不是一个很理想的情况,因为无论何时,尤其是我们的应用需要与这类结构体进行交互的时候,我们都需要将这些值进行解包。这意味着我们不得不要处理值可能为空的情况,并且很明显,这并不是这种情况下的最优解决方案。这也是一种非常笨拙的处理方式。记住:可空性是非常棒的,因为我们可以使用不可空特性。不得不到处处理值为空的情况是不太好的选择。

因此我们需要对其进行改进,同时这也是非常普遍的 JSON 解析做法。我们并不准备用可空类型来替代结构体,我们会使用一个静态函数返回可空的 Application 值。如果这个过程中解析失败的话,那么就会返回一个空值。这比我们之前的方法要好很多,因为我们不必对可空值进行处理。

public struct Application {
    public let ID: String
    public let name: String
    public let bundleIdentifier: String

    public static func decode(j: [String: AnyObject]) -> Application? {
            guard let ID = j["id"] as? String,
                  let name = j["name"] as? String,
                  let bundleIdentifier = j["identifier"] as? String else { return nil }

        return Application(
            ID: ID,
            name: name,
            bundleIdentifier: bundleIdentifier
            )           
    }
}

不过这段代码仍然存在问题:如果有一部分值没有解析成功的话,那么我们是没有办法准确地知道错误发生的地方(例子中我们丢失了一个关键值,类型错误了么?)。在 Fabric 应用中我们使用了很棒的 Decodable library 第三方库。

代码生成 (27:28)

这是 Fabric 中的一个代码片段:

import Decodable /// https://github.com/Anviking/Decodable

public struct Application: Decodable {
    public let ID: String
    public let name: String
    public let bundleIdentifier: String

    public static func decode(j: AnyObject) throws -> Application {
        return try Application(
            ID: j => "id",
            name: j => "name",
            bundleIdentifier: j => "identifier"
        )
    }
}

优秀的 JSON 解析库有很多,不止这一个。我倾向于使用能够根据 JSON 直接生成 Model 的 JSON 解析库,这样能保证类型安全。我们在手工解析 JSON 的时候难以避免的一些细节问题,而这正是 Docode 所擅长的。这样能节省我们的开发时间还免除了我们调试的麻烦。而且使用它还不会让您的代码变得更复杂。

我还要介绍我在 Fabric 中使用的另一个很棒的工具,这样大家大概就会相信我们所讲的一切都是极有价值的。Swift从刚刚发布起,就相当重视安全性。但其中仍然存在很多不安全的东西,我以字符串输入作为例:

/// Swift < 2.2
UIBarButtonItem(barButtonSystemItem: .Done, target: self, action: Selector("buttonTapped"))

// Swift 2.2
UIBarButtonItem(barButtonSystemItem: .Done, target: self, action: #selector(ViewController.buttonTapped))

其中一个问题就是创建 Selector 的时候。在 Objective-C 中,我们有 @selector 语法,Swift 中创建 Selector 比 Objective-C 还更不安全。幸好 Swift 2.2 修复了这个问题,我们有了新的 #Selector 语法,在编译时就可以确定对应类中的 Selector 是否有效了。

还有其他的使用字符串作为输入 API的例子:

super.init(nibName: "ViewControllerNibName", bundle: nil)

let nib = UINib(nibName: "NibName", bundle: nil)

tableView.registerNib(nib, forCellReuseIdentifier: "ReuseIdentifier")

let cell = tableView.dequeueReusableCellWithIdentifier("ReuseIdentifier", forIndexPath: indexPath) as! MyTableViewCell

let image = UIImage(named: "ImageName")!

实例化视图控制器、取得 Nibs、使用 cell 的重用标识符来注册 Nibs、从 Bundle 中取得图片等等,这些都太容易出错了。当您应用里的 cell 种类增多之后,尤其是当您想对您应用的一部分进行重构的时候,犯错的可能性太高了。

R.swift (29:56)

Fabric 中,我们还用了一个库,叫做 R.Swift参见幻灯片)。

每次我添加一个 Nib 到应用之中,或者做了其它改变的时候,我们都会在命令行中调用 fastlane code_gen , 它会调用 R.Swift 并自动生成一个 R.generated.swift 文件,其中包含:

  • 对 Nibs 的引用
  • cells 的重用标识符
  • 目录中的图片名,以及其它文件名。

它是怎么做到的?它用起来什么样?

super.init(nibResource: R.nib.myViewController)

class MyTableViewCell: UITableViewCell, ReusableNibTableViewCell {
    /// 如果 Cell 不正确,那么根本无法编译
    static let nibResource = R.nib.myTableViewCell
}

tableView.registerReusableNibCell(MyTableViewCell)

let cell = MyTableViewCell.dequeueFromTableView(tableView, indexPath)

let image = R.image.imageName

这些是一些关于使用 RxSwift 的相关例子,其中有部分使用了我写的辅助API,以便用于用于简化代码,但这全都是倚仗于字符串被转化成了静态属性,从而我们可以访问。这样我们在编译时就可以验证 Nibs 的名字、重用标识符、图片名等等。它用起来太棒了,我已经不可能再像以前一样使用硬编码字符串了。它还可以可以做到一点:我们甚至可以验证返回的 Nib 类型与 Cell 是否匹配。

错误报告 (30:42)

当我们写代码的时候,调用 API 失败会报告相关错误。我们已经看过有关 JSON 解析错误的例子,这里是另外一个。

do {
    try fileManager.createDirectoryAtPath(path, withIntermediateDirectories: true, attributes: nil)
    /// ...
}
catch {
    print("Error: \(error)")
}

比如我们调用了一个可能会抛出错误的基础库 API,有时候调用 API 会发生错误,需要给用户弹出一个提示。但也有时候,我们会直接把错误信息打印到控制台上,结果却丢下不管,因为我们确实没有其他更好的方式来处理这些错误。因此我们的处理方式一直都是这样。但是您一定想知道,错误处理在真实产品中应该是如何进行的呢?我们写代码的时候并不清楚,是否可以采取一些措施来避免错误的发生,或者在错误发生的时候采取一些补救措施。导致错误产生的原因有可能是应用里其它部分的代码导致的,而且只出现在某几个版本之中,这使得我们几乎没办法发现这些错误。设想一下您的应用崩溃了,而您因为没有崩溃报告找不到错误原因。那些不致命的错误虽然不会使您的应用崩溃,但您的应用却因此不能提供给用户最好的体验。所以 Crashlytics 能提供用于错误记录的 API 实在是太棒了。

do {
    try fileManager.createDirectoryAtPath(path, withIntermediateDirectories: true, attributes: nil)
    /// ...
}
catch {
    Crashlytics.sharedInstance().recordError(error, withAdditionalUserInfo: userInfo)
}

只需要一行代码(非常类似于print)就可以非常容易实现这个功能,而且它让我知道错误在真实产品中到底会不会被发生,在 Fabric 中我们记录了所有错误。我们可以记录错误的数量和详细信息。在 Fabric 中,每当错误触发的时候,我们也可以直接发送通知,这样您在Fabric 中会收到错误相关的通知信息。

这里展示了 Fabric 的真实案例(见幻灯片),比如一个 JSON 错误,我们可以看到一个键(叫 stability )丢失了。我鼓励您去试一试,如果关于这部分您有任何问题,我们很乐意解答。

如果您对我们怎么做东西很好奇,也欢迎通过 Twitter 来联系我,或者发邮件 (javi@twitter.com, fabric-app-iOS@twitter.com) 给我。

谢谢。

问答时间到 (33:16)

问:我非常想知道您对 ViewModel 的处理方式,我个人非常喜欢使用 ViewModel,但我却不知道处理 ViewModel 的最佳实践是什么。在使用 ViewModel 的时候我比较习惯使用语言中一些纯粹的简单的对象,但是我发现您会使用一些非常复杂的对象,比如什么什么API。您这样做的原因是什么?因为我觉得这会让调试变得更复杂,因为可能您得模拟出一个对象来做测试?

Javi:实话说,我们从来不对应用中的 ViewModel 做测试,我们只是对 ViewModel 传入的代码做测试。确实,我们要把 AuthenticatedFrabricAPI 对象传进 ViewModel 之中,如果您想要测试,您得写一个协议,您可以创建一个这个对象测试版本,这的确有点难度,尤其是在 Swift 中,因为您没法创建一个它的子类,我也没太好的回答,但是我觉得如果您的对象不明确,而 API 接口很清晰,传一个很类似真正对象并且不发送任何请求。另一件事就是我们想要模拟网络请求,它没有单元测试,但是我们可以通过OHTTPStubs,我们用它测试所有的网络代码,并在它访问网络之前捕获所有请求,这是另一种方法。

问:讲得好,我的问题是怎么设置应用中的先决条件?我觉得这并没有太多共性,但是有时候您的确想要用断言,但是这样,如果您的应用进入了错误的状态,就很可能导致崩溃。您是怎么处理这个问题的?

Javi:这确实很有挑战性,我个人的观点是在应用中保留一定数量的断言,我一般会使用的更多,但是在 Swift 中我觉得我可以在编译的过程中就验证更多的东西。比如在 Objective-C 之中我通常每个方法都会使用 NSParameterAssert 来验证我需要的对象并不为空。在 Swift 中我不需要这样做了。还有一点就是如果出现了不应该发生的状况,我一般直接让应用崩溃掉。我可以可以只是报告一下错误,或者了解一下这个错误,然后不让应用崩溃。但是如过这个状况会导致应用完全无法使用,我更倾向于直接崩溃。我没法举出一个在 ` Fabric 中的例子,我们只在调试状态下使用一些断言,来帮助我们确定某些方法是不是执行在主线程,线程有时候会很复杂,您有时候就会不经意的使用非主线程修改了 UI,还有很多这样的情况。是的,确实有很多的断言,我觉得我很幸运的可以在编译时就做好这些东西,比如使用代码生成,我不再需要使用断言来判断是否 UITableViewCell` 返回了错误的类。

问题:讲得好(每个人都这么说),我注意到您说您的 ViewModel 会有一些变量,但是您也讲过您用 ReactiveCocoa。我很好奇,您可不可以讲一下为什么您不同时让它可以被观察呢?

Javi:让我用我觉得您提到的那页幻灯片来讲,这是伪代码。我并不想用 ReactiveCocoa 语法,但是用 var 可以让它随时被修改,实际代码中应该用 let ,内部有一个可变属性,但它是私有的,只是我们改了它,所有不可变的属性都改成了可变的。

问:我想问问画图,您们用什么库来画图?

Javi: 我之前完全没提到,我们用 JBChartView 来画图

问:我有几个关于不致命错误的问题,我没听清这部分特性,即便是应用不崩溃也要报告么?

Javi: 是的,就像崩溃了一样,我们把栈的状态记录下来,但是如果不崩溃的话,报告的种类是不同的,应用运行之中您发多少报告都可以。

问:太棒了,能介绍一下故事板的 OC Hack 么,我对这个非常好奇,因为我自己的文章中也有很多相关内容。

Javi:关于这个我会发推特。

问:还有表视图,我不确定您需要支持 iPad 么,或者您有这样的计划么?因为尺寸适配问题我们因为一些原因一直没有解决。

Javi:我对此不是非常感兴趣。这个应用只支持 iPhone,我们发现大多数的人使用 iPhone,同时支持二者肯定会相当复杂。我们想要开发的快一些。我不能说我们到底有没有支持 iPad 的计划,但是之后会说的,我不知道我们现在的 Nibs 能帮上多大忙,因为这同时取决于您到底想让 iPad 版的样子有多不一样,如果您只是想让它变得更大一点,我猜您五分钟就能实现,但是我不知道它看起来到底怎么样。

问:您试过集合视图的 Cell 么,因为我们这部分还有些问题。

Javi:是的,我猜因为我用表视图太久而导致我太偏向于它了,关于这些问题我用表视图试过,但是我在考虑到底用不用集合视图,或者我能不能解决这些问题,或许有一天我会试试。

问:最后一个问题,您提到您通过 API 委托来查看控制器,您确定这是真的吗,您怎么安全的做到的,我们来谈一下,您在栈里有很多控制器然后一个个 push 到栈顶,而令牌过期了,您怎么传这样的反馈信息出来?

Javi:这是个好问题,这样做需要点技巧,我告诉您我们怎么做到的,我不知道这样是不是最好的方式,或者如果有更好的方式,我必须学习一下。这样做很有挑战性,有个问题是 API 客户端在整个应用中都是共享的,这样或许不太好。令牌自己处理刷新问题。它在自己的串行队列中处理。如果有请求到来,它会一直等到刷新,您的控制器不需要担心这个问题,只要您接入了认证的 FabricAPI,它含有一个令牌,即便是它在您请求的时候并不合法,它也会马上刷新。

问: 我们发现这带来了其他问题,我们仅仅尝试去更新它,让它出现一两次,不过当它有资格之后,如果我们屏蔽了这个账户,是否所有的都不会成功?您们解决了这种情况么?

Javi: 暂时还没有,不过据我所知,当我们尝试改进它的可能性时候,发现了我们会有一堆 401 错误,这会导致您账户被登出。与此同时,我们在您点击登出时候所做的事情,是一种非常简单的方法,在这里我销毁了一切。我从窗口改变了根视图控制器,这让现在那里的视图控制器重新排列,同时我分配了登陆界面,这也是我们在界面上所能做的事情。

About the content

This content has been published here with the express permission of the author.

Javi Soto

Javi Soto is a Swift mobile engineer in the Fabric team at Twitter. He has previously worked on the Pebble iOS app, and has been working on iOS apps since 2010.

4 design patterns for a RESTless mobile integration »

close