迁移Swift3.0爬坑与Swift交互OC之变化

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