一份非常詳盡的 Objective-C 到 Swift 的遷移指南

jetleesg發表於2018-06-03

原文在我的部落格:一份非常詳盡的 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,避免一下重寫很多模組,導致沒有動力寫下去。後面幾乎全是體力活,就是時間問題了。

相關文章