田腾飞的博客

【iOS开发】迁移Swift3.0爬坑与Swift交互OC之变化

都知道苹果要在下个版本的Xcode中移除Swift2.3的支持,强制开发者使用Swift3.0,这是一个很悲痛的现实😹。然而正好公司的项目是OC和Swift混编的项目,里面用到了一个第三方库SwiftBond,当时SwiftBond还没有升级Swift3.0,老大害怕是个坑,所以就让我使用RxSwift去替换掉这个库,然而正当我要动手的时候,突然发现我要把项目升级到Swift3.0啊,不然换了RxSwift没有卵用啊!!😂😂

让我45度角仰望星空,我的悲伤逆流成河!

没办法谁让苹果逼的紧呢,正好也能提升一下自己Swift的水平,所以就开干了,没想到在这个过程中我的悲伤却逆流成海了😭。

我发现原来项目中使用的Swift写的代码简直不能瞅,像我这种对代码洁癖的很多地方都进行了重写。并且原来的Swift代码也并没有按照Swift文件中的标准来写,导致里面坑巨多,使用Convert转换以后,每个Swift文件中几乎都是一二百个错误,我只能一个一个手动去改,而且还遇到了非常难以发现的巨坑,不过到头来我还是成功地把项目迁移到了Swift3.0,并且把SwiftBond替换为了RxSwift👻。由于这个过程中坑非常多,特此总结下来,以免大伙遇到此坑以后无从下手。

去除@objc

项目中很多地方都使用了@objcdynamic关键字修饰,例如:

1
2
@objc var clockInShare: Int = 0
@objc dynamic func setupModels() { ... }

将所有的继承了NSObject的里面的非private方法和属性前的@objcdynamic关键字去掉,因为继承了NSObject的类,Swift会默认在前面添加@objc关键字,而dynamic关键字一般使用KVO等动态特性的时候才用的到。

使用extensnion进行归类

有些文件中的类在一个大括号{}中包含了全部的属性和方法,或者还是和OC写法一样,一下继承了UITableViewDataSourceUITableViewDelegate,在里面使用了//MARK: 进行分类,但我感觉这种写法太乱了。所以将他们全部使用extension进行分类,这样子更符合Swift语言的优美风格

1
2
3
// MARK: - Actions
@objc dynamic func bi_UnselectedWord() {
}

更改为:

1
2
3
extension CCSequenceExerciseViewController {
func bi_UnselectedWord() {}
}
1
extension CCSequenceExerciseViewController: UITableViewDataSource, UITableViewDelegate { ... }

更改为:

1
2
extension CCSequenceExerciseViewController: UITableViewDataSource { ... }
extension CCSequenceExerciseViewController: UITableViewDelegate { ... }

使用extension分类的时候也有一个改变,原来类中使用的private关键字,Swift2中extension中是可以访问这个private属性,但是到了Swift3.0中private属性作用域变为了{}之间,所以extension就不能访问了。苹果又新添加了一个关键字为fileprivate表示只能在这个文件中被访问,换成这个关键字就可以了

闭包更改

原来Swift2.3中闭包的声明是这样子写的:

1
2
typealias Command = ()->()
var buttonCommand = Command?()

Swift3.0编译会提示修改,更改为如下:

1
2
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类型。

比如:

1
let cloudTime = NSDate().dateByAddingTimeInterval(NSUserDefaults.standardUserDefaults().cc_TimeDiffToServer)

更改为

1
let cloudTime = Date().addingTimeInterval(UserDefaults.standard.cc_TimeDiffToServer)

再比如:

1
2
3
4
@objc dynamic func getSavedCheckInInfo() -> NSDictionary{
.....
return CCDataDownHelper.fetchDataWithKey(key) as! NSDictionary
}

更改为

1
2
3
4
func getSavedCheckInInfo() -> Dictionary<String, AnyObject>? {
.....
return (checkInInfo as? Dictionary<String, AnyObject>)
}

不带NS前缀的类型没有某个方法

注意有时候Swift内置类型并没有包含带有NS前缀类型里面的所有方法,如果我们使用Swift类型调用这些方法,会提示没有这个方法,细心的你会发现这个方法是带有NS前缀的类型才有的方法,所以我们必须将Swift类型转换为NS前缀的类型才能调用此方法。
例如:

1
2
let userDic = ["ttf": "123"]
userDic.write(toFile: filePath, atomically: true)

这时候会报一个错误:value of type [String: String] has no member write,意思就是没有这个方法,这时候我们就需要将他转为带有NS前缀的类型了

1
2
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的时候就很蛋疼了,每个返回值都要去处理一下。而且我看到原来文件里面有这样去处理这个值的:

1
2
3
4
5
6
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值。更改为:

1
2
3
4
5
6
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的值,项目中有很多这样的地方全部让我重写了😹,想想都累。

下面这个是处理服务器端返回的值

1
2
3
4
5
6
let obj:AnyObject = response.originalContent
if !(obj is NSDictionary) {
failure(reason: "")
return;
}
success(dic: (obj as! NSDictionary))

更改为:

1
2
3
4
5
6
7
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
但是每个页面都确实没有错误啊?而且没有任何错误提示,也实在找不到任何有用的错误信息。

后来搞了好久,实在没有办法,就搞了一个笨办法,重搞项目,把所有的Swift写的模块全部移除,一个模块一个模块的添加,一个模块一个模块的迁移Swift3.0,保证每个模块编译通过以后添加下一个模块,后来添加了一个swift文件,编译又报了这个错误,我就在这个文件中一行一行的注释,最终发现了问题的所在:

1
let attributeTitle = NSAttributedString(string: "PK", attributes: [NSBaselineOffsetAttributeName : NSNumber(int: -1)])

就是因为这个NSNumber的使用导致这个Swift编译器的错误,而且页面也不报错,不知道是不是Swift编译器的bug还是其他原因,有知道的小伙伴可以留言告诉我一下。

更改为:

1
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协议是这样

1
2
3
4
@protocol NavigatorInterceptor <NSObject>
@optional
- (void)interceptOpenWithContext:(HJNavigatorInterceptorContext *)context;
@end

Swift文件继承这个协议不能去直接实现这个方法

1
2
3
extension StrangeWordBookNavigatorInterceptor: NavigatorInterceptor {
func interceptOpenWithContext(context: HJNavigatorInterceptorContext!) { }
}

在Swift2.3中这样实现是可以的,但是到了Swift3中,这样子实现就错误了。永远都不会执行这段代码。重写OC方法的时候首先要看OC方法生成的Swift方法是什么样

可以看到转换成Swift对应的文件中声明的方法是和原来的不一样的,我们应该实现Swift中对应的方法。

1
2
3
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但没有错误提示

1
2
3
4
5
6
7
8
9
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)!

更改为:

1
2
3
4
5
6
7
8
9
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 一点错误都没有,直接运行。

总结

  1. 尽量按照Swift推荐规范写代码
  2. 多使用extension进行分类
  3. 使用Swift的内置类型
  4. 避免使用NSNumber
  5. 尽量使用Swift调用OC而不是OC调用Swift
  6. 调用OC方法的时候要注意他对应的Swift版本的方法
  7. OC接口添加nullable,nonnull等关键字修饰
  8. 使用可选绑定或者guard处理optional

最后我要来一句“Swift是世界上最牛逼的语言PHP靠边站”,Swift雄霸天下🤥

小伙伴们如果感觉文章可以,可以关注博主博客

小伙伴们也可以关注博主微博,探索博主内心世界😁

如要转载请注明出处。

热评文章