遷移Swift3.0爬坑與Swift交互OC之變化

xm2614 8年前發布 | 13K 次閱讀 Swift Apple Swift開發

都知道蘋果要在下個版本的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

項目中很多地方都使用了 @objc 和 dynamic 關鍵字修飾,例如:

@objc var clockInShare: Int = 0
@objc dynamic funcsetupModels() { ... }

將所有的繼承了NSObject的里面的非private方法和屬性前的 @objc 和 dynamic 關鍵字去掉,因為繼承了NSObject的類,Swift會默認在前面添加@objc關鍵字,而dynamic關鍵字一般使用KVO等動態特性的時候才用的到。

使用extensnion進行歸類

有些文件中的類在一個大括號{}中包含了全部的屬性和方法,或者還是和OC寫法一樣,一下繼承了 UITableViewDataSource , UITableViewDelegate ,在里面使用了//MARK: 進行分類,但我感覺這種寫法太亂了。所以將他們全部使用extension進行分類,這樣子更符合Swift語言的優美風格

// MARK: - Actions 
@objc dynamic funcbi_UnselectedWord() {
}

更改為:

extensionCCSequenceExerciseViewController{
    funcbi_UnselectedWord() {}
}
extensionCCSequenceExerciseViewController:UITableViewDataSource,UITableViewDelegate{ ... }

更改為:

extensionCCSequenceExerciseViewController:UITableViewDataSource{ ... }
extensionCCSequenceExerciseViewController: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 funcgetSavedCheckInInfo() -> NSDictionary{
    .....
    return CCDataDownHelper.fetchDataWithKey(key) as! NSDictionary
}

更改為

funcgetSavedCheckInInfo() -> 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

但是每個頁面都確實沒有錯誤啊?而且沒有任何錯誤提示,也實在找不到任何有用的錯誤信息。

后來搞了好久,實在沒有辦法,就搞了一個笨辦法,重搞項目,把所有的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協議是這樣

@protocolNavigatorInterceptor<NSObject>
@optional
- (void)interceptOpenWithContext:(HJNavigatorInterceptorContext *)context;
@end

Swift文件繼承這個協議不能去直接實現這個方法

extensionStrangeWordBookNavigatorInterceptor:NavigatorInterceptor{
    funcinterceptOpenWithContext(context: HJNavigatorInterceptorContext!) { }
}

在Swift2.3中這樣實現是可以的,但是到了Swift3中,這樣子實現就錯誤了。永遠都不會執行這段代碼。重寫OC方法的時候首先要看OC方法生成的Swift方法是什么樣

可以看到轉換成Swift對應的文件中聲明的方法是和原來的不一樣的,我們應該實現Swift中對應的方法。

extensionCCStrangeWordBookNavigatorInterceptor:HJNavigatorInterceptor{
    funcinterceptOpen(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 一點錯誤都沒有,直接運行。

 

來自:http://codertian.com/2016/12/17/Swift3-0-pa-keng-ji-jin/

 

 本文由用戶 xm2614 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!