天天看點

Swift3.0要注意的地方

都知道蘋果要在下個版本的Xcode中移除Swift2.3的支援,強制開發者使用Swift3.0,這是一個很悲痛的現實��。然而正好公司的項目是OC和Swift混編的項目,裡面用到了一個第三方庫

SwiftBond

,當時SwiftBond還沒有更新Swift3.0,老大害怕是個坑,是以就讓我使用RxSwift去替換掉這個庫,然而正當我要動手的時候,突然發現我要把項目更新到Swift3.0啊,不然換了RxSwift沒有卵用啊!!����

Swift3.0要注意的地方

beishang.jpeg

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

沒辦法誰讓蘋果逼的緊呢,正好也能提升一下自己Swift的水準,是以就開幹了,沒想到在這個過程中我的悲傷卻逆流成海了��。

我發現原來項目中使用的Swift寫的代碼簡直不能瞅,像我這種對代碼潔癖的很多地方都進行了重寫。并且原來的Swift代碼也并沒有按照Swift檔案中的标準來寫,導緻裡面坑巨多,使用Convert轉換以後,每個Swift檔案中幾乎都是一二百個錯誤,我隻能一個一個手動去改,而且還遇到了非常難以發現的巨坑,不過到頭來我還是成功地把項目遷移到了Swift3.0,并且把SwiftBond替換為了RxSwift��。由于這個過程中坑非常多,特此總結下來,以免大夥遇到此坑以後無從下手。

去除@objc

項目中很多地方都使用了

@objc

dynamic

關鍵字修飾,例如:

@objc var clockInShare: Int = 
@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

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

Swift3.0要注意的地方

error.png

後來搞了好久,實在沒有辦法,就搞了一個笨辦法,重搞項目,把所有的Swift寫的子產品全部移除,一個子產品一個子產品的添加,一個子產品一個子產品的遷移Swift3.0,保證每個子產品編譯通過以後添加下一個子產品,後來添加了一個swift檔案,編譯又報了這個錯誤,我就在這個檔案中一行一行的注釋,最終發現了問題的所在:

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

就是因為這個NSNumber的使用導緻這個Swift編譯器的錯誤,而且頁面也不報錯,不知道是不是Swift編譯器的bug還是其他原因,有知道的小夥伴可以留言告訴我一下。

更改為:

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

說實話這個坑實在是太難找,後來又添加一個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方法是什麼樣

Swift3.0要注意的地方

swift.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 ==  {
    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 ==  {
    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