2016年2月24日 星期三

Swift Vapor Framework on Heroku

大家早,之前文章得到大家的喜愛,雖然沒有超過 300 人回覆,但有超過 300 人看過,筆者為了感謝大家的支持,想介紹如何把 Vapor 的專案放到 Heroku 這個有名的平台上面。讀者應該也是面臨後 Parse 時代,正在尋找替代方案,筆者個人覺得 Heroku 是個不錯的選擇,也撐超過 5 年呢,應該還蠻可靠的。
首先就是先註冊 Heroku 帳號,登入之後,有個 heroku 的工具程式要下載,在這邊下載。下載安裝完在 Terminal 執行 heroku login 就可以登入使用 heroku 的資源,大部分的設定都可以在本機,用 Heroku 的工具程式就可以完成,所以這個程式很重要。
還記得上一篇的 Vapor 專案嗎?如下
swiftServer/
├── Package.swift
├── Resources
│   └── index.html
└── Sources
    └── main.swift

因爲我們要部署到 Heroku 所以需要一個 Procfile
新增如下

swiftServer/
├── Procfile
├── Package.swift
├── Resources
│   └── index.html
└── Sources
    └── main.swift

Procfile 內容如下

web: SwiftServer --port=$PORT

簡單的一行,存檔就行了
接著我們要把這個專案部署到 Heroku
詳細的內容可以參考 Heroku 的教學

接著我們先把 Vapor 專案用 Git 來管理
在 Terminal 輸入
git init
得到
Initialized empty Git repository in /Users/michael/Documents/project/heroku/<某資料夾>/.git/

再輸入
git add .
目的是加入檔案到 git repo
最後要 commit 輸入
git commit -m "first on heroku"
得到

6 files changed, 42 insertions(+)
 create mode 100644 .DS_Store
 create mode 100644 Package.swift
 create mode 160000 Packages/vapor-0.2.3
 create mode 100644 Procfile
 create mode 100644 Resources/index.html
 create mode 100644 Sources/main.swift



然後輸入 heroku create,藉以產生一個 heroku的專案,會看到如下結果。
https://stark-journey-93380.herokuapp.com/ | https://git.heroku.com/stark-journey-93380.git其中  stark-journey-93380 是 heroku 隨機產生的專案名稱,之後還可以改。
回到 Heroku 網頁看一下,會看到如下圖片


有看到一個同樣名稱的 Heroku App在上面。
然後為了可以讓 Heroku 認得是 Swift 的專案
要利用這個 heroku 的 buildpack
在 Terminal 輸入
heroku buildpacks:set https://github.com/kylef/heroku-buildpack-swift.git --app <專案名稱> 
最後的專案名稱就是 heroku 一開始產生給我們的,在這個例子是 stark-journey-93380
得到如下回覆

Buildpack set. Next release on stark-journey-93380 will use https://github.com/kylef/heroku-buildpack-swift.git.
Run git push heroku master to create a new release using this buildpack.

因為 Heroku 有提供 git 機制,所以我們要把這個 local 的 git 和 remote heroku 的 git 結合
利用 git remote -v 來看一下遠端有什麼可以結合的 git repositoty
會看到如下
heroku    https://git.heroku.com/stark-journey-93380.git (fetch)
heroku    https://git.heroku.com/stark-journey-93380.git (push)

這個 local 的 Vapor 專案就是要和 stark-journey-93380結合
輸入
heroku git:remote -a stark-journey-93380
會看到
set git remote heroku to https://git.heroku.com/stark-journey-93380.git
代表結合成功了
最後一個步驟就是把程式碼 push 到 Heroku 上面,輸入
git push heroku master
會得到如下回覆
Counting objects: 10, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (10/10), 1.22 KiB | 0 bytes/s, done.
Total 10 (delta 0), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Fetching set buildpack https://github.com/kylef/heroku-buildpack-swift.git... done
remote: -----> Swift app detected
remote: Cloning into 'swiftenv'...
remote: -----> Installing DEVELOPMENT-SNAPSHOT-2016-02-08-a
remote: Downloading https://swift.org/builds/development/ubuntu1404/swift-DEVELOPMENT-SNAPSHOT-2016-02-08-a/swift-DEVELOPMENT-SNAPSHOT-2016-02-08-a-ubuntu14.04.tar.gz
remote: DEVELOPMENT-SNAPSHOT-2016-02-08-a has been installed.
remote: -----> Installing clang-3.7.0
remote: -----> Building Package
remote: Cloning https://github.com/tannernelson/vapor.git
remote: Using version 0.2.4 of package vapor
remote: Compiling Swift Module 'Vapor' (33 sources)
remote: Linking Library:  .build/release/Vapor.a
remote: Compiling Swift Module 'SwiftServer' (1 sources)
remote: Linking Executable:  .build/release/SwiftServer
remote: -----> Copying dynamic libraries
remote: -----> Copying binaries to 'bin'
remote:
remote: -----> Discovering process types
remote:        Procfile declares types -> web
remote:
remote: -----> Compressing...
remote:        Done: 7.8M
remote: -----> Launching...
remote:        Released v3
remote:        https://stark-journey-93380.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/stark-journey-93380.git
 * [new branch]      master -> master

有點長
因為我們有寫 Heroku buildpack,所以除了把 code 放到 heroku 之外,heroku 還會利用 Swift Package Manager 去下載 compiler 再來 compiler 我們的 Vapor 專案
在 Terminal 可以輸入
heroku open
就可以用預設 Browser 打開 Heroku App 會看到


為什麼事 Page not found 是因為我們沒有定義 / 要做什麼事
所以我們更改路徑爲
https://stark-journey-93380.herokuapp.com/welcome
就會看到

再測試 https://stark-journey-93380.herokuapp.com/view
就會看到

如此,部署完成。
之後如果程式有什麼修改
就用
1. git commit -a
2. git push heroku master

這兩個指令就可以了
如果要更改 Heroku app 名稱要用
heroku apps:rename <新的名稱>
不過要注意名稱不能被其他 Heroku 使用者使用,會更改不成功,多注意回覆就好了。


2016年2月23日 星期二

用 Swift 開發 Server Side App 並利用 Swift Package Manager - Vapor Framework

首先和各位看官說聲 2016 新年快樂。希望大家在新的一年立訂新的目標。這篇文章要如何用 Vapor 這個 Framework 來開發 Server Side App。Vapor 使用 Swift 來架構而成,個人覺得使用起來比起 Swifton 或是 Kitura 還有 Perfect 來的簡潔。有興趣的可以去多多比較。
而 Vapor 剛好利用 Swift Package Manager 的方式來管理套件,有別於比較常見的 CocoaPods 和 Carthage 來管理,Vapor 使用 Apple 提出的 Swift Package Manager 方式來管理。
要提醒讀者,Vapor 更新還蠻頻繁的所以 Google 到的寫法可能是舊的,請看好 Vapor 的版本,這篇用的是 0.2.3 。筆者會持續更新。

安裝 Swift 3.0 Dev


準備工具:Swift 3.0 Dev 從這邊下載,目前 Swift 2.2 Release 還沒支援 Swift Package Manager 所以要下載 Latest Development Snapshot 。
作業環境: OS X El Capitan 10.11.2
下載完會看到一個 swift-DEVELOPMENT-SNAPSHOT-2016-02-08-a-osx.pkg
點兩下安裝。如果沒有看到如下安裝畫面
 
這是安全的考量,Mac 認為這個程式不是認證的開發者???
要手動打開,系統偏好設定->安全性與隱私
安裝好之後會在
 /Library/Developer/Toolchains/
看到這幾個資料夾
swift-DEVELOPMENT-SNAPSHOT-2016-02-08-a.xctoolchain/
swift-latest.xctoolchain/
之後就用 swift-latest.xctoolchain/ 就可以了
試一下 Swift 版本
打開 Terminal 輸入以下指令
 /Library/Developer/Toolchains/swift-latest.xctoolchain/usr/bin/swift --version
看結果是不是以下
Apple Swift version 3.0-dev (LLVM a7663bb722, Clang 4ca3c7fa28, Swift 1c2f40e246)
Target: x86_64-apple-macosx10.9
如果是 3.0-dev 就沒有問題了。
為了省輸入時間可以在.profile 檔加上
export PATH=/Library/Developer/Toolchains/swift-latest.xctoolchain/usr/bin:$PATH
開啟 Terminal 就執行  .profile 這樣就直接用 swift 就好了。

建立 Vapor 專案

建立一個資料夾名為 swiftServer,其架構如下
swiftServer/
├── Package.swift
├── Resources
│   └── index.html
└── Sources
    └── main.swift

Package 內容如下
 
import PackageDescription

let package = Package(
name: "SwiftServer",
dependencies: [
.Package(url: "https://github.com/tannernelson/vapor.git", majorVersion: 0)
 ]
)
稍微解釋一下,Package.swift 的語法也是 Swift 一開始要 import PackageDescription 接著就可以用 Package 這個 class 有興趣的讀者可以在這看到 Package 的全貌。majorVersion 爲 0 因為現在 Vapor 的版本是 0.2.1 majorVersion 爲 0 依照 source code 會找 0..<1 的版本

main.swift 內容如下
 
import Vapor

let app = Application()
app.get("welcome") { request in
 return "Hello Vapor"
}
app.start(port:8080) 
Vapor 裡面有 Application 這個 class 也就 Server 一直執行的 Daemon 。然後綁定在 port 8080,
接著我們利用 app.get 來定義 HTTP GET Method 的 Path。目在這個例子新增 /welcome 這個 Path。
是時候在本機來執行這個 Vapor server 。
在 Package.swift 同一個目錄下,在 Terminal 輸入以下指令。
swift build

成功之後會看到

Cloning https://github.com/tannernelson/vapor.git
Using version 0.2.3 of package vapor
Compiling Swift Module 'Vapor' (32 sources)
/Users/michael/Documents/project/swiftServer/Packages/vapor-0.2.3/Sources/Application+Route.swift:108:13: warning: variable 'handler' was never mutated; consider changing to 'let' constant
        var handler = { request in
        ~~~ ^
        let
Linking Library:  .build/debug/Vapor.a
Compiling Swift Module 'SwiftServer' (1 sources)
Linking Executable:  .build/debug/SwiftServer

看到目前的 Vapor 版本為 0.2.3
然後這理有個 Warning 是 Swift compiler 常見的 Warning ,建議把 var 改成 let 。雖然我們有 source code 但是還是不建議自己改,就給 Vapor 維護者在一個版本改。最後一行是 Linking Executable:  .build/debug/SwiftServer 就是代表 compiler 完的執行檔。
只要在 Terminal 直接執行
.build/debug/SwiftServer
會看到
Server has started on port 8080

然後打開本機的 Browser 在 URL 的地方輸入
http://localhost:8080/welcome

就會看到如下



幾件事提醒

1. 如果有修改 code 要重新利用 swift build 來 compile 之前,請先用 rm .build/debug/SwiftServer 把舊的執行檔刪除。
2. 程式碼的 app.start(port:8080) 請務必是最後一行。
3. 也可以輸出 JSON 比如直接回傳 [ "name":"Michael"],留給讀者自行嘗試。

HTML 結合

輸出的結果有時候需要是 html ,Vapor 也可以輸出已經編寫好的 html 檔案,但必須放在 Resources 這個資料裡面,在看一次專案結構圖。
swiftServer/
├── Package.swift
├── Resources
│   └── index.html
└── Sources
    └── main.swift

在這 index.html 的內容為

 <html>
    <body>
        <h1>Vapor Server</h1>
This is tutorial for building Vapor Server.
    </body>
</html>
輸入好之後在 main.swift 新增一個 Path
修改完如下
 
import Vapor

let app = Application()
app.get("welcome") { request in
 return "Hello Vapor"
}
app.get("view") { request in
    var view:View? = nil
    do {
        try view = View(path: "index.html")
    }catch {
        print("can not find html : error")
    }
    if let okView = view {
        return okView
    }
    return "Error"
}
app.start(port:8080) 
我們新增了 /view 這個 path。 要注意的是 View 這個 class 的 init?(path:) 會丟出 error,所以要用 do-try-catch 來承接。
最後把 unwrapped 的 okView 回傳就好了。
先把 Server 停掉,ctrl+C
記得
1. rm  .build/debug/SwiftServer
2. swift build

再重新執行
.build/debug/SwiftServer

打開 Browser 輸入,http://localhost:8080/view
會看到


如果這篇很多人回應我們再來分享一篇,如何結合 Database。很多人是多少呢?超過 300 人吧?試試人氣。


2015年10月12日 星期一

螢幕的轉向

螢幕的轉向

要設定app支援的轉向,有兩個要素。分別是”裝置”與”View Controller”。兩者設定的交集,才是真正最後能呈現的轉向。

裝置支援的方向
要設定裝置所支援的方向,只要在專案Device Orientation勾選要支援的方向。 



其所修改後,其實是對應到Info.plist中,Supported interface orientations的設定。 




View Controller 支援的方向
View Controller以呈現類型,大致上有三種。分別是UINavigationController, UITabBarController與的UIViewController。

  • UINavigationController

若以UINavigationController管理多個UIViewController,則以UINavigationController統一管理支援的方向。
  • UITabBarController

若以UITabBarController管理多個UIViewController,則以UITabBarController統一管理支援的方向。
  • UIViewController

單獨的UIViewController則自己處理支援的方向。若前面UINavigationController或UITabBarController以presentViewController方式呈獻UIViewController,則此UIViewController不受UINavigationController或UITabBarController所管理。

從iOS 7到目前的iOS9有兩個method要去實作,分別表示
  • shouldAutorotate() -> Bool :表示是否支援轉向
  • supportedInterfaceOrientations() -> UIInterfaceOrientationMask :列出所支援的轉向


說明
若裝置支援的方向勾選Portrait, Landscape Left, Landscape Right。用如下支援轉向的method寫在不同地方做說明。

    override func shouldAutorotate() -> Bool {
        return true
    }

    override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
        return [.Portrait]
    }

  • 使用UINavigationController,但支援轉向的method寫在其中的一個UIViewController中。進行轉向的結果如下。可以看出已被轉向。表示method限制只支援Portrait的設定無作用。


  • 使用UINavigationController,但新增繼承UINavigationController的Class,將支援轉向的method寫在裡面。並使用這個新增的class管理Navigation。可以看得出來,轉向被限制在Portrait有起作用。


有興趣可以動手對UITabBarController做一樣的嘗試。可以發現,原來UINavigationController與UITabBarController除了管理UIViewController之外,還負責轉向的支援。















2015年9月17日 星期四

從 Swift 1.x 到 Swift 2.0

從 Swift 1.x 到 Swift 2.0 轉換時候,有什麼要注意的呢?

一般 Apple 的做法是新的 IDE 才會支援新的語法或是平台,所以要測試就拿新的 Xcode 7 來測試一下
不過在測試之前要先來討論一下,用 Xcode 6 開發 和用 Xcode 7 開發差別在哪?
對於開發者而言,最重要的事情就是 Xcode 可以支援的平台 OS 是幾版到幾版?
以 Apple 的做法來說,非常舊版本的 Xcode。以這次 iOS 9 的更新來說我用 Xcode 6 就無法把 App 部署到 iOS 9 的 Device 上面,所以來比較一下可以部署的平台
用 Xcode 7 打開一個專案如下可以看到

圖片無法 Copy 到下方的版本,Xcode 7 可以支援的就是
iOS 6.0 - 9.1
另外再來看一下 Xcode 6.4
同樣的地方
會看到是
iOS 6.0 - 8.4
換句話說,如果要寫 App 支援 iOS 6.0 之前的版本, Xcode 6.4也不能部署
要開發 iOS 9 請用 Xcode 7




接著回到正題,拿一個現成的 Project 來轉看看 這個 App 操作方式如下
 https://www.youtube.com/watch?v=gIyhpRXP8so
先用 Xcode 7 打開

 就會看到轉換成最新 Swift  語言的提示
接著按下一步
接著選擇要檢視的 Target
PS. 如果沒有自動出現的話可以手動啟動這個 Convert 機制如下圖

接著就會出現比較的畫面,我們一一來看一下
第一個我們看到的不一樣
Swift 2.0 之後,都用  print() 來取代之前的 print() 和 println()
差別在 2.0 的 
print() 
代表 1.x 的 
println()
而 1.0 的 
print() 
在 2.0 要寫成 
print("Hello", terminator: "")
或者你也可以想成 2.0 的 
print() 
其實是
 print("Hello", terminator: "\n") 
的簡寫

接著看下一個
 這點是如果 var some = "Hello",對於 some 這個變數,如果之後的程式碼沒有用到,Xcode 就會強烈建議把 var 改成 let ,也就是會自動把 

var some = "Hello"

轉成

let some = "Hello"

 接著看下一個
這個原因是
在 1.x 的時候,有一個 
var name:String = "Michael"
 name 的型別是 String
如果我們想要用 for-in 走訪這個 string 把大寫的字找出來
在 1.x 會這樣寫

for c:Character in name {
    var str = "\(c)"
    if str == str.uppercaseString {
        print("\(str) is uppercase ")
    }
}

直接把 name 好像當成集合用,
而在 2.0 會改成
for c:Character in name.characters {
    var str = "\(c)"
    if str == str.uppercaseString {
        print("\(str) is uppercase ")
    }
}
這樣比較合理一點,用 characters 來回傳一個由 Character 所組成的 Array 用 for-in來 iterate 所有的元素


有學員提供這個問題
在 1.x 的時候這樣寫

var urlStr = "http://www.apple.com"

var iphoneURL = urlStr.stringByAppendingPathComponent("iphone")

stringByAppendingPathComponent 本來是 NSString 常用的 method
Swift 的 String 也是可以用
但是這樣 urlStr 裡面是 http:// 開頭的,也就是意圖要處理一個 URL
此時用 Xcode 7 打開,Convert 到新的 Syntax 就會出現下圖

也就是目前 Xcode 7 建議先轉成 NSString 再做 stringByAppendingPathComponent
這樣一來可以正常的執行
如果不聽,在 Xcode 7 就會直接給錯誤如下圖

 它就直接出現錯誤,stringByAppendingPathComponent 不可以在 String 用了,建議用 NSURL 來處理 path component 的問題



String 的 toInt() 被拿掉了如下圖
 在 2.0 如果要把數字集合而成的 String 轉成 Int 要就要用 Int 的 constructor
Int("12345")


/* by MusiCabbage */
好康道相報!!  SequenceType 本來一些奇怪的方法,變得更直覺了!!

例如說,本來我們想知道Array中有沒有某個Object,
必需這樣寫…

var array = ["a", "b", "c", "d", "e"]

contains(array, "a")

在 swift 2.0 的世界裡,我們只需要這樣寫…

array.contains("a")

類似的做法,CollectionType 裡面那些奇怪的方法,也一起跟進了!!
以前如果想要找到某個Object在Array中的Index,
是這樣寫…

var index = find(array, "c")

在 swift 2.0,完全換了個方法,但更容易懂了…

array.indexOf("c")





持續更新中

2014年12月7日 星期日

Swift - Struct 與 Class 的差異性

Hi, There。今天這個篇文章要來介紹 Swift 中 struct 和 class 有什麼不一樣的地方?首先要先和大家提到一個觀念,Value Type 和 Reference Type 其中 struct 是 Value Type 而 class 是 Reference Type 所以這篇文章呈現的 struct 的行為也可以套用到所有的 value type 物件,相同地 class 的行為也可以套用到 reference type 的物件上。

我們來建立一個自已的 struct 名稱為 SRectangle 。程式碼如下呈現。

struct SRectangle {
 var width = 200
}
這個 struct 有一個 property 名為 width 。
再來建立一個 class 名為 CRectangle 也有一個叫做 width 的 property 程式碼如下

class CRectangle {
 var width = 200
}
準備好了,我們先來看第一個差異點

1. 建構物件時

struct


var sRect = SRectangle()
// 或者 

sRect = SRectangle(width:300)

sRect.width // 結果就是 300

class


var cRet = CRectangle()
// class 不能直接用 CRectangle(width:300) 必需要定義一個 constructor
cRect.width // 為 200

主要的差別就是 class 在產生物件時不能很自然把 property 放在 constructor 的參數裡

2. 指定給另一個變數的行為不同

struct


var sRect = SRectangle()
// 或者 

var sRect2 = sRect

sRect2.width // 目前值是 200,因為 sRect 直接 copy 一份完整記憶體給 sRect2
sRect2.width = 500
sRect.width // sRect.width 值不受 sRect2 影響還是 200

class


var cRect = CRectangle()
// 或者 

var cRect2 = cRect

cRect2.width // 目前值是 200,因為 sRect 直接 copy 一份完整記憶體給 sRect2
cRect2.width = 500
cRect.width // cRect.width 也改變成了 500

以上是 value type 和 reference type 最大的不同,在每次做 assignment 的時候,value type 都會複製一份完整相同的內容給另一個變數,而 class 則是把記憶體的位置給變數。所以 reference type 大多會保有一份記憶體而重覆利用。

3. 關於 immutable 變數

在 Swift 這個語言的特色之一就是可變動內容的變數和不可變動內容的變數用 var 和 let 來區別。如果一開始用 let 來修飾變數就會造成 compiler 的錯誤。如下

var name = "Michael"
name.write(" Pan") // 這樣是正常

let name = "Michael"
name.write(" Pan") // 會造成 compile 錯誤

struct

大至也遵循這個規則如下

let sRect = SRectangle()

sRect.width = 500 // 會造成錯誤

class


let cRect = CRectangle()

cRect.width = 500 // 預設可以直接更改雖然是 let 修飾的 cRect

let 這個效果無法延申至 class 。不過要提醒大家在 Swift 常用的 String, Array, Dictionary 都是 struct 所以 let 是會有效果的

4. mutating function

我們先要為 struct 和 class 新增一個 method 名為 changeWidth() 而且我們在不改變原始的程式碼為前提下,新增一個 method。如下

struct


struct SRectangle {
    var width = 200
    
}

extension SRectangle {
    mutating func  changeWidth(width:Int){
        self.width = width
    }
}

class


class CRectangle {
    var width = 0
}

extension CRectangle {
    func changeWidth(width:Int){
        self.width = width
    }
}

這裡用了 extension 新增 class 裡的 method 而不需要改到原本的程式碼。 以。struct 和 class 的差別是 struct 的 function 要去改變 property 的值的時候需要加上 mutating 字樣,而 class 不用這樣做

5. Struct 和 Class 裡的 function 共同特色 - Implicit External Parameter Name

function 在定義時,有一種特別的寫法叫做 external parameter name 舉例來說 一般寫法

func setSize( width:Int, height:Int) {
    println("width \(width), height \(height)")
}

setSize(50, 100) // 直接丟入參數
External Parameter Name

func setSize(width width:Int, height height:Int) {
    println("width \(width), height \(height)")
}

setSize(width:50, height:100) // 必需在參數值前加上 external parameter name

另一個簡寫

func setSize(#width:Int, #height:Int) {
    println("width \(width), height \(height)")
}

setSize(width:50, height:100) // 當 External name 和 internal name 一樣的時候可以在 internal name 前面加上 # 就可以少寫 external name

這樣的行為對於一般 function 並沒有強制一定要寫 external name 不過在 struct 和 class 裡是強制使用的。如下

struct


struct SRectangle {
    var width = 200
    
}

extension SRectangle {
    mutating func  changeWidth(width:Int){
        self.width = width
    }
    func setName(name:String, width:Int) {
        println("name \(name), width is \(width)")
    }
}
var sRect = SRectangle()
sRect.setName("Michael", width: 500)


class


class CRectangle {
    var width = 0
}

extension CRectangle {
    func changeWidth(width:Int){
        self.width = width
    }
    func setName(name:String, width:Int) {
        println("name \(name), width is \(width)")
    }
}

var cRect = CRectangle()
cRect.setName("Michael", width: 500)

無論 struct 或是 class 裡的 function 第二個參數開始都強制加入 external parameter name。在定義的時候不用特別寫,就會有了。

6. 繼承

物件導向語言,令人覺得強大的能力莫過於繼承,而 struct 沒有 繼承的功能,只有 class 有。如下

class


class CRectangle {
    var width = 0
}

extension CRectangle {
    func changeWidth(width:Int){
        self.width = width
    }
}
class CSquare : CRectangle {  // :CRectangle 代表繼承的語法
    
    // CSquare 這個 class 看起來什麼內容都沒有
}

var cSquare = CSquare()
cSquare.changeWidth(300)  // 可以直接使用父類別的 function
cSquare.width  // 也可以直接使用從父類別繼承下來的 property


以上是簡單常用到的行為裡 struct 和 class 常令使用者搞混的情況,希望對大家有幫助。 歡迎提供更多建議。

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 筆。