顯示具有 Mobile 標籤的文章。 顯示所有文章
顯示具有 Mobile 標籤的文章。 顯示所有文章

2015年3月6日 星期五

Swift Auto Layout

    目前蘋果最新的機型是4.7吋的iPhone 6以及5.5吋的iPhone 6 Plus,如果再加上舊的機型如3.5吋的iPhone 4(含更舊的3, 3GS等)、4吋的iPhone 5,再加上原本9.4吋的iPad系列機型,隨著蘋果逐漸導入更大螢幕的手機,似乎長期以來一直困擾著Android開發者的螢幕尺寸問題也開始漫延到iOS,成為開發者的夢魘之一。
    不過如果我們攤開iPhone歷屆的機型來比較,會發現從型號5開始是一個分水嶺,iPhone 4S(含)之前的舊機型螢幕尺寸都是3:2,而從iPhone 5開始則都是接近16:9的比例,因此對於iPhone來說,雖然螢幕愈來愈大,但其實只有兩種尺寸大小:舊型的3:2與新型的16:9。
  至於iPad,則無論是從最舊的第一代iPad到最新的iPad Air 或iPad Mini Retina,它們的螢幕尺寸比例皆維持在4:3,因此,對於iOS開發者來說,要開發同時適用於iPhone與iPad的APP,要應付的螢幕只有三種:3:216:9、4:3,這比起Android開發者來說輕鬆了不少。
iPhone Aspect Ratios

點(Points)與像素(Pixels)

    App開發者在設計與溝通時應該要用的是「點」概念,而非「像素」,每個點單獨對映到一個像素(非Retina)或二個像素(Retina)視設備的解析度而定,例如,對於320點x480點這個2:3的螢幕尺寸而言,它適用於前述的4S(含)以下的機型,320點x568點(接近9:16)則適用於5(含)更新的機型,而我們在編排畫面物件時,只要考量「點」的比例即可,像素的轉換則由iOS替我們處理,若為Retina螢幕自動將點以及圖片顯示長寬乘上2。
所以,在設計iOS APP時,我們只要針對兩種規格比例,並且加上「點」的概念去設計畫面佈局,就可以讓程式同時適用於iPhone與iPad(這類的App稱為Universal),而不同尺寸圖片無法無中生有,所以在開發App時我們還是必須分別提供不同的解析度圖片(例如在檔名後方加上「@2X」代表for Retina使用),Android也是採用類似的方法,但是更為複雜,因為它有六種不同的解析度定義:dpi、mdpi、hdpi、xhdpi、xxhdpi以及xxxhdpi,開發者若想支援全部的解析度,就必須將不同大小的圖片放置於該六個資料夾下。

關於Auto Layout

    為了因應未來尺寸的多樣化(可能是考慮到Apple WatchTV、HomeKit…?),從Xcode 6開始有了Auto Layout這個功能,它是一種以約束條件(Constraints)為基礎的佈局系統,目的是讓開發者能夠方便快速的建立適用於不同尺寸、橫向或直向等螢幕的APP,就像目前網站所流行的Responsive Design一樣,能夠自動適應不同尺寸的裝置。
  那什麼是以約束條件為基礎的佈局」呢?就是我們給予佈局中的元件設下各種條件與限制而非固定的像素值,這樣可使得它們依照著這些限制呈現出理想的相對排列,而非絕對的固定顯示於某一處,因此,舉例來說,如果我們要將一個按鈕擺在螢幕正中央,那麼它的兩個約束條件就是:水平置中與垂直置中。
    對於一個元件來說,它會有15種約束條件,即三種尺寸 x 五種位置:

    三種元件的尺寸

以約束條件方式來思考一個元件在螢幕上的尺寸,它會有三種情況:
  • 固定尺寸的長和寬
  • 固定尺寸的長和彈性縮放的寬
  • 彈性縮放的長和固定尺寸的寬
  • 彈性縮放的長和彈性縮放的寬(不列入)
最後的彈性縮放的長和彈性縮放的寬」不列入的原因是這樣的尺寸比較特殊,它代表了無論放置在任何母元件中,它都會自動會伸展到跟母元件一樣大,因此它只適用於Scrollview元件(一種滾動不定長度的view,可用來收納其它元件)。

五種元件的位置

  • 在母元件的正中心
  • 靠上方排列
  • 靠下方排列
  • 靠左側排列
  • 靠右側排列
想像一下,當下方的藍色元件的限制是:「彈性縮放的長和固定尺寸的寬」加上「靠右側排列」,那麼它會排列如下,無論螢幕尺寸大小或直擺橫放,佈局結果皆相同:
  







在Swift中使用Auto Layout

當我們在Xcode中開啟了一個新專案,預設便已經把Auto Layout和Size Classes選項打開了,因此你會注意到StoryBoard 的編輯畫面尺寸接近正方形,與以往接近iPhone尺寸的長方形不同,代表開發出的APP將適用於所有的Apple裝置。

尺寸類別(Size Classes)是iOS8新導入的類別,它可以讓開發者使用統一的Storyboard來開發不同的APP UI,如果我們改為不選取而disable它,那麼,Xcode就會要求我們選擇一種裝置型號(iPhone或iPad),並將Storyboard變更為其大小。



在右下方如紅圈所示,就是Auto Layout選單,所有的layout設置都收納在這選單中。
  • Align:建立對齊的約束條件,像是對齊兩個元件的左側
  • Pin:建立間隔的約束條件,像是訂出UI控制的寬度
  • Issues:解決佈局的問題。(Xcode發現有問題的佈局時,會自動提示)
  • Resizing:指出尺寸如何影響約束條件。

不錯的Auto Layout教學

  在第一篇作者首先演示了傳統不使用Auto Layout會造成的問題,造成必須額外加入指令來修正,之後再透過Auto Layout功能實作則能夠完美的排列。
    作者提到這種以約束條件進行的佈局稱為「Designing by intent」(依意圖來設計),意指考慮的不是物件的實際位置,而是以物件相對於其它物件的位置的限制條件,例如以往我們會如此設計:在左側的(20, 230)的位置,若改以限制條件的設計,就應改為:在母元件中垂直置中,距離母元件左側邊緣20點的距離。
    另一個Auto Layout的好處是,它對於App的國際化設計有幫助,舉例來說,不同的語言詮釋一樣的文字時會有不同的長度,以「加入」這個詞彙來說,用中文與英文(Join)可能只需要15點寬度的Label就夠了,但改為德文(beitreten)就需要兩倍30點的寬度,此時,Auto Layout就發揮作用了,它會在不影響佈局的情況下自動擴展排列。

2015年2月12日 星期四

Swift學習心得 – Optional與Unwrap
    「?」和「!」這兩個符號經常會在Swift程式中看到,它們算是Swift程式中比較特別的部份,我在本文中特別提出來,因為Optional型別在Swift中扮演了相當重要的角色,若不瞭解這兩個特殊符號的用法,便無法深入瞭解Swift。
    在C語言,我們經常指定一個變數但沒有先給值,就像這樣:
   
        如果我們一直都沒有給予它任何值,那麼在程式中使用到number這個變數時,我們便會得到undefined的錯誤訊息,此時,我們事後透過偵錯作業來更正這個錯誤。
    而Swift則嚴格要求所有變數和常數都要先給值,不能為空值或unknow的值,但有時候,我們還是會有需要有空值的情況,例如,搜尋時傳回空的結果代表沒有找到任何資訊,因此,Swift為了解決這種狀況,建立了一種稱為optional」的資料型別,凡是特別被指定為optional資料型別,代表它容許有值或沒有值,其它非optional型別的變數(預設狀況),則要求一定要有值。

沒有指定Optional會出現什麼問題?




    舉例來說,上面程式的第一行沒有問題,但是到了第二行便會出現錯誤,為什麼?我們不是已經指定了空值(nil)給message變數了嗎?
    這是因為若沒有指定的話,Swift變數預設都是non-optional而非optional型別,因此nil空值是不被接受的,我想Swift的考量點,主要是空值在程式運算中經常會造成問題,而Apple在創建Swift語言時又強調安全為首要考量,它不希望在Run time執行時期發生任何的錯誤,因此才會要求我們要預先告知那些可能為空值的變數,以避免後續可能造成的問題和debug,所以在上面範例中,我們的message必須被指定為optional才能將nil賦予給它。
所以同樣的,下方的範例也會在第二行出現compile錯誤,因為只有宣告而沒有指定值給message2,這點對於用Object-C開發APP的人而言可能會相當不習慣,因為Object-C的變數宣告時不需要給予啟始值。


指定optional型別

    因此,要避免上述的錯誤,我們必須替那些可能沒有值的變數或參數加上「?」,表示它是optional,這樣Swift在compile時便可過關了,不會發生錯誤:




    為什麼Swift要這麼特別的要求呢?這是有理由的,我們看看下方的Object-C例子:

    這個findStockCode函式會傳回Apple或Google的美股代號,如果不是這兩個代號其中之一,那麼就會傳回nil空值。現在,我們在下方的程式碼中使用這個函式,執行結果會在紅色那行中發生runtime exception,因為傳入的是”Facebook”而不是Apple或Google,所以會讓函式傳回nil空值。
   

    如果這個函式用Swift來寫,程式碼要修改如下:

    請注意第一行,我們必須替傳回值型別String加入「?」符號,代表它為Optional,可能有傳回值,也有可能傳回nil(空值)。
    這樣要求開發人員需特別的指定Optional便是為了避免意料外的情況(出現runtime error),以減少APP在執行時期可能產生的錯誤。(說明白了,也可能是為了APP的使用者體驗吧),也提昇了Swift程式碼品質。

Unwrap拆解

    繼續前面的findStockCode函式範例,現在我們在Swift程式中要使用它:


    結果卻還是在紅色那行產生了compile error,我們不是已經指定了Optional型別了嗎?為什麼還會發生錯誤?
這是因為在使用Optional型別變數時,我們必須先確定該Optional型別變數是否有值,若有的話必須用「!」來unwrap它才行,處理Optional有下列兩種方式:

接收和處理Optional型別

Forced Unwrapping

    第一種是forced unwrapping,我們將上面的範例修改如下,執行時就不會出現錯誤了。請注意紅字的部份,我們加上了「if和「! 」:首先使用if確定stockCode有值(有值會傳回true),然後再用「! 」來unwrap這個optional的變數。



    如果我們沒有先用if 去判斷而直接去unwrap會有什麼結果?在complile時不會有任何錯誤,但是在執行時Swift會丟出如下的Runtime error,因此,先用if來判斷optional變數是必要的。
            fatal error: Can’t unwrap Optional.None

Optional Binding

    另一種較簡便且更常被使用的方式稱為optional binding,它可以快速的檢杳Optional變數是否有值,若有的話就unwrap它。我們一樣用上面的例子來說明:



    紅色的部份「if let是optional binding的重點,if let tempStockCode = stockCode這句話所作的事情就是:如果stockCode變數有值,就unwrap它並且把值放入tempStockCode這個暫存變數中,因為tempStockCode確定有值,所以我們不需要加上「!」的unwrap符號。
    我們也可以將「if let」改成「if var」,省略tempStockCode這暫存變數,讓程式碼更為精簡:

    以上是Optional和Unwrap的作法,在下一篇知識文件中,我們再來看看「Optional Chaining」可選鏈的用法,「Optional Chaining」指的是把呼叫、使用optional等等行為串連在一起,當其中有一段回傳nil,則整體的結果就是nil。

2015年2月5日 星期四

Swift學習心得 – Closure

    接續上文的函式,我們來談談「閉包(Closure)」;正如它的名稱一樣,「閉包」這個名詞讓我們這些初學者感覺既抽象又不親切,對於Swift親切易用的形象立馬大打了折扣,但事實上「閉包」是一種更深層次的函式使用方法,不使用閉包,我們一樣可以寫出相同功能的程式,因此這篇您如果沒有興趣,可以先跳過,亦能學習並寫出完整的Swift程式,不過,若我們瞭解並妥善的使用閉包,則可以讓我們的程式更為幹練與精簡。   
維基百科對於「閉包」的解釋是這樣的:「閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是參照了自由變數的函式。這個被參照的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。
看不懂對不對?沒關係,除非真的實作過,否則我們初入門者很難一開始便瞭解它的含義,我找到另外一種比較可以讓人接受的解釋是:「閉包其實也是函式,不過它比較特別的是它擁有一組特殊的環境變數--本來以為應該消失,可是卻依然存在的變數。」,因此,我們大概知道閉包就像一大包的東西,它具有如下的特性:
  • 閉包是一種函式
  • 這個函式搭配了一組環境變數
  • 在使用閉包時,能夠用到這些環境變數
下面我們來透過三個步驟來瞭解「閉包(Closure)」:
  1. 就像我們可以指定數值或字串給某個變數一樣,「函式」也可以當作參數般,指定給變數使用。
  2. 當作參數所傳入的函式變數,它可以「Capture」到位於函式上層的變數(也就是,我們可以在閉包函式中取用到外層的變數值)。
  3. 有兩種方式可以用來建立函式,一種是常見的用func開頭方式宣告,這是最常用的方式,另一種則是用前後大括號{}包起來,這經常用在我們的閉包(Closure)函式中。

A)把函式當作參數值來使用

就像我們經常會指定一個數值或字串給參數一樣,例如下方的範例(請仔細看綠色的說明文字)
    // 這是最常見的,宣告一個變數並給予一個數值參數
func printInt(i: Int) {
           println("you passed \(i)")
}
    //我們也可以把這個函式當作值指定給某個變數
let funVar = printInt
    //把printInt指定給變數funVar後,我們就可以像使用原先函式般的來使用funVar,感覺好像是把函式改名了。
funVar(2)  // 把funVar當成printInt來使用,會印出"you passed 2"
//所以我們可以有一個函式,其參數是另一個函式,如下紅色的參數所示
func useFunction(funParam: (Int) -> () ) {
           //在useFunction中呼叫這個傳入的參數
           funParam(3)
}
   
    //所以,我們可以把printInt這個函式當成參數傳入useFunction函式中,
useFunction(printInt)
//也可以把funVar這個函式變數傳入
useFunction(funVar)

B)閉包函式可Capture到上層的變數:

在Closure函式的外層所宣告的變數,其值仍可被該Closure函式所讀取(在Closure我們稱為補獲),請仔細看下方的例子:
//宣告一個returnFunc函式,其回傳值是一個(Int) -> () 的函式
func returnFunc() -> (Int) -> () {
 var counter = 0  // counter這個變數待會兒可在下方的innerFunc閉包中被capture
 func innerFunc(i: Int) {
      //可讀取到counter這個變數,這個變數位在innerFunc閉包外部,這個動作我們稱為capture
   counter += i   
   println("running total is now \(counter)")
 }
 return innerFunc
}

//下方是這個returnFunc函式使用
let f = returnFunc()
f(3)  //會印出"running total is now 3"
f(4)  //會印出"running total is now 7"
//如果我們重新呼叫returnFunc,counter的值會重新開始計算
let g = returnFunc()
g(2)  //會印出"running total is now 2"
g(2)  //會印出"running total is now 4"
//不過,雖然重新呼叫returnFunc,但是counter值並不會相互影響,因為它們屬於不同的變數
f(2)  //會印出"running total is now 9"

C)閉包函式的格式:

Swift有兩種函式的宣告方式:
  1. 用func開頭,這是最常用的方式。
  2. 用前後大括號{}包起來,常用在Closure函式。
我們看看下方的例子doubler變數,它被塞入了一個用前後大括號{}包起來、未命名(anonymous)的函式作為它的值,這個函式就是閉包Closure。
let doubler = { (i: Int) -> Int in return i*2 }
//這個doubler能像一般的變數般當作參數傳遞
[1, 2, 3].map(doubler)  
用前後大括號{}包起來這種方式,不同於用func開頭來宣告,首先,它不需要給名稱,因此它是匿名(anonymous)的。這種函式用法主要用在閉包中,我們稱它是一種自包含(self-contained)的匿名函式:用一個大括號 { } 把程式碼封裝起來,括號裡緊接著函式的型態 ( ) -> ( ) ,型態宣告後面則用 in 這個關鍵字將型態與 Closure 的主程式碼分開,這樣的寫法就是 Closure 最標準的型式;Swift這種closure的方法相當類似於C和Object-C中的程式碼塊、C++和C#中的Lambda運算式、或Java中的匿名內部類別。
    因此Swift閉包語法的標準格式如下:
        {(參數列表) -> 回傳值型別 in
            指令組
        }
例:
        {( a:Int, b:Int) -> Int in
            return a+b
        }
    我們還可以把它寫成一行如下:
{ (參數列表) -> 回傳值型別 in 指令組 }
例:
        {( a:Int, b:Int) -> Int in return a+b }

    但Swift其實可以自動根據上下文環境自動判斷出參數型別和回傳值型別,所以我們可以再簡化如下:
        { a, b in return a+b }
    這樣的閉包夠簡單了嗎?但還可以更簡化,因為return關鍵字其實也可以不需要:
        { a, b in a+b }
    夠簡化了嗎?但如果我們用$0, $1, $2… 來表示閉包中的參數,$0代表第一個參數,$1代表第二個參數,$2代表第三個參數,依此類推則$n+1代表第n個參數,這在Swift中稱為參數名稱縮寫功能,使用此功能後,閉包中的in這個關鍵字也可以省略了,因此我們可以再把上述的閉包寫成如下:
{ $0 + $1 }
  很神奇的,本來我們的閉包長得如下:
        {( a:Int, b:Int) -> Int in
            return a+b
        }
竟然可以縮寫成為 { $0 + $1 } ,足見閉包在簡化程式碼、增加可讀性上的便利。
        所以,還記得我們一開始舉的doubler例子嗎?它可以逐步簡化如下:
  1. [1, 2, 3].map( { (i: Int) ->Int in return i * 2 } )
  2. [1, 2, 3].map( { i in return i * 2 } )
  3. [1, 2, 3].map( { i in i * 2 } )
  4. [1, 2, 3].map( { $0 * 2 } )
  5. [1, 2, 3].map() { $0 * 2 }
  6. [1, 2, 3].map { $0 * 2 }

D)一個簡單的例子

最後我們再用一個簡單的加減計算函式作為例子,當作本文的總結。
首先我們用標準的方法來實作一次。
    // calculate函式會回傳一個函式型別為(Int,Int)-> Int的值
    func calculate(opr :String)-> (Int,Int)-> Int {  
        func add(a:Int, b:Int) -> Int {    //定義加法add函式
            return a + b
        }
        func sub(a:Int, b:Int) -> Int {    //定義減法sub函式
            return a - b
        }
        var result : (Int, Int)-> Int   
        switch (opr) {            //若傳入為+則傳回add函式,-則傳回sub函式
        case “+”:
            result = add
        case “-“:
            result = sub
        default:
            result = add
        }
        return result;
}

    這個函式的用法是;
        var f1 : (Int,Int)-> Int    //先宣告一個f1變數,型態為(Int,Int)-> Int的函式
        f1 = calculate(“+”)    //呼叫calculate函式並傳入+,則會傳回加法add函式儲存於f1變數
        println(“10 + 5 = \(f1(10, 5))”)    // f1(10, 5)等同於執行add(10, 5)
        var f2 : (Int,Int)-> Int    //先宣告一個f2變數,型態為(Int,Int)-> Int的函式
        f2 = calculate(“-”)    //呼叫calculate函式並傳入-,則會傳回加法sub函式儲存於f2變數
        println(“10 - 5 = \(f2(10, 5))”)    // f1(10, 5)等同於執行sub(10, 5)

接下來我們用閉包的方法來實作一次。

func calculate(opr :String)-> (Int, Int)-> Int {
    var result : (Int, Int) -> Int
    switch (opr) {
        case “+” :
            result = {( a: Int, b: Int) -> Int in
                return a + b
            }
        default:
            result = {( a: Int, b: Int) -> Int in
                return a – b
            }
    }
    return result;
}

簡化閉包格式

    請注意紅色的部份,它們分別取代了標準函式作法中的add與sub兩個函式,而這部份就是我們所稱的閉包。依上文中提到的閉包簡化方式,程式碼可改寫如下:
func calculate(opr :String)-> (Int, Int)-> Int {
    var result : (Int, Int) -> Int
    switch (opr) {
        case “+” :
            result = { $0 + $1 }
        default:
            result = { $0 + $1 }
    }   
    return result;
}