一份非常详尽的 Objective-C 到 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
替换成 e
,To(self.
替换成 To(
,mas_
替换成 snp.
,);
替换成 )
。老实说,这部分改写,是最轻松的😂
其他
在重写的过程中,把 AppDelegate 改成 Swift 之后,发现不再需要 main.m 了,查询资料得知这是正常的,@UIApplicationMain
帮我们做了这件事情。
再就是有些函数可以用 extension 的方式写,可以写的很优雅。
总之,Swift 上面还是有着很多 Cocoa 的影子,尽管他有很多新特性,在设计模式方面,跟 OC 差异不大,也可能我入门时间短,写法太 OC 化,所以如果有类似的,还请多多指正。
重写工作也没那么难,我们的 app 虽然不大,其实也不小,有完整的用户模块,有购物模块,订单模块,支付模块,推送模块,几天时间就全部改写完毕,并且已经在测试,目前还没发现有很大问题。
最基础的模块先搭建,比如主题颜色管理,API 模块,一些工具类,基础框架搭好之后,因为 OC 代码可以被 Swift 调用,在开始的时候,做好计划,小的模块先调用 OC,避免一下重写很多模块,导致没有动力写下去。后面几乎全是体力活,就是时间问题了。
英文版
Requirements: Xcode 9, Swift 4.0/4.1
In 2014, when Swift 1.1 was released, I read the official documents, and wrote a demo, at that time, my personal project was under development and it needed to be released as soon as possible, therefore, I didn't rewrite it in Swift. And all the companies which I worked for were using Objective-C, which resulted the lack of chance for me to write a Swift application for commercial usage. Today, I have realized Swift is becoming more and more popular, and many companies have migrated their projects to Swift, then I made a fancy decision, rewriting our project in Swift.
If you ever paid attention to me, you may know that I published an article named <How did I maintain all the function with a deletion of 60,000 lines of code>, I refactored all the three projects in a few weeks.
Today, I have rewrote one of them in Swift, the task cost me 3-4 days, and I'm sharing some experience about the migration work.
I'm going to start with Optional Type, the scheme of migration, Network layer, Data model layer, and the UI layer.
Optional Type
A new language, if it's grammar or expression is extremely different from other languages, most of programmers can adapt to it in a short time, but wtf is Optional Type... What does !
mean, and also ?
, and ??
, the first time I tried Swift, I did a lot of additional work on JSON parsing, a lot of if-else statements, a lot of !
to force unwrapping variables.
And many documentations can not explain that thoroughly, actually, Optional Type can be understood easily, if a variable was declared with Optional, it can be assigned a nil
object, for example, a response from API server, we want a list of something, sometimes there are no records in the database. Alright, some back-end programmers return an empty array to clients, it looks like this: {errorCode: 0, body: []}, some programmers return a null object to clients, looks like this: {errorCode: 0, body: null}.
The problem is, despite null
and []
all represent that there are no items, for Swift language, null
means nil
object while []
is Array Type. They are totally different, I will explain that in the Data model layer chapter.
Scheme of migration
At first, I planned to migrate it to Swift gradually, later I found that some common modules can not be called by OC and Swift simultaneously, such as Struct written in Swift, and some classes which aren't subclasses of NSObject. Finally, I decided to rewrite all the modules.
In general, we should do the migration work little by little, or we will feel frustrated. First, we can rewrite an independent page, such as an event list, during the migration, we need to design API layer and Data model layer firstly.
Network layer
A universal solution is using Alamofire, my app is not complex, so I wrapped Alamofire in an APIService singleton.
There are many ways to implement a singleton, I'll give the best practice, at least in Swift 4.0/4.1.
class APIService {
static let shared = APIService()
}
Then we can add features to it, set up the network monitor in init()
method, and some universal configuration, or for advance usages, you can write an AccessTokenAdapter
by following the official documents to deal with HTTP headers. I'll give a simple example.
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
}()
In some open-sourced project, the network layer has been designed too complex, including a URL layer, and a function for every request, looked ugly, there were many duplicated code, even duplicated hard code strings. Besides, every time you add a new API, you are required to add a new class, and implement the url getter method, and wrap a function for every request. I don't think it makes sense, for security reasons? MITM attack can capture all the network traffic, and those strings can also be revealed by analyzing the binary file in the Reverse Engineering field. It just brings useless work to programmers.
About the wrapping of request function, I did some routine action, for some simple non-params get
request, I wrote a function, to simplify the call methods:
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)
}
It looks like this when it is called:
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?
keyword can prevent the crash if value from back-end is null
instead if []
, using as!
will cause a crash because null
is not Array Type.
Assuming there is a null
object from back-end, ??
keyword can assign []
to the array
variable, to make sure the value is always an array.
Data model layer
Struct in swift has been endowed with a lot of new features, but I'm not going to write too much about that. The point is, Swift 4 has finally answered the question of how to parse JSON with Swift.
You can find more details in this article: Ultimate Guide to JSON Parsing with Swift 4
The writer has thoroughly explained the Codable feature. There are still some points that we should be aware of, if the APIs you are using are not in the standard, use some ?
to prevent crash.
Additionally, data should be processed in Data layer, computed properties can be used to deal with data. For example:
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 layer
The migration of UI layer is the easiest one, lazy load variables can be rewritten in lazy var get, the layout code of Masonry can be easily translated into SnapKit, and most of the UIKit code looks similar to Objective-C.
[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);
}];
Function name can be duplicated by using copy-paste, then replace mas_e
, To(self.
, mas_
, );
with e
, To(
, snp.
, )
respectively. I think this part is the most convenient part of the migration.
Others
During the migration, I found that Swift projects do not need main.m
any more, it is reasonable because @UIApplicationMain
has done that work for us.
And the extension feature of Swift is so powerful, we can implement some functions in a graceful way.
Despite Swift has more features than OC, Apple has not rewritten the Cocoa framework for Swift, Swift projects are still very similar to Objective-C projects.
Migration work is not as hard as we thought, my project is in medium-scale, including user module, shopping module, order module, payment module, APNS module, and they are migrated in several days, and I have arranged test efforts, there aren't many issues at present, quite stable.
In summary, build the foundation modules first, such as theme color management, API layer, and some common utils. OC code can be called by Swift smoothly, so, at first, do not wonder refactoring all classes at a time, some branch line classes migration tasks should be put at the end. When those fundamental modules are ready, the migration task will transform into physical work, the only requirement is time.