都知道苹果要在下个版本的Xcode中移除Swift2.3的支持,强制开发者使用Swift3.0,这是一个很悲痛的现实��。然而正好公司的项目是OC和Swift混编的项目,里面用到了一个第三方库SwiftBond
,当时SwiftBond还没有升级Swift3.0,老大害怕是个坑,所以就让我使用RxSwift去替换掉这个库,然而正当我要动手的时候,突然发现我要把项目升级到Swift3.0啊,不然换了RxSwift没有卵用啊!!����
![](https://i-blog.csdnimg.cn/blog_migrate/999a3ddb73f5224423301b84a0563879.webp?x-image-process=image/format,png)
让我45度角仰望星空,我的悲伤逆流成河!
没办法谁让苹果逼的紧呢,正好也能提升一下自己Swift的水平,所以就开干了,没想到在这个过程中我的悲伤却逆流成海了��。
我发现原来项目中使用的Swift写的代码简直不能瞅,像我这种对代码洁癖的很多地方都进行了重写。并且原来的Swift代码也并没有按照Swift文件中的标准来写,导致里面坑巨多,使用Convert转换以后,每个Swift文件中几乎都是一二百个错误,我只能一个一个手动去改,而且还遇到了非常难以发现的巨坑,不过到头来我还是成功地把项目迁移到了Swift3.0,并且把SwiftBond替换为了RxSwift��。由于这个过程中坑非常多,特此总结下来,以免大伙遇到此坑以后无从下手。
去除@objc
项目中很多地方都使用了@objc
和dynamic
关键字修饰,例如:
@objc var clockInShare: Int = 0
@objc dynamic func setupModels() { ... }
将所有的继承了NSObject的里面的非private方法和属性前的@objc
和dynamic
关键字去掉,因为继承了NSObject的类,Swift会默认在前面添加@objc关键字,而dynamic关键字一般使用KVO等动态特性的时候才用的到。
使用extensnion进行归类
有些文件中的类在一个大括号{}中包含了全部的属性和方法,或者还是和OC写法一样,一下继承了UITableViewDataSource
,UITableViewDelegate
,在里面使用了//MARK: 进行分类,但我感觉这种写法太乱了。所以将他们全部使用extension进行分类,这样子更符合Swift语言的优美风格
// MARK: - Actions
@objc dynamic func bi_UnselectedWord() {
}
更改为:
extension CCSequenceExerciseViewController {
func bi_UnselectedWord() {}
}
extension CCSequenceExerciseViewController: UITableViewDataSource, UITableViewDelegate { ... }
更改为:
extension CCSequenceExerciseViewController: UITableViewDataSource { ... }
extension CCSequenceExerciseViewController: UITableViewDelegate { ... }
使用extension分类的时候也有一个改变,原来类中使用的private关键字,Swift2中extension中是可以访问这个private属性,但是到了Swift3.0中private属性作用域变为了{}之间,所以extension就不能访问了。苹果又新添加了一个关键字为fileprivate表示只能在这个文件中被访问,换成这个关键字就可以了
闭包更改
原来Swift2.3中闭包的声明是这样子写的:
typealias Command = ()->()
var buttonCommand = Command?()
Swift3.0编译会提示修改,更改为如下:
typealias Command = ()->()
var buttonCommand: Command?
去除Swift文件中的NS前缀的类
Swift3.0中把大量的带有NS的类型去掉了NS前缀,与OC交互的时候,Swift调用OC方法中的返回值会默认为Swift中类型,也就是说默认把类类型转换为了Swift中的值类型,比如OC方法返回NSArray那么Swift中会默认为Array,我简单测试了几个常用的返回类型,如下:
OC | Swift |
---|---|
NSArray | Array |
NSDictionary | Dictionary |
NSString | String |
id | Any |
NSDate | Date |
NSNumber | NSNumer |
NSInteger | Int |
可以看到原来OC中的id对应Swift中的AnyObject,现在更改为对应Swift中的Any类型,灵活性更高了,这个要注意。
OC中的NSNumbe仍然对应Swift中的NSNumber(使用NSNumber会有一个大坑,后面会说到)。
发现我们项目中的Swift文件中使用了很多的NSArray,NSDictionary,NSString,NSDate,这可能是历史的原因吧。因为Swift建议尽量使用Swift中内置的一些类型,并且Swift3.0已经默认转为不带NS前缀的类型了,虽然项目使用NS前缀的也能运行,但是我对代码有洁癖,把所有使用到NS的地方全部重写了,换成了不带NS前缀的Swift类型。
比如:
let cloudTime = NSDate().dateByAddingTimeInterval(NSUserDefaults.standardUserDefaults().cc_TimeDiffToServer)
更改为
let cloudTime = Date().addingTimeInterval(UserDefaults.standard.cc_TimeDiffToServer)
再比如:
@objc dynamic func getSavedCheckInInfo() -> NSDictionary{
.....
return CCDataDownHelper.fetchDataWithKey(key) as! NSDictionary
}
更改为
func getSavedCheckInInfo() -> Dictionary<String, AnyObject>? {
.....
return (checkInInfo as? Dictionary<String, AnyObject>)
}
不带NS前缀的类型没有某个方法
注意有时候Swift内置类型并没有包含带有NS前缀类型里面的所有方法,如果我们使用Swift类型调用这些方法,会提示没有这个方法,细心的你会发现这个方法是带有NS前缀的类型才有的方法,所以我们必须将Swift类型转换为NS前缀的类型才能调用此方法。
例如:
let userDic = ["ttf": "123"]
userDic.write(toFile: filePath, atomically: true)
这时候会报一个错误:value of type [String: String] has no member write
,意思就是没有这个方法,这时候我们就需要将他转为带有NS前缀的类型了
let userDic = ["ttf": "123"]
(userDic as NSDictionary).write(toFile: filePath, atomically: true)
但还是要注意在Swift文件中尽最大可能滴使用Swift的数据类型。
可选值的使用
因为Swift的出现,OC中也添加了几个关键字nullable
, nonnull
等关键字来修饰参数和返回值。OC文件中的返回值如果不包含这几个关键字,Swift调用OC的方法默认的返回值类型是一个optional类型,如果你添加了nonnull关键字来修饰,Swift中得到的值就是一个非optional的普通值。
然而我们项目中原来的OC方法的返回值都是不包含任何关键字的,所以Swift去使用OC的时候就很蛋疼了,每个返回值都要去处理一下。而且我看到原来文件里面有这样去处理这个值的:
let bgcfg = CCBgcfgService()
let copywriterMode = bgcfg.inquireDataWithType(.Copywriter, subType:.CopywriteCheckInShare)
var array = NSArray()
if copywriterMode != nil {
array = copywriterMode.valueForKey("texts") as! NSArray
}
看到这里我又默默地重写了整个Swift的文件,这里copywriterMode是OC方法返回的一个可选值,不应该使用OC里面的处理方式这个optional值。更改为:
let copyWriterMode = bgcfg.inquireData(with: .Copywriter, subType:.CopywriteCheckInShare)
var array: Array<AnyObject>? = nil
if let writeMode = copyWriterMode as? CCBackgroundCfgCopywriterModel {
array = writeMode.value(forKey: "texts") as? Array<AnyObject>
}
最好使用可选绑定,或者使用guard let
来处理optional的值,项目中有很多这样的地方全部让我重写了��,想想都累。
下面这个是处理服务器端返回的值
let obj:AnyObject = response.originalContent
if !(obj is NSDictionary) {
failure(reason: "")
return;
}
success(dic: (obj as! NSDictionary))
更改为:
guard let response = response else { return }
let obj = response.originalContent as? Dictionary<String, AnyObject>
if let obj = obj {
success(obj)
} else {
failure("")
}
注意:如果你写OC方法一定要加上nullable
, nonnull
等关键字修饰,Swift中处理optional值的时候尽量去选择使用可选绑定或者guard let
巨坑一 NSNumber
其实更改Swift3.0,我搞了两遍,第一遍手动把编译错误全部消除掉以后,发现木有错误了,我小心翼翼地按下了common+B,编译的正爽的时候,突然一个红色感叹号出来了,一个错误编译错误Command /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc failed with exit code
但是每个页面都确实没有错误啊?而且没有任何错误提示,也实在找不到任何有用的错误信息。
![](https://i-blog.csdnimg.cn/blog_migrate/b298da44831c99fe81d32bd0a2ac294a.webp?x-image-process=image/format,png)
后来搞了好久,实在没有办法,就搞了一个笨办法,重搞项目,把所有的Swift写的模块全部移除,一个模块一个模块的添加,一个模块一个模块的迁移Swift3.0,保证每个模块编译通过以后添加下一个模块,后来添加了一个swift文件,编译又报了这个错误,我就在这个文件中一行一行的注释,最终发现了问题的所在:
let attributeTitle = NSAttributedString(string: "PK", attributes: [NSBaselineOffsetAttributeName : NSNumber(int: -1)])
就是因为这个NSNumber的使用导致这个Swift编译器的错误,而且页面也不报错,不知道是不是Swift编译器的bug还是其他原因,有知道的小伙伴可以留言告诉我一下。
更改为:
let attributeTitle = NSAttributedString(string: "PK", attributes: [NSBaselineOffsetAttributeName : -1.0])
说实话这个坑实在是太难找,后来又添加一个Swift文件,又出现这个问题,我就直接搜NSNumber,果然是有,把NSNmber去掉以后,编译通过。如果有小伙伴也遇到这个错误,可以尝试搜下NSNumber更换,错误应该会解决。
巨坑二 重写OC方法
我们项目中有几个使用Swift写的Interceptor,他继承一个OC的协议,并且重写了OC的方法,每打开一个页面都是去执行每个拦截器文件中的方法,但是我把项目升级到Swift3.0以后,这几个Swift写的拦截器就再也没有执行过,对比了好多遍重写的方法确实和OC定义的一模一样啊?页面上也没有任何报错,项目也可以编译成功啊?
后来实在搞不懂了就去请教了公司的一位大神,才明白因为Swift3.0的API大变,Swift去重写OC方法的时候,其实并不是要去重写OC声明的方法,而是要去重写OC转换为Swift所声明的方法。例如一个OC协议是这样
@protocol NavigatorInterceptor <NSObject>
@optional
- (void)interceptOpenWithContext:(HJNavigatorInterceptorContext *)context;
@end
Swift文件继承这个协议不能去直接实现这个方法
extension StrangeWordBookNavigatorInterceptor: NavigatorInterceptor {
func interceptOpenWithContext(context: HJNavigatorInterceptorContext!) { }
}
在Swift2.3中这样实现是可以的,但是到了Swift3中,这样子实现就错误了。永远都不会执行这段代码。重写OC方法的时候首先要看OC方法生成的Swift方法是什么样
![](https://i-blog.csdnimg.cn/blog_migrate/6219abf17979024b0ba8b3ab92a5dbad.webp?x-image-process=image/format,png)
可以看到转换成Swift对应的文件中声明的方法是和原来的不一样的,我们应该实现Swift中对应的方法。
extension CCStrangeWordBookNavigatorInterceptor: HJNavigatorInterceptor {
func interceptOpen(with context: HJNavigatorInterceptorContext!) {}
}
这样子程序就正常运行了,每一个使用Swift所写的拦截器都会走了。
另外提醒大伙一句:从这个坑可以知道,以后我们使用Swift调用OC的方法的时候都要先去看看OC生成对应Swift版本的方法是什么样子,这样子才能保证程序的稳定,虽然我测试的Swift直接调用OC类型的方法暂时不会有啥问题,但最好还是改为Swift的。我就花了很多时候将项目中Swift调用OC的方法全部改为对应Swift的版本了。
巨坑三 介词
Swift3.0将方法中的介词都转移到了括号里面。比如:
-
UIFont.systemFontOfSize(14)
改为UIFont.systemFont(ofSize: 14)
-
writeToFile()
改为write(toFile:)
-
initWithName(name: String)
改为init(with name: String)
-
NSJSONSerialization.dataWithJSONObject(JSONArray, options:)
改为JSONSerialization.data(withJSONObject: JSONArray as Any, options:)
反正只要有介词的方法都做了改变,包括OC方法的Swift版本,完全不一样了,这就是为什么调用或者重写OC方法的时候一定要去看一下他所对应的Swift版本。
最坑的就是如果你Swift中有些地方还是原来的介词在外面的写法,但是Xcode并不给错误提示,编译也可以通过,但是你运行程序走到那个地方程序直接就crash了,真是无语,例如下面这个地方就一直crash但没有错误提示
let s = subjects.removeAtIndex(index)
if s.subjectType.rawValue == 9 {
s.options = s.options.lowercaseString
s.answerOption = s.answerOption.lowercaseString
}
self.subjects.append(s)
s.index = self.subjects.indexOf(s)!
更改为:
let s = subjects.remove(at: index)
if s.subjectType.rawValue == 9 {
s.options = s.options.lowercased()
s.answerOption = s.answerOption.lowercased()
}
self.subjects.append(s)
s.index = self.subjects.index(of: s)!
剩下的大部分更改也只是语法问题,如果你的Swift项目是按照Swift语言标准来写的,那么你Convert到Swift3.0非常轻松,几乎没有什么错误,有的话也只是一点小小的语法问题,就像我们项目中的watch版本完全纯Swift写的,一键convert swift3.0 一点错误都没有,直接运行。
总结
- 尽量按照Swift推荐规范写代码
- 多使用extension进行分类
- 使用Swift的内置类型
- 避免使用NSNumber
- 尽量使用Swift调用OC而不是OC调用Swift
- 调用OC方法的时候要注意他对应的Swift版本的方法
- OC接口添加nullable,nonnull等关键字修饰
- 使用可选绑定或者guard处理optional