原文在我的博客:一份非常详尽的 Objective-C 到 Swift 的迁移指南

国际友人可以看这里:A guidebook for migrating from Objective-C to Swift

运行环境:Xcode 9,Swift 4.0/4.1

按照惯例先说几句废话,Swift 在刚发布的时候,我学过一点点,写了几行代码,第二年发现以前的代码不能跑了,就弃坑 Swift,再加上实习过的公司主要用 OC,确实没机会系统的学一下 Swift,近来发现一些想要的第三方库,都只提供 Swift 版本,以及一些其他原因,决定把公司的项目完全用 Swift 改写。

认识我的朋友可能知道,我在去年年底发过一篇文章,叫《从重构到吐血 - 我是如何删掉 6 万行代码并且不删减原有功能的》,当时花了几周时间重构了所有代码,三个项目。

最近也一样,花了三四天时间,重写了其中一个项目,并且整理出来一些经验。目前除了一些必须依赖的第三方库比如 AliyunOSS,全部转到 Swift 了,可以说是 Almost Pure Swift。

如果写太详细的话,篇幅就太大了,所以有些地方会省略一点写。

先大概列个提纲,我打算讲讲可选类型,重写的顺序,网络层,数据层,UI 层。

可选类型

我认为一门语言,语法奇怪不是很大问题,熟悉下就好,但是 Optional 类型是真的难理解,! ? ?? 这类符号傻傻看不懂,最开始解析个 json 到处都是 ?,再加上网上各种文档,素质参差不齐,越学越迷茫。

Optional 类型很好理解,只是区分了下 nil 和 非 nil,如果这个 property 不一定存在,比如后端传来的 json,有时候格式是空数组 [],有时候是 null,这两种在语义上理解都是空,但是对 Swift 语言是完全不同的。具体的我会在数据层详细写下。

重写的顺序

最开始的打算是慢慢迁移到 Swift,先从最边缘的模块开始写,UI 改版再重写以前的代码,后来越写越上瘾,感觉找回了本科做项目的感觉,通宵写代码,就索性全部重写了。

还有一个原因是写着写着发现有些通用的部分,和之前的 OC 代码有关联,新的模块用 Swift 写会有无法混编的情况,比如 Swift 的结构体,非继承自 NSObject 的类,在 OC 无法正常用。

总的顺序还是从边缘到中心,先写最边缘的业务代码,比如某个刷新列表,这个时候就要写 Swift 的网络层,数据层,这两块也可以和 UI 层掺杂着写。

网络层

Swift 的网络层一般做法是用 Alamofire,我们的 app 不算复杂,只是对 Alamofire 做一个封装就够了。最开始我执着于遵循 Alamofire 的链式调用,发现好鸡儿难,然后惊喜的发现 Swift 也有 block,于是用了模仿 OC 网络层封装的方式,做一个单例,封装下 request 方法。

单例的写法,有好几种,篇幅限制,我直接贴出最佳实践,至少是 Swift 4.0/4.1 的最佳实践。

class APIService {
    
    static let shared = APIService()
}
复制代码

然后就可以往里面添枝加叶了,比如在 init() 方法设置一些网络状态监控,一些通用的设置,我就贴一个精简版的,然后可以按照官方文档,写一个 AccessTokenAdapter,用来处理头部的授权信息。

lazy var sManager: SessionManager = {
    let l = (UserDefaults.standard.object(forKey: "AppleLanguages") as! Array<String>)[0]

    var defaultHeaders = Alamofire.SessionManager.defaultHTTPHeaders
    defaultHeaders["User-Agent"] = "Customized UA"
    defaultHeaders["Accept-Language"] = "\(l),en;q=0.8"
    let configuration = URLSessionConfiguration.default
    configuration.httpAdditionalHeaders = defaultHeaders
    let sManager = Alamofire.SessionManager(configuration: configuration)

    return sManager
}()
复制代码

在有些开源项目里面,网络层分层过于详细,url 封一层,每个请求写一个函数,而且写的还贼鸡儿丑,仍然一大堆重复代码,重复的 hard code 字符串,不知道这种封装的意义何在,每加几个 api,要新建一个类,然后写 url 层,再写每个 request 的函数,封这么多层,仍然到处可见字符串硬编码,还都是重复的。

关于 request 的封装,我做了非常基础的封装,毕竟 app 没那么复杂,Alamofire 的部分太长了,大概思路就是根据传过来的参数,设置请求的序列化方式,设置 headers,设置参数等等,为了方便一些不需要传参的 get 方法,我做了这么一个操作:

func request(path: String, success: ((Any) -> Void)?, failure: ((ErrorModel) -> Void)?) {
    request(method: .get, path: path, params: nil, paramsType: nil, requireAuth: true, success: success, failure: failure)
}

func request(method: HTTPMethod, path: String, params: [String: Any]?, paramsType: ParamsType?, success: ((Any) -> Void)?, failure: ((ErrorModel) -> Void)?) {
    request(method: method, path: path, params: params, paramsType: paramsType, requireAuth: true, success: success, failure: failure)
}
复制代码

调用的时候大概就是这样:

APIService.shared.request(path: "/get/some-list/api", success: { (data) in
    
    let array = data as? [[String: Any]] ?? []
    let data = try! JSONSerialization.data(withJSONObject: array, options: [])

    guard let items = try? JSONDecoder().decode([ItemModel].self, from: data) else {
        return
    }
    
    tableView.reloadData()
}) { (error) in
    
}
复制代码

as? 是为了防止后端返回 null 而不是 [],如果真返回了 null,?? 的作用是给 array 一个默认值,保证 array 一定是 Array 类型,而不是 Optional,方便后面的解析。

数据层

Swift 在结构体方面真是强大了太多了,篇幅关系不写那么多,Swift 4 引入了一个原生 json 转模型的方法,而且我还发现一个国人,翻译了老外的文章,不注明原地址,当原创了。

原文:Ultimate Guide to JSON Parsing with Swift 4

原文写的很详细,代码不再贴了,需要注意的是,如果后端返回数据不够规范,多用几个 ? 避免 crash。

同样的,数据放在数据层处理,善用计算属性,举个例子

struct ActivityModel : Codable {
    
    let createTime: Date
    
    var createTimeString: String {
        return createTime.formattedString(withDateFormat: "yyyy-MM-dd")
    }
}

struct OrderModel : Codable {
    
    let currency: String
    let status: OrderStatus
    var statusString: String {
        switch status {
        case .deleted:
            return "Deleted"
        case .created:
            return "Created"
        case .paid:
            return "Completed"
        case .cancelled:
            return "Cancelled"
        }
    }
}
复制代码

UI 层

UI 层其实是最简单的,lazy load 直接用 lazy var get 重写,Masonry 布局代码可以很方便的转成 SnapKit 代码,UIKit 框架的代码直接翻译即可。

[aView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.mas_equalTo(self);
    make.bottom.mas_equalTo(self).offset(-5);
    make.leading.mas_equalTo(self);
    make.trailing.mas_equalTo(self);
}];

[bView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.mas_equalTo(self.aView.mas_bottom);
    make.leading.mas_equalTo(self.aView).offset(12);
    make.height.mas_equalTo(45);
    make.width.mas_equalTo(45);
}];
复制代码

方法名用 copy paste 解决,然后开启编辑器的替换功能,将 mas_e 替换成 eTo(self. 替换成 To(mas_ 替换成 snp.); 替换成 )。老实说,这部分改写,是最轻松的?

其他

在重写的过程中,把 AppDelegate 改成 Swift 之后,发现不再需要 main.m 了,查询资料得知这是正常的,@UIApplicationMain 帮我们做了这件事情。

再就是有些函数可以用 extension 的方式写,可以写的很优雅。

总之,Swift 上面还是有着很多 Cocoa 的影子,尽管他有很多新特性,在设计模式方面,跟 OC 差异不大,也可能我入门时间短,写法太 OC 化,所以如果有类似的,还请多多指正。

重写工作也没那么难,我们的 app 虽然不大,其实也不小,有完整的用户模块,有购物模块,订单模块,支付模块,推送模块,几天时间就全部改写完毕,并且已经在测试,目前还没发现有很大问题。

最基础的模块先搭建,比如主题颜色管理,API 模块,一些工具类,基础框架搭好之后,因为 OC 代码可以被 Swift 调用,在开始的时候,做好计划,小的模块先调用 OC,避免一下重写很多模块,导致没有动力写下去。后面几乎全是体力活,就是时间问题了。