2014年10月27日 星期一

關於 Swift 繼承的兩三事

談到繼承就要用先寫一下 Swift 這個語言的 class 的寫法:

class Rectangle {  // 非正確寫法
  var width:Double
  var height:Double
}

這個就是 swift class 的定義方式,名稱為 Rectangle。有兩個 property 各是 width 和 height。 但是這樣寫並不算完成,因為 Swift 要求在產生這個 class 的實體(instance) 的時候所有的 property 都要有明確的值。所以我們來加上初始值

class Rectangle {
  var width:Double = 200.0 
  var height:Double = 300.0
}

這樣簡單的 class 就準備完成,然後我們需要來產生實體如下

var rect = Rectangle()
這樣就可以用 rect 來使用實體,然後改變 property 的內容如下

var rect = Rectangle()
rect.width = 50.0
Rectangle() 做了什麼事?讀者一定很好奇。其實對熟悉 Objective-C 的開發者來說,他就是 alloc - init 這兩個動作的簡化。 在 Swift 裡 Rectangle() 就會對應到 class 裡面的 init()。比如我們改寫一下 Rectangle 成

class Rectangle {
  var width:Double
  var height:Double
  init(){
    width = 200.0
    height = 300.0
  }
}

多了一個 init() 而且 var width 和 var height 之後並沒有給值,這樣可以嗎? 這樣是可行的,因為在真正使用實體前,init() 會最先被呼叫,所以在 init() 裡面把 propety 值都設定好,就可以保證之後存取 property 都不怕沒有值。 讀者可以直接在 playground 上面試試,init() 這個第一個被執行的 method 也有一個別名叫做建構子 - constructor。 然後我們來新增一個 method 名為 area 如下

class Rectangle {
    var width:Double
    var height:Double
    init(){
        width  = 200.0
        height = 300.0
    }
    
    func area() -> Double {
        return width * height
    }
}
var rect = Rectangle()
rect.area() // 會得到 60000 
以上,就完成一個很基本的 Class 的定義。接下來才是繼承的問題 XD 所以我們來產生另一個 class 名為 Square,繼承 Rectangle 如下

class Square : Rectangle {
    
}
var s = Square()
s.height = 20.0
s.width = 20.0
s.area()
Square 裡面空空如也,只有一個 : Rectangle 的宣告,這樣一來 Square 就擁有 Rectangle 所有內容,包含 width 和 height 的 property 還有 area() 這個 method 自然也可以直接使用 這個就是繼承的好處 接下來我們來把 Square 做一些修改,比較符合我們的期待。

class Square : Rectangle { // 非正確寫法
    var side:Double
}

新增一個 side 的 variable 這樣一來要記得在 init() 指定給 side 值。 但是因為 Square 繼承 Rectangle ,已經有一個 init() 所以在 Square 的 init() 前面要加上 override (覆寫) 字樣。如下

class Square : Rectangle {
    var side:Double
    override init(){
        side = 50
    }
}
當子類別(指 Square) 要重寫一個和父類別(指 Rectangle)一樣的 method 的時候就要加上 override 的字樣,這樣執行的時候就是採用子類別的 method 程式碼而不是父類別的程式碼。 這樣一來就可以執行如下程式碼

class Square : Rectangle {
    var side:Double
    override init(){
        side = 50
        // 最後會執行 super.init()
    }
}
var s = Square()
s.area() // 得到 60000
有沒有發現一個奇怪的地方?按照推理 Square() 會去執行 Square 的 init(),裡面只有 side =50 ,s.area() 是去執行 Rectangle 裡面的 area() 程式碼,因為在 Square 並沒有 override area()。但 init() 裡面沒有對於 width 和 height 有給定值,s.area() 竟然會有值出現。由此我們可以推論 override init() 最後會去執行父類別的 init(),也就把 Square 從 Rectangle 繼承來的 width 和 height 都給了值 但是還是怪怪的,Square 的 area() 竟然不是和 side 有關而是 width 和 height ,所以我們更新 Square 如下

class Square : Rectangle {
    var side:Double
    override init(){
        side = 50
    }
    override  func area() -> Double {
        return side * side
    }
}
所以我們覆寫了 area(),讓它和 side 有關。這樣如下的程式碼在 Square 的用法上就更合理

class Square : Rectangle {
    var side:Double
    override init(){
        side = 50
    }
    
    override  func area() -> Double {
        return side * side
    }
}
var s = Square()
s.side = 30.0
s.area() // 會得到 900.0
So far, So good 看起來很不錯,但還是有一個小小的缺點。也就是當我們用 Square 的 side 的時候,也可以用 width 和 height,這兩個從 Rectangle 得到的變數,但這三個就是不同的變數,這樣一來觀念上很奇怪。比較好的方式是希望使用 Square 的 width 的時候也就是和 side 有連帶關係。拿 width 來說,如果設定 width 的值同時就是設定 side 的值,取得 width 的值也就是取得 side 的值這樣會比較合理。於是我們要來用到 Swift 語言裡很特別的功能,就是 override property。如下

class Square : Rectangle {
    var side:Double
    override init(){
        side = 50
    }
    
    override  func area() -> Double {
        return side * side
    }
    
    override var width:Double{
        get{
            return side
        }
        set{
            side = newValue
        }
    }
}
覆寫 property var width:Double 的方法如下
  
    override var width:Double{
        get{
            return side
        }
        set{
            side = newValue
        }
    }
前面也要加上一個 override 然後要用 {} 包起來,裡面有 get 和 set 關鍵字。 get 很好理解就回傳 side 的值,set 的區塊裡面要用 newValue 這個保留字代表要設定給 width 的新值,然後指定給 side 相同地 height 也照辦如下
  
override var height:Double{
        get{
            return side
        }
        set{
            side = newValue
        }
}
這樣一來不論是存取 width 或是 height 都是存取 side。 很理想,來測試一下
  
var s = Square()
s.width = 50.0
s.area() // 得到 2500.0
做個這篇的小結

  1. 簡單 class 定義方式, 建構子 init() 使用時機 
  2. 繼承的寫法,init() 在子類別和父類別的使用時機
  3. 覆寫 method 和 property 的方法 
謝謝各位,大家下次見

2014年10月7日 星期二

如何讓 Swift 與 Objective-C 共生 - 無痛轉生

Hi There,  Swift 語言推出以來締造了許多的記錄,對於 iOS Developer 來說 Objective-C 是再熟悉不過了,現在有個新的小朋友 Swift 的出現,不知道需要需要再花時間來學習,這篇文章不是來教導怎麼學習 Swift 來開發 iOS 程式,而且先從 Swift 怎麼利用已定義好的 Objective-C 的 Class 來給想學或是正在學的 Developers 一個參考。
首先要介紹的是 Bridging Header 這個檔案。
首先 產生一個 Single View Application 的專案。如下圖
這個就是新的 Xcode 6 產生專案的畫面,比之前 Xcode 5 少了幾個 (少了幾個呢?留給讀者去比較了)。
接著我們要選擇 Swift 這個語言。如下
 命名為 SwiftObjc
然後我們要在專案的 Navigator 中新增一個 Objective-C 的 Class。如下。
記得要選的是 Cocoa Touch Class 而不是 Objective-C File 哦 。
命名為 Car。
記得 Language 是 Objective-C 哦。
接著不一樣的事情發生了。Xcode 會自動跳出一個提醒視窗。如下。

這個視窗的意思是說,Xcode 想幫開發者自動加入 bridging header 的檔案。
現在就先選擇 Yes 。來看看專案有什麼變化?如下
多了三個檔案哦。我們想要的 Car 這個 Class 的 .h, .m ,還有一個所謂的 Bridging Header 。在這的名稱為 SwiftObjC-Bridging-Header.h 。所以跳出視窗問的就是這個 Header,裡面長什麼樣子呢?基本上是空白。
因為我們要在 Swift Class 裡面用 Objective-C 的 Car class 。所以必需要加上如下的。程式碼。
就是 #import "Car.h" 。
也可以自已手動加,但需要改變專案的設定,如下圖
有一個 Objective-C Bridging Header 的設定要寫,記得除了寫 .h 檔名之外,還要加上 Product 名稱哦,通常就是 Project 名稱。
接下來我們就在 ViewController.swift 裡面使用 Car 這個 Class
如下
就可以直接在 viewDidLoad 裡面用 Car 這個 Class。而且用 Swift 語法
Car()
在 Swift 中 Car() 就是和 Objective-C 的
[[Car alloc] init ]
 而且聰明的讀者有沒有發現到,在 Swift 裡面每個 row 的程式碼最後都不需要分號 ; 結尾
接著我們來加一下 method 在 Car 裡面。如下新增

Method 的名稱叫 setPrice:andYear: 這樣的 selector 要怎麼在 Swift 呼叫呢?Swift 也是支援  selector 的觀念,如下使用。
setPrice:andYear: 在 Swift 的語法就是
func setPrice( a:Int, andYear: b:Int)
 這也是 Swift 跳脫了傳統的 C function 的束縛加了一個新的語法叫 external parameter name 。也就是 andYear: ,在 Swift 的 function 裡用第二個參數的 external parameter name 來表示。
接著執行是會 Crash。為什麼?因為 Car.m 並沒有實作 -setPrice:andYear:。所以記得要實作。如下。
順便加上兩個  ivar,各是 price 和 year
這樣一來在 ViewController.swift 就可以執行成功。
對於 Selector 的觀念,我們再來做一個小小的實驗。
先在 ViewController.swift 再加上一些程式碼

我們要利用 NSTimer 來呼叫兩個  function 。分別是
func action(a:NSTimer, second b:Int ) {
        println("action1")}
這個的 selector 寫成 Selector("action:second:")

以及
func action(action a:NSTimer, second b:NSTimer){
        println("action2") }
而這樣的寫法,無法對應到 Objective-C 觀念的 Selector
執行後會發現一直出現 action1 的 log是正常的。

接著我們來看一下存取 ivar。在這個時候如果直接從 Swift 存取 Car 的 ivar 會發生如下錯誤。


放在  .m 的 ivar 應該是無法被看到,如果試著把 ivar 拿到 .h 來如下

結果也是一樣。在 Swift 要正常存取 Objective-C 的 ivar 。最好的方式就是利用 property,將 Car.h 改寫如下


再利用 Swift 來執行如下
就可以成功執行。

最後要來解釋一下 Objective-C 的 class method 要怎麼在 Swift 被使用。
我們要在 Car.h 加上 class method 如下
當然 .m 要對應好

怎麼在 Swift 中使用 +sharedInstance 呢?如下

var myCar = Car.sharedInstance()

這樣就可以了。看起來不難,不過提醒讀者名稱這事對於 Objective-C 和 Swift 都很重要,如果有一個 class method 命名成如下
+(instancetype) car; 

在使用

var myCar = Car.car()
就會造成 compiler 的錯誤,目前可以的話就盡量避免,直接用 Class 名稱第一個字小寫的 class method。
談了這麼多,到底應用在真實的例子是如何呢?
我們就拿 AFNetworking 的 framework 來測試。

這是一個非常有名也很多人用在網路的資料的傳輸上的 open source 的 framework。
在這就不簡介。先把 AFNetworking 的檔案都拉到專案
然後在 Bridging Header File 也要加上 AFNetworking.h 如下
最後補上一段,下載 Image 的程式碼就大功告成了啦
最後回答大家常問麥老師的問題,Swift 是不是非學不可?Swift 是不是會取代 Objective-C ?
Swift 是不是非學不可?
在這一二年內答案不是,但是因為 Swift 的好學好寫,我想再過不久 Swift 的 framework 不會比 Objective-C 少,到時,如果用到只有 Swift 版的 framework 就是非學不可了。
Swift 是不是會取代 Objective-C ?
我個人是覺得再 5 年可能有機會,因為一個語言從出生到非常成熟可能需要 10 的年磨練,依照熱門程度不同,時間會有所調整,Swift 這麼火紅的情況下,可能會使它比較快穩定。我們可以看一個參考,如果 Apple 某個常用應用程式是用 Swift 重新改寫,完全取代 Objective-C 的日子就很近了。

以上一些,很久沒有寫文章的碎碎唸。希望大家有所收獲。請不吝指教。Michael 筆。