Swift 程式語言

以Parse與Swift建構一個像Instagram一樣的App

以Parse與Swift建構一個像Instagram一樣的App
以Parse與Swift建構一個像Instagram一樣的App
In: Swift 程式語言

這篇教學解釋了如何由Parse來建立後台的方法。我們會建立一個像Instagram 一樣的App,其中包含了這些功能:

  1. 從Parse 載入資料,將它存在local 端。
  2. 儲存資料至Parse,並將其寫入至雲端。
  3. 投票或者是對喜愛的貓咪圖片按讚。

這個App完全由Swift所建構,Swift是Apple作為iOS App的新程式語言。Parse還沒有以Swift來編寫,所以我們會建立一個Bridging Header來處理它。

Paws - Parse Demo in Swift

以下將是你會學到的部分:

  • 利用Parse 雲來做資料的存取
  • 在Swift 專案中以Cocoapods整合Objective-C 框架(framework)
  • 以介面建構器(Interface Builder)設定視圖以及一個自訂的表格視圖cell。
  • 從頭開始以Swift來編寫整個App。
  • 實作Auto Layout 與約束條件。
  • 使用手勢辨識器(gesture recognizers),optionals、條件式(condition)、屬性(property)、outlet與動作(action)。

我們開始吧

首先,確定你已經有了Parse帳號。你可以至Parse.com 以Facebook、Google+或者 GitHub帳號,或者輸入你的email地址來登入。

接著登入Parse並至https://www.parse.com/apps進到你的App Dashboard。

點擊上方處的「Create a new App」按鈕建立一個新的App。並輸入「Paws」作為我們App的名稱。然後,打開新的App並確定你有見到Core標籤,看起來如下圖所示。

Parse Cloud

建立 Datastore

Parse技術上只是一個線上資料庫。資料以帶有名稱以及幾個欄位的物件來儲存,跟試算表很像。這樣的物件稱作一個類別,而它的功能如同資料結構的藍圖,我們要處理的類別稱作Cat。

在Core 標籤,點擊「Add a class」按鈕。確認下拉框是寫Custom,並輸入類別名稱為Cat,然後點擊「Create class」。

parse-create-class

Parse幫我們建立了新類別並加入一堆標準的欄位,像是objectId、createdAt、updatedAt 以及 ACL,我們加入更多欄位吧!

按下上方的 +Col 按鈕,以名稱搭配型態加入以下的欄位:

  • Name: name, type: String.
  • votes, type Number.
  • url, type String.
  • cc_by, type String.

這些欄位用來儲存我們cat資料庫的基本資訊。

資料導入

現在我們已經設定好架構,我們可以導入資料。儲存這個檔案至你的電腦:Cat.json

然後,回到Core標籤與我們的資料庫,點擊左上方的Import 按鈕。選取你剛儲存以及上傳的檔案。確認 Collection Type 「Custom」被選取,並重新將collection 命名為Cat (不是 rs1_Cat),然後點擊Finish Import 。 Parse會通知你的導入已經完成,通常會很快,然後點擊Got it! 。然後重載網頁。

如果一切沒有問題,你應該會見到資料庫中有填入10隻貓咪。它們全部都有名字、一個URL、一些票選數(votes)、以及標註圖片原始作者的欄位。

Parse Cloud

這是我們在Parse中所需要的。現在,我們來設定Swift Xcode專案。

設定Xcode專案

打開Xcode並建立一個新專案,從開始的畫面,或使用選單File -> New -> Project 。

從 iOS->Application挑選「Single View Application」模板,在下一個畫面,輸入以下的欄位:

  • Product Name: Paws
  • Organization Name: 這裏輸入任何你想輸入的名稱
  • Organization Identifier: 任何你想輸入的,例如 com.appcoda.tw
  • Language: Swift
  • Devices: Universal
  • 不要勾選Use Core Data

點擊Next 並幫專案選擇檔案夾,然後點擊建立。

我們不準備使用Stoyboard,所以點擊左上角名稱為「Paws、 2 targets、 iOS SDK」,來打開Project 設定。在左側列表,點擊Targets下方的Paws,並在畫面主區域的 Main Interface setting,從輸入框中移除Main 文字。

rs1_xcode_2

以Cocoapods 來加入Parse函式庫

我們在App的程式中使用Parse 之前,我們必須將它加入作為依賴件(dependency)。我們使用的Cocoapods是一個套件管理器。許多App專案依賴第三方函式庫,像是Parse,而Cocoapods是可以很容易將函式庫導入並確保能即時更新的工具。

在終端機執行以下的指令來安裝Cocoapods。它會詢問你的Mac使用者密碼。不要加入$ 符號。這個符號只是指出一個shell指令。

$ sudo gem install cocoapods

如果幾分鐘內沒有動作不要擔心, 表示正在安裝Cocoapods。完成後,你會見到好幾行指令,最後會見到gems installed。

cocoapods-install

接下來,在Xcode App專案的根目錄下建立一個空的檔案,將檔案命名為Podfile。打開你偏好的文字編輯器,並貼上以下幾行:

pod 'Parse', '~> 1.7.1'
pod 'ParseUI', '~> 1.1.3'

這個Podfile是告訴Cocoapods,要使用哪一個函式庫,在這個專案中,Parse版本是 1.7.1而ParseUI版本是 1.1.3

現在,關掉Xcode並使用終端機切換至App專案的根目錄。在終端機輸入cd,並在Finder中查詢一下Paws的目錄,然後將目錄拖曳至終端機中。

接下來輸入以下的指令:

$ pod install

Cocoapods 會尋找Podfile並嘗試安裝我們所設定的依賴件。這過程可能需要幾分鐘,結果應該如下所示:

cocoapods-parse-install

CocoaPods已經下載並編譯Parse以及其依賴件,並將其加入新的Workspace。從現在開始,我們不再使用原來的App專案,我們將使用由CocoaPods 所建立的Workspace,它裡面包含了原來的專案與CocoaPods專案。

注意:在 Xcode中使用Finder來瀏覽App的根目錄以打開新的Workspace,並在裡面打開Paws.xcworkspace。確認一下在專案導覽器(Project navigator)左側的兩個專案分別顯示:Pods與Paws。

在我們編寫App程式前,我們需要先建立Parse與我們專案間的連結。Parse是以Objective-C所撰寫的,而我們的專案是以Swift來編寫,這兩個如果沒有做好設定是無法混在一起的。

在Swift專案中使用Objective-C

任何Objective-C函式庫、專案或類別可以透過Bridging Header來跟Swift一起使用。技術上,這樣的介接會將標頭檔(header file)從Objective-C轉譯為Swift。

執行以下步驟來建立Bridging Header:

  1. 在Paws專案的Paws目錄上按右鍵,並加入一個新的檔案至Paws目錄,然後點擊New File… 。
  2. 從iOS -> Source 分類,挑選Objective-C檔案模板,並點擊Next。
  3. 將類別命名為「Paws(或者任何你偏好的名稱)」然後按下continue繼續並儲存檔案。在出現的提示中點擊「Yes」來設置一個Objective-C Bridging Header。

bridge-header

Xcode建立了兩個新檔案:Paws.m and Paws-Bridging-Header.h。我們不需要Paws.m檔,所以只要將它刪除即可,在Paws-Bridging-Header.h檔撰寫以下的程式:

#import   
#import   
#import 

設定這麼多工作只是為了編寫一個專案的程式,對嗎?別擔心,我們稍後就會有些有趣的東西了。記得Parse提供了一個可以馬上使用的後台,已經幫我們省下不少工作天了。

確認Parse的運作

回到https://parse.com/apps,在Parse的Dashboard。將指標移到右上角你的帳戶上,然後點擊Account,接著點擊上方的App keys標籤,你也可以直接以這個網址進去 https://parse.com/account/keys

這個頁面列出了你的App Keys,稍後需要它來辨識這個App與Parse間的網路服務。這樣的Key是由一串字母與數字的組合而成,應該要保密才是,基本上就是App的密碼。

接著打開在Xcode的AppDelegate.swift檔,在application didFinishLaunchingWithOptions 方法處。加入以下的程式至方法中,確認它是方法的第一行,整個方法看起來應該像這樣:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool  
{
    Parse.setApplicationId("...", clientKey: "...")

    return true  
}

將「…」以Parse上的App keys來取代,取代的部分是:

  1. 第一,Application ID。
  2. 第二,Client Key。

然後,試著去執行這個程式:

  1. 確定有選取正確的裝置,檢查Play按鈕的右上角的裝置有被選取,例如你的iPhone或者iPhone 6。
  2. 然後點擊Play按鈕或按下Command-R。

這個專案應該可以順利執行沒有錯誤。在執行期間,當你見到iPhone,它會有一個黑色畫面然後抱怨了一下視窗是空的,這是因為我們移除了MainInterface storyboard,然後還沒有取代它。

好消息是,你已經將你的專案與 Parse以及CocoaPods設定成功了!

建立表格視圖控制器

要顯示Paws App的貓咪們,我們將使用一個表格視圖控制器(table view controller)。這是iOS很常見的介面元件,以垂直列表的方式將資料顯示在每列上。一個很棒的例子是iPhone上的Contacts App:一個顯示聯絡人與電話的垂直列表。在Objective-C 與Swift ,表格視圖控制以UITableViewController 類別為代表。

註記:對類別這個專有名詞不熟悉是嗎?把它想成一個原型,一個用來鑄鐵的模具,將鐵放進模具中,就會得到一個複製品。這樣的複製品稱作某類別的實體(instance)

Parse有一個非常相似的部分稱作ParseUI,這是一個UI元件的集合,跟Parse產品緊緊的整合在一起。我們將會使用PFQueryTableViewController 類別,它延伸了UITableViewController的功能加上Parse上的資料,兩者配合完美。

我們開始建立一個新的Swift類別稱作CatsTableViewController,在專案導覽器,在Paws目錄上按右鍵並挑選New file。從iOS -> Source挑選Cocoa Touch Class 模板。輸入以下的設定:

  • Class: CatsTableViewController
  • Subclass of: PFQueryTableViewController
  • Language: Swift
  • 確認 Also create XIB file 沒有勾選

New class for table view controller

將檔案儲存在Paws目錄。在選擇目錄的時候,確定Paws被勾選作為Target(下面的地方)。

打開新類別檔案,你可以見到基本的結構: 一個viewDidLoad方法,一個didReceiveMemoryWarning方法。注意 CatsTableViewControllerPFQueryTableViewController的擴展,它是子類別。而PFQueryTableViewController又是UITableViewController的擴展。所以在我們從ParseUI加入程式以及功能時,我們的CatsTableViewController會繼承表格視圖功能。

表格視圖控制器的程式撰寫

我們來撰寫表格視圖控制器。首先,我們必須覆寫我們類別的建構方法來設定幾個基本設定。

加入以下兩個方法到類別的上方,就在檔案的第一個大括弧之後。

override init(style: UITableViewStyle, className: String!)  
{
    super.init(style: style, className: className)

    self.pullToRefreshEnabled = true 
    self.paginationEnabled = false  
    self.objectsPerPage = 25

    self.parseClassName = className  
}

required init(coder aDecoder:NSCoder)  
{
    fatalError("NSCoding not supported")  
}

你剛加入兩個方法:

  • 一個指定的初始化設定,init,有兩個參數:一個是表格視圖的樣式,一個是我們從Parse上取得會用到的className(也就是Cat)。
  • 一個必要的初始化設定,required init,有一個參數:一個NSCoder的實體。它的內容跟現在無關,我們只要把它固定作為必要實做的方法,但結構上不是重要的方法。

在第一個init,發生了這幾件事:

  1. 經由super.init()呼叫,透過其父類別(super class)PFQueryTableViewController來初始化自己。
  2. 然後將pullToRefreshEnabled 設為true。這是繼承了PFQueryTableViewController屬性。而self 這個特別的變數是參照目前的範圍,也就是這類別的實體。
  3. 然後,我們關掉了分頁,並設定我們表格物件的最大數為25。
  4. 最後,我們儲存了className參數在parseClassName這一個實體屬性中。

而稍後,我們建立了CatsTableViewController 類別的實體,這個建構器(constructor),或者稱指定初始器(designated initializer),會被呼叫作為設定表格視圖控制器的基礎。

編寫以queryForTable 來取得資料的程式

以Parse 來實作表格視圖的核心是使用PFQueryTableViewController的queryForTable方法。當我們將類別作為PFQueryTableViewController的子類別時,已經繼承這個方法,而我們必須覆寫它:PFQueryTableViewController 在它要連上表格視圖控制器至Parse 的datastore時會呼叫它。它幫表格查詢文字資料,因此這個方法稱作queryForTable。在這個方法內,我們可以客製化取得方式。

將這個方法加至CatsTableViewController 類別,在 viewDidLoad方法下方,注意一下大括號!

override func queryForTable() -> PFQuery {
    var query:PFQuery = PFQuery(className:self.parseClassName!)
    
    if(objects?.count == 0)
    {
        query.cachePolicy = PFCachePolicy.CacheThenNetwork
    }
    
    query.orderByAscending("name")
    
    return query
}

我們來看一下這個新方法的所用到的signature(譯註:signature指的是其函數名稱、參數型別等):

override func queryForTable() -> PFQuery

它內容是什麼?它設定了queryForTable的新方法,告訴了編譯器幾件事:

  1. 以相同名稱(或signature)覆寫了父類別方法,以及陳述的覆寫。
  2. 以func來宣告方法,以及其方法名稱, queryForTable
  3. 以及宣告方法間的參數,在我們的例子是沒有參數。
  4. 最後這個 -> PFQuery ,是方法回傳型態。

方法內,發生了什麼事呢:

  1. 宣告一個新參數,稱作query,並以一個帶有名稱參數為classname(以self.parseClassName 來取得其值)的建構方法(constructor method)來實體化它,換句話說,當我們的表格類別名稱是Cat,Cat實體的查詢(query)在這個地方便會建立。
  2. 然後,倘若查詢結果是空的話,我們在query設定cachePolicy屬性。它的值是PFCachePolicy.CacheThenNetwork 常數,表示查詢會先針對物件做離線快取(cache)查詢,如果不存在的話,它會從線上的Parse datastore下載物件。當表格視圖第一次放置在我們的App的畫面上,if陳述幾乎至少會執行一次。
  3. 然後,我們以「name」欄來排序查詢物件。
  4. 最後,我們將查詢回傳。

將資料放到畫面上

幾乎快完成了!讓我們撰寫這個類別的最後方法。它會自己將資料放到表格視圖上:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell? {
    
    let cellIdentifier:String = "Cell"
    
    var cell:PFTableViewCell? = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? PFTableViewCell
    
    if(cell == nil) {
        cell = PFTableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: cellIdentifier)
    }
    
    if let pfObject = object {
        cell?.textLabel?.text = pfObject["name"] as? String
    }
    
    return cell;
}

這個方法的signature 是這樣:以tableView、indexPath、object參數覆寫tableView cellForRowAtIndexPath方法,回傳一個很明確地,解開(unwarp)的PFTableViewCell 實體。說白話點:這裏有一個表格視圖、有一個物件,這是它的索引(列數),現在請回傳可用的cell 視圖。

然後,我們先宣告一個文字型態的識別碼(identifier)給我們的cell,每一個cell型態有不同的識別碼。我們只會使用一種cell型態,所以我們指定其型態為String,其值為「Cell」。「let」這個陳述則是宣告一個常數,跟變數相反。

然後,我們宣告一個optional變數 cell,型態是PFTableViewCell? ,而我們嘗試要從tableView 參數 dequeue它,這個dequeue是一種重新使用舊表格cell,以增快表格視圖顯示的機制。實體方法dequeueReusableCellWithIdentifier 有一個參數,作為我們設定這個cell型態的識別碼名稱。方法的回傳型態是optional,而我們將它以「?」陳述轉型(casting)為PFTableViewCell,轉型基本上是將一種型態轉型為另一個相容型態,在這裏,我們將UITableViewCell轉為PFTableViewCell,為什麼需要用optional?這是因為如果沒cell去dequeue,這個方法會回傳nil。

然後,當cell實際上是空值,我們從PFTableViewCell建立一個新的cell。我們以識別碼來指出cell的型態,並給予一個UITableViewCellStyle.Default的樣式。

接著,在下一個if陳述,我們做一些很酷的事。通常,你使用optional時,你需要解開(unwrap)它。在這麼做之前,你需要檢查optional是否為空值(nil)。你不能解開一個有可能是空值的optional。在我們的例子,我們使用optional binding(if let)來確認optional是否為非空值。倘若它有包含一個值,我們將這個值指定給一個暫時的常數(pfObject)。

然後,我們指定object[“name”]為textLabel的文字屬性。這個文字標籤很明顯地是在表格視圖的列上做顯示。這個型態為PFObject的變數物件是NSObject的子類別,所以我們可以使用[“…”] 標記來取得名稱為「name」物件的屬性。然後將其轉型為optional String?,因為這個物件可能有,也可能沒有一個稱作name的屬性,它有可能是空值。

最後,我們回傳cell。

快速回顧一下CatsTableViewController

好的,在CatsTableViewController 類別內,我們做了三件事:

  1. 設定幾個基本設定來初始化類別實體。
  2. 覆寫queryForTable: 來整合Parse 後台,這裏我們要使用的類別也是我們的快取方案。
  3. 建立能重複使用的cell並將資料填入,將資料放置畫面中。

將表格視圖放至畫面上

現在,當你想要執行我們的App,它還不會做任何事,我們還沒有將CatsTableViewController連結到我們的App!我們來進行吧。

回到AppDelegate類別並調整application didFinishLaunchingWithOptions 方法如下所示:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool  
{
    Parse.setApplicationId("...", clientKey: "...")

    var tableVC:CatsTableViewController = CatsTableViewController(className: "Cat")
    tableVC.title = "Paws"
    
    UINavigationBar.appearance().tintColor = UIColor(red: 0.05, green: 0.47, blue: 0.91, alpha: 1.0)
    UINavigationBar.appearance().barTintColor = UIColor(red: 0.05, green: 0.47, blue: 0.91, alpha: 1.0)
    UINavigationBar.appearance().titleTextAttributes = [NSForegroundColorAttributeName: UIColor.whiteColor()]
    UIApplication.sharedApplication().statusBarStyle = UIStatusBarStyle.LightContent
    
    
    var navigationVC:UINavigationController = UINavigationController(rootViewController: tableVC)
    
    let frame = UIScreen.mainScreen().bounds
    window = UIWindow(frame: frame)
    
    window!.rootViewController = navigationVC
    window!.makeKeyAndVisible()
    
    return true
}

首先,確認你有使用正確的 Parse Application ID與Client Key。

然後,我們建立新的CatsTableViewController的實體,並指定給tableVC 變數。我們使用一個只有一個classNmae(值為「Cat」)參數的初始器,一開始這個初始器在內部會被呼叫。我們也給tableVC一個標題(title),「Paws」,一個在任何UIViewController(我們的表格視圖繼承這個類別)可以取得的屬性,並且稍後被導覽控制器(navigation controller)來使用。

然後,我們變更UINavigationBar的外觀。該類別是由導覽控制器所使用,這是在許許多多的App中在上方所使用的導覽條(navigation bar)。設定外觀來確認該類別的實體有繼承一些樣式規則,在這個範例是指 tintColorbarTintColor。它們兩個都是設為青色,並直接以UIColor的實體來指定。導覽條的文字顏色是設為白色,然後我們設定iPhone狀態欄(status bar)為light主題(也是白色)。

注意:這不是必要的,但是要讓狀態欄的顏色變更生效,你必須加上一個新列至Info.plist檔。你會在Supporting Files目錄找到它。打開這個檔案,插入一個新列(在選單按右鍵),然後不是在裡面貼上「View controller-based status bar appearance」就是「UIViewControllerBasedStatusBarAppearance」,確認row值設為NO或false。

然後,我們最後建立UINavigationController自己的實體,並將其指定給navigationVC變數。我們以rootViewController,也就是第一個視圖控制器為:tableVC來初始化它。以下是我們的視圖層級表示:

UIWindow -> UINavigationController -> CatsTableViewController

那麼,我們在導覽控制器中內部顯示一個表格視圖控制器,並把它放進我們App 的UIWindow的最上層根視圖控制器。

最後,一些樣板程式:我們建立一個UIWindow實體,指定給它一個框架,指定根視圖控制器,讓它變成我們App的主視窗。

執行App

好的,我們按下Command-R,或按下左上的Play按鈕來執行App。如果一切都正常的話,你的App應該會出現一個基本的藍色表格視圖,顯示10筆貓咪名。

你可以下拉表格視圖來更新,它會從Parse下載新的資料,並重新載入表格視圖。

我們待會可以豐富這個基礎App的內容。花點時間來享受目前的成果吧!你已經成功的以Pars實作出一個可以運作的App原型,太棒了!

paws-text-demo

在介面建構器建立一個客制的 UITableViewCell

好的,讓我們以客製化的表格視圖cell來美化這個App,我們不再只是顯示文字的cell,我們會以圖像–名稱–票選 的cell來代替,它看起來就如同這篇文章最上面的圖片一樣。

我們由建立一個新類別來開始,類別名稱為CatsTableViewCell。在Xcode的檔案導覽器中的Paws按右鍵,並選取New File… 。從iOS->Source加入一個Cocoa Touch類別。將它稱作CatsTableViewCell 並作為UITableViewCell的子類別。然後勾選Also create XIB file。語言選Swift,檔案當然就是建立在Paws目錄中。

然後,打開CatsTableViewCell.swift 檔,並變更類別定義為:

class CatsTableViewCell: PFTableViewCell

看一下我們做了什麼?這個類別是擴展為PFTableViewCell,取代了UITableViewCell。記得tableView cellForRowAtIndexPath 方法嗎?它回傳PFTableViewCell 型態的實體,這也是我們要變更的原因。

在這個類別的第一行,第一個大括號之後,加入以下的outlet至新的CatsTableViewCell中。

@IBOutlet weak var catImageView:UIImageView?  
@IBOutlet weak var catNameLabel:UILabel?  
@IBOutlet weak var catVotesLabel:UILabel?  
@IBOutlet weak var catCreditLabel:UILabel?

我們需要這4個outlet來顯示Parse的4種資料:貓咪圖片、貓咪名稱、所得票選數、以及圖片的原始作者。

接下來,從專案導覽器中打開CatsTableViewCell.xib,它會在介面建構器中打開,這是在Xcode內對設定App使用者介面超有幫助的工具。這是一個鷹架的工具,它不會建立一個有功能的App UI,只是先做為定義。以汽車音響來比喻的話,介面建構器建立了音響主控台,而你寫的Swift程式就是線路。

首先,執行以下步驟:

  1. 在左側的文件大綱(Document Outline)點擊主cell元件。
  2. 選取右側尺寸檢閱器(Size Inspector)標籤。
  3. 變更Row Height 為350,Width為 320,同樣的Height為350。

主要視圖現在應該會重新調整了大小。

rs1_xcode_5

現在我們將會加入4個視圖:

  1. 從右下元件庫中,找到UIImageView類別。
  2. 從元件庫拖曳圖像視圖(image view)至cell視圖。
  3. 調整圖像視圖大小,將其水平置中,並距離上、左、右側邊緣各15點。你也可以用尺寸檢閱器來完成,選擇圖像視圖,然後設定它的X與Y位置為15點,Width與Height設為290點。

重複上面的步驟來加入3個新視圖,也就是UILabel的實體。將其中一個對齊左側,一個對齊右側。以上面的視圖做為參考。左側標籤位於(25,317,209,21),分別代表X、Y、Width與Height 。右側的標籤是位於(225,317,69,21)。而圖片版權聲明標籤是位於(199,285,106,21)。

interface-builder-custom-cell

接下來,設置全部的4個視圖。帶出右側的屬性檢閱器,針對每一個視圖做以下的設定:

  1. 圖像視圖:Mode 設定為Aspect Fill、勾選 Clip Subviews。
  2. 左側標籤:Font 設定為 System Bold 17.0 ,而 Color 設為 Black。
  3. 右側標籤:Font設定為System 14.0而Color 設為 Light Gray Color。
  4. 圖片版權聲明標籤: Font設定為System 14.0 而Color 設為 White。

現在我們將這些視圖與outlet做連結。首先,再次選取在左側文件大綱(Document outline)的Cats Table View Cell。然後切換至右邊的連結檢閱器(Connections Inspector)標籤。

現在,在位於檢閱器下Outlets下的四個outlet。是否看到空的圓圈?從catImageView右側圓圈,拖曳至cell中的圖像視圖。會出現一條藍色線,而在檢閱器,outlet出現了一個對應選項。重複以上的步驟來完成其他3個標籤。

rs1_xcode_6

針對客製化Cell設定Auto Layout約束條件

注意:事實上你可以略過Auto Layout約束條件(Constraints)的設定,但是你的App在不同的iPhone畫面看起來會不好看。倘若你對Auto Layout的約束條件不熟,花點功夫學習一下。你的未來會感謝你現在的行動。你可以參照這篇有關Auto Layout的教學

為了讓你的App在iPhone 4、4S、5、5S、6與6Plus看起來都正常,我們必須針對介面建構器的UI元件加上一些規則。介面建構器內建了一個非常方便的功能稱作Auto Layout,並使用約束條件來管理視圖位置、對齊與尺寸調整。Auto Layout的用法可能有點奇特,因為它直覺且非常具邏輯性。說倒底,這個工具在處理複雜的的調整工作非常有效率,可以替你省下佈局程式的撰寫工作量。

技術上,約束條件只是視圖的規則。以下是我們的視圖要去遵循的規則:

  • 圖像視圖: 水平置中、高度固定為 290 點、寬度彈性、但是距離cell上、左、右側各 15 點。
  • 左側標籤:寬度彈性、高度固定為21點、距離cell 左側邊緣25點(稱作Leading Space)、而距離cell底部11點。
  • 右側標籤:寬度彈性、高度固定為21點、距離cell 右側邊緣25點、而距離cell底部11點。
  • 圖片版權聲明標籤:寬度彈性、高度固定為21點、距離右側邊緣15點、而距離頂部285點(有效的將其定位在圖像視圖的右下方)。

至少有四種方式可以設定約束條件,但是我們會使用最簡單的方式:以上方的Editor選單來實作。

要設置圖像視圖,選取它之後再選取以下的選單選項:

  1. Editor -> Align -> Horizontal Center In Container
  2. Editor -> Pin -> Height
  3. Editor -> Pin -> Leading Space To Superview
  4. Editor -> Pin -> Trailing Space To Superview
  5. Editor -> Pin -> Top Space To SuperView

然後,設定左側標籤:

  1. Editor -> Pin -> Height
  2. Editor -> Pin -> Leading Space To Superview
  3. Editor -> Pin -> Bottom Space To Superview

再來是設定右側標籤:

  1. Editor -> Pin -> Height
  2. Editor -> Pin -> Trailing Space To Superview
  3. Editor -> Pin -> Bottom Space To Superview

然後,設定圖片版權聲明標籤:

  1. Editor -> Pin -> Height
  2. Editor -> Pin -> Trailing Space To Superview
  3. Editor -> Pin -> Bottom Space To Superview

你有看到我們剛剛將一些視圖靠近邊緣,並以一些位置來固定?你的畫面看起來會跟下圖類似。

rs1_xcode_7

以Swift來使用客製化表格視圖Cell

好的,我們回到程式部分–介面建構部分就這樣!打開
CatsTableViewController.swift ,位於初始器init(style: classname:) 處。

在該方法self.parseClassName = className; 的下方處,加入以下兩行程式:

self.tableView.rowHeight = 350
self.tableView.allowsSelection = false

第一行設定列為合適高度,而第二行不允許做cell的選取。

然後,加入以下的程式至viewDidLoad方法內,在super.viewDidLoad() 上方:

tableView.registerNib(UINib(nibName: "CatsTableViewCell", bundle: nil), forCellReuseIdentifier: cellIdentifier)

這一行有可能會產生一個錯誤。為了解決它,將以下在tableView cellForRowAtIndexPath方法的這一行移到類別上面,並重新命名其值為「CatCell」。

let cellIdentifier:String = "Cell"

這類別目前的定義如下所示:

class CatsTableViewController: PFQueryTableViewController  
{
    let cellIdentifier:String = "CatCell"

    override init!(style: UITableViewStyle, className: String!)

我們已經將cellIdentifier 常數從local方法範圍移到class範圍,可以在整個類別使用:tableView cellForRowAtIndexPath 以及 viewDidLoad 皆可。

接下來,以下面的程式去取代tableView cellForRowAtIndexPath的內容:

var cell:CatsTableViewCell? = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? CatsTableViewCell

if(cell == nil) {
    cell = NSBundle.mainBundle().loadNibNamed("CatsTableViewCell", owner: self, options: nil)[0] as? CatsTableViewCell
}

if let pfObject = object {
    cell?.catNameLabel?.text = pfObject["name"] as? String
    
    var votes:Int? = pfObject["votes"] as? Int
    if votes == nil {
        votes = 0
    }
    cell?.catVotesLabel?.text = "\(votes!) votes"
    
    var credit:String? = pfObject["cc_by"] as? String
    if credit != nil {
        cell?.catCreditLabel?.text = "\(credit!) / CC 2.0"
    }
}

return cell

跟我們之前所使用的程式有什麼不同呢?是這樣的:

  1. 這個cell型態從PFTableViewCell 變更為 CatsTableViewCell
  2. 當cell是nil(也就是它不能被dequeue時),一個新cell從我們剛建立的XIB中建立出來。我們從集合中取得它,並給予目前類別的所有權,然後將其轉型為CatsTableViewCell
  3. 然後我們檢查是否有存在的物件,並試著去將之前Parse物件的name欄位值指定做為其標籤文字。
  4. 然後,catVotesLabel與其文字也是一樣。在Parse內的votes欄,不是String型態而是Int型態,因此轉型為Int?。當votes碰巧是空的,我們將其設定為零。然後以一個稱作字串插值(string interpolation)的技巧來將標籤文字設為 …votes。至於圖片版權聲明標籤的內容大致相同。

最後,我們回傳cell。注意是正確的型態!

我們再次執行App。一切都沒問題嗎?沒有bug、閃退、冒煙或者有火焰?很好!
但是,圖像在哪?

從Parse非同步下載圖像

沒有圖像!我們來將它加入吧。在tableView cellForRowAtIndexPath 加入以下的程式,將其加在最後的if陳述(針對圖片版權聲明標籤)之後,return陳述之前。

cell?.catImageView?.image = nil
if var urlString:String? = pfObject["url"] as? String {
    var url:NSURL? = NSURL(string: urlString!)
        
    if var url:NSURL? = NSURL(string: urlString!) {
        var error:NSError?
        var request:NSURLRequest = NSURLRequest(URL: url!, cachePolicy: NSURLRequestCachePolicy.ReturnCacheDataElseLoad, timeoutInterval: 5.0)
            
        NSOperationQueue.mainQueue().cancelAllOperations()
            
        NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {
            (response:NSURLResponse!, imageData:NSData!, error:NSError!) -> Void in
                
            cell?.catImageView?.image = UIImage(data: imageData)
                
        })
    }
}

哇!這又做什麼?好的,技術上我們將Parse url 欄變成NSURL型態的實體。

我們在主執行佇列使用它來開始非同步的NSURLConnection,下載圖像為NSData 物件。當下載完成之後,一個閉包(closure)開始執行,將下載資料指定為UIImage,也就是指定給catImageView的image屬性。

這裏我不深入探討,因為上面錯綜複雜的程式跟我們的App不相關。不過,注意以下幾點:

  • NSURLConnection的使用很快速,但是可能有點難懂。我們使用很棒的AFNetworking (Objective-C) 或Alamofire (Swift) 函式庫來處理許多複雜的網路資料源。
  • Parse允許你儲存圖像在雲端,且可以直接利用,這是ParseUI整合的部分,但是它不能處理外部的 URLs(例如來自Flicker的貓咪圖片)
  • 在開始另一個非同步的連結,我們先清除所有主佇列的操作。這可能有點難理解:也就是它從主佇列移除pending或沒有完成的下載。你可以試著移除該行程式並執行App,你會見到所有的圖像混在一起。當它重新使用一個cell時,這個dequeue機制沒有重設任何pending的連結,因而造成所有下載圖像的混亂。

準備好了嗎?再次執行App,看是否正常運作。

再次加碼:做出像Instagram一樣的功能

你已經走到這裡了,真的太棒了!我們的App還欠缺一味?要完成的話需要加入最後的功能:像「Instagram」一樣的功能,也就是當你按圖片兩下,就會投該隻貓咪一票,並加上一個簡潔的小貓爪動畫。

首先,幫貓爪圖片加上outlet至表格視圖cell。加入以下的程式至 CatsTableViewCell,在其他4個outlet的下面:

@IBOutlet weak var catPawIcon:UIImageView?

然後,加上一個UIImageView至在介面建構器的CatsTableViewCell.xib,記得怎麼做嗎?

  1. 在元件庫尋找UIImageView 類別。
  2. 從元件庫拖曳它至表格視圖cell。

確認拖曳至圖像視圖的中心,調整新的圖像視圖的寬度與高度皆為100點。它的X與Y兩者大約是110點。然後,圖像視圖選取後,加上以下的約束條件。

  1. Editor -> Pin -> Width
  2. Editor -> Pin -> Height
  3. Editor -> Pin -> Top Space To Superview
  4. Editor -> Align -> Horizontal Center In Container

你可以見到,圖像視圖水平置中,寬度與高度皆固定為100點,跟上方保持固定間距,能確保將其置於貓咪圖像視圖的正中間。

xib-custom-cell-imageview

現在,在文件大綱選擇上方元件選取Cats Table View Cell來建立一個outlet連結,然後選取連結檢閱器標籤,並從catPawIcon拖曳一個藍色線至cell中的圖像視圖中。

接下來,下載paw.zip 。這個檔案包含了三個圖檔,一張圖片有三種解析度。我們必須要在可以使用它之前導入它。

首先,將壓縮檔解開。然後打開在Xcode的Images.xcassets 檔。然後,在左側的清單按下右鍵(一個稱作AppIcon的啟動圖像)並點擊New Image Set,或使用左下的「+」按鈕。將剛加入進去的asset重新命名為Paw並打開它。

現在拖曳剛解壓縮的檔案,一個接一個拖曳進去打開的asset 中,要確認檔案有相符:

倘若你看不見這些檔案的話別擔心,因為它們全部都是白色的。

xcode-image-asset

然後回到CatsTableViewCell.xib 並選取小的圖像視圖。至屬性檢閱器,從下拉式清單選取正確的Paw圖像,這個白色的貓爪圖像現在應該在cell視圖中能看到了。

rs1_xcode_8

最後記得將小的圖像視圖跟catPawIcon outlet連結在一起。

現在,我們回到程式部分。打開在Xcode中的 CatsTableViewCell。在awakeFromNib方法的super.awakeFromNib() 之前加入下面的程式。

let gesture = UITapGestureRecognizer(target: self, action:Selector("onDoubleTap:"))
gesture.numberOfTapsRequired = 2
contentView.addGestureRecognizer(gesture)

catPawIcon?.hidden = true

這裏做了兩件事:

  • 首先,我們設定一個UITapGestureRecognizer,可以讓我們跟任何視圖互動。在這個範例中,我們將它加進去contentView,也就是存放cell的兩個標籤以及兩個圖像視圖的視圖。 它以self做為target,一個選擇onDoubleTap: 方法的selector做為action來初始化。所以,偵測到點擊兩下的動作時,自己(目前類別)的onDoubleTap: 方法便會執行。另外,我們設定點擊數為2,也就是點擊兩下。
  • 第二,我們隱藏catPawIcon outlet。

接下來,加入onDoubleTap 方法至類別中,在awakeFromNib()下:

func onDoubleTap(sender:AnyObject) {
    catPawIcon?.hidden = false
    catPawIcon?.alpha = 1.0
    
    UIView.animateWithDuration(1.0, delay: 1.0, options:nil, animations: {
        
        self.catPawIcon?.alpha = 0
        
        }, completion: {
            (value:Bool) in
            
            self.catPawIcon?.hidden = true
    })
}

這樣的方法稱作一個action,並且總是需要一個參數:以AnyObject型態來做為sender。在這個方法,以下的動畫程式會發生:

  1. 首先,透過設定hidden 為 false將catPawIcon 變成可視。
  2. 第二,它的alpha(透明度)是設為1.0,也就是可以全部看見。這是在圖像狀態需要重設時必須做的,其動畫結束時的alpha值是為0。
  3. 然後設定動畫程式。使用UIView的一個類別方法,有帶5個參數:動畫時間,動畫之前的延遲時間、一些選項、接著是動畫屬性的閉包,然後是動畫完畢後要執行的閉包。

視覺上,會發生這些動作:

  1. 重設視圖為可視,並設定透明度(alpha channel)為可視。
  2. 延遲一秒。
  3. 在一秒內,動畫的透明度從1變為0。
  4. 完成後,隱藏圖像。

這個方案最棒的事是它很容易使用:這程式將會完全處理動畫。我們只需要設定它的開始狀態與結束狀態以及區間,而動畫框架(framework)會自行安排狀態以及步驟。技術上我們使用兩個屬性:一個連續alpha值、一個處理貓爪圖像可視性的布林值。

最後,執行這個App並了解是否功能正常。你可以點擊cell兩下,會短暫出現一個貓爪圖示,然後慢慢淡出。

paws-demo-animation

動作正常嗎?非常棒!

以Parse整合投票動作

還剩下一件事要做,就是當執行按兩下動作時,在Parse Cat物件增加votes欄的票數。

但是要如何做呢?

我們要變更的物件是在CatsTableViewController的tableView cellForRowAtIndexPath 方法的PFObject型態物件。我們不能在表格視圖cell中,也就是點擊兩下動作的地方來存取它。

我們不能移動onDoubleTap方法,所以我們要建立表格視圖物件與表格視圖cell的關聯性。

採取以下的步驟來實現這個做法:

  1. CatsTableViewCell,在類別的上方,outlet底下以這行程式來建立一個新屬性:
  2. var parseObject:PFObject?
    
  3. 然後,在tableView cellForRowAtIndexPath,在cell==nil陳述大括號的下方,寫下這行程式:
  4. cell?.parseObject = object
    

我們現在已經建立了一個機制,將物件從cellForRowAtIndexPath 複製到表格視圖cell,讓物件實體可以在CatsTableViewCell 類別取得。

然後,調整CatsTableViewCell的onDoubleTap 方法,在方法的開始處加入以下的程式:

if(parseObject != nil) {
    if var votes:Int? = parseObject!.objectForKey("votes") as? Int {
        votes!++
        
        parseObject!.setObject(votes!, forKey: "votes");
        parseObject!.saveInBackground();
        
        catVotesLabel?.text = "\(votes!) votes";
    }
}

這個程式做以下的工作:

  1. 檢查parseObject是否為空值。
  2. 試著從parseObject取得投票數(votes),將其轉型為optional Int。
  3. 倘若votes不是空值,以++運算符來增加votes變數值,這跟votes = votes! + 1; 一樣。
  4. 然後以setObject方法將votes變數指定回去給parseObject集合。
  5. 然後呼叫在parseObject!saveInBackground()方法。這會將目前的物件儲存在背景,然後在適當時機將其寫入Parse雲中。
  6. 最後,更新它的文字來呈現新的投票數。

嗯,這就是全部的內容!按下 Command-R或者執行按鈕來執行App,確認新功能是否可行。

是不是可以運作?太棒了!

總結

以下是你今天所學習的內容:

  • 以Parse來存取雲端中的資料。
  • 以Cocoapods來整合Swift專案與Objective-C框架。
  • 以介面建構來設定視圖與客製表格視圖cell。
  • 從無到有以Swift撰寫整個App。
  • Auto Layout與約束條件的實作。
  • 使用手勢辨識、optionals、條件式、閉包、屬性、outlet與action。

你可以下載完整Paws專案檔來進一步了解。請使用Xcode 6.3(或以上)的版本來執行這個專案。注意你必須將在AppDelegate.swift檔中的application key與 client key 變更為你自己的。同樣的,記得自己寫程式會更學得更好,更能有所領悟,所以不要只是複製貼上而已。

你對這篇文章有什麼看法呢?可以在以下的評論中留言給我。

譯者簡介:王豪勳 -渥合數位服務創辦人,畢業於台灣大學應用力學研究所,曾在半導體產業服務多年,近年來專注於協助客戶進行App軟體以及網站開發,平常致力於研究各式最軟硬體技術,擁有多本譯作。
原文Building an Instagram-Like App with Parse and Swift
作者
De Vries Reinder
Reinder de Vries 是創業家也是手機程式開發員,所開發的Apps超過50個,編寫的程式有多達十萬人次下載使用。Reinder 熱心教學,自設平台 LearnAppMaking.com 培訓更多有志之士成為手機程式開發員。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。