屬于你自己的待辦清單app(Your own to-do app)
To-do list(待辦清單)app是Apple Store里最流行的app類型之一,僅次于fart app(國外非常流行的一種整蠱app)。iPhone甚至有自己內建的提醒app(幸運的是,并沒有內建的fart app)。
制作一個待辦清單app,某種程度上已經成了iOS開發初學者的一種儀式,所以你也要做一個,以免被當作異教徒燒死。
屬于你自己的這個待辦清單app—Checklists,在它完成的時候,看起來應該是這個樣子的:
這個app可以使你將待辦事項組織為一個列表,并且核對你已經做完的事項。同時你會設置一個提醒器,在哪些事項處理時間到來時在iPhone上彈窗提醒,即使app并沒有運行,也同樣會提醒。
Table views and navigation controller(表視圖和導航控制器)
本次課程將為你介紹兩種在iOS中最常用的UI元素:table view和navigation controller。
一個table view可以展示一個列表。上面圖中的三個界面都使用了table view。事實上,這個app的所有界面都由table view組成。table view這種組件的適用性非常強并且也是你在iOS開發的路上遇到的最重要的一個環節,沒有之一。
navigation controller(導航控制器)允許你建立一個界面層級,讓你從一個頁面轉到另一個頁面,通過導航欄頂部標題為‘back’的按鈕。
在這個app中,比如你點擊分類列表中叫做“會議”的分類,就會滑入具體的包含幾個會議的待辦列表界面中。而左上角的按鈕會是你通過一個平滑的動畫,回到先前的分類列表界面中。移動這些具備層級關系的界面,就是navgation controller的作用。
navigation controller和table view經常一起使用:
看一眼你的iPhone中內建的app—日歷、信息、備忘錄、聯系人、郵件,設置—你會注意到,即使他們不同,但是他們都以同樣的方式給你提供良好的服務。
那是因為它們都使用了table view和navigation controller:
(音樂app同樣也在底部包含一個選項卡,你會在下一個課程學到這些東西)
如果你想要學會如何開發iOS app,那么你必須精通這兩種組件,因為它們幾乎在每種app中都會出現。這就是為什么你要對本次課程投入極大精力。你同時會學會如何從一個界面傳遞數據到另一個界面,這是非常重要的一個技能,并且大多數初學者都會被它弄暈。
當本次課程結束時,你會非常熟悉view controller、table view和delegate這些概念,你在睡著時也可以對它們編程(雖然我希望你能夢到點其他好東西)。
這次你將會面對非常長的源代碼,所以多花點時間去沉淀它們。我非常鼓勵你在這些代碼上自己修修改改,多去實驗。改變內容看看會發生什么,即使把app搞崩潰了也不要緊。
錯誤會帶來bug,在本次課程的全程,你會在體驗痛苦到撕裂頭發的挫折感,到你一瞬間意識到錯誤在哪里并且修正它們的舒適感的過程中。
毫無疑問,多寫多練就是學習代碼的最快方法。
順便說一下:如果你有不清楚的事情——比如,你想知道為什么swift中的方法名稱看起來如此滑稽—不要驚慌!有點信心,并且堅持下去,所有的東西都會在合適的時候被解釋清楚。
Checklists app的設計
為了你知道你前進的方向,這里展示一下Checklists app工作方式的概覽:
這里簡單翻譯一下:
1、Checklists的待辦項目分類列表(點擊?或者i按鈕轉到2,點擊具體某個條目的名稱轉到4)
2、添加或者編輯Checklists條目(點擊默認圖標轉到3)
3、選擇圖標(比如你可以為“行程”這個分類添加一個飛機樣式的圖標)
4、每一個Checklists的具體待辦事項(點擊?或者i按鈕轉到5)
5、添加或編輯某一個Checklists的具體待辦事項(點擊日期轉到6)
6、選擇一個提醒時間
7、時間到了以后,在iPhone上彈出提醒窗口
app的主界面展示你所有的Checklists(1)。你可以創建多個Checklists項目。
一個checklists需要一個名稱,一個圖標,0個或多個具體的待辦內容。你可以點擊Add/Edit來增加一個checklists或者改變checklists的名稱或圖標,就是(2),(3)所示。
如果你點擊Checklists的名稱,則會轉到具體的待辦事項界面,如(4)所示。
一條具體的待辦事項需要一個描述,一個對勾復選框用于表示是否完成,和一個可以選擇的執行時間。你可以通過點擊Add/Edit來增加一個待辦條目或者編輯一個現有的待辦條目,如(5)所示。
如果選擇了提醒時間,則當到這個時間時,iOS會自動提醒用戶。即使app不是正在運行的狀態也可以提醒,這是非常高級的一個功能,但是我覺得你有能力完成它。
你可以在隨書附件中找到所有關于本次課程的源代碼,所以你可以盡情的作出修改,以了解它們是如何工作的,不要怕弄壞它們。
準備好了嘛?我們要開始了!
重要:我們的課程是基于Xcode 8的,如果你還在使用Xcode 7,請立刻去Mac store中升級版本。
但是也不要升級的太高了,Apple公司也常常在一個新版本即將發布前,發布一個可用的beta版本。不要用beta版本來學習我們的內容,beta版本經常會出現你預料之外的東西,然后把你帶到云里霧里。堅持用官方的正式版本。
初識table view
了解table view是非常重要的,你將通過檢查它的工作方式作為開始。制作一個列表從來沒有像這么愉快過。
因為聰明的開發者都會把工作分解為小的,簡單的步驟,所以這就是你在這一小節課程中要做的事情:
1、放一個table view到app的界面上
2、在table view中放入數據
3、允許用戶點擊表中的某一行觸發一個對勾符號的顯示和關閉
一旦你擁有了這些基礎部分并且能夠運行,你將會持續將本次課程中的新功能不斷的添加上去,直到最終完成這個app。
運行Xcode,并且新建一個工程項目。選擇Single View Application模版:
Xcode會要求你填寫一些選項:
照著下面填寫:
Product Name:Checklists
Team:保持默認不要動
Organization Name:你的或者你公司的名字
Organization Identifier:使用你自己的標識符,倒過來的域名或者郵箱
Language:Swift
Device:iPhone
Use Core Data,Include Unit Tests,Include UI Tests:這三個復選框不要選中
點擊Next選擇一個目錄保存project
如果你想的話,你可以立刻運行這個app,不過此時你只能得到一個白色的屏幕。
Checklists只運行在豎屏模式下,但是Xcode會默認豎屏及橫屏方向都可以運行。
點擊工程導航器中的Checklists工程項目(藍色圖標那個),打開工程設置界面,并且找到General子頁。然后在Deployment Info分節中找到Device Orientation,確保僅選中Portrait復選框。
隨著landscape復選框被取消選中,你旋轉設備就再也起不到任何效果。app始終為豎屏顯示。
上下顛倒(Upside down)
設置設備方向的地方還有一個上下顛倒選項,但是你基本用不到它。
如果你的app支持上下顛倒,用戶就可以旋轉它們的iPhone,這樣會使得Home鍵位于app界面的上方,而不是下方。
這會導致一些混亂,特別是當用戶在運行app的過程中,如果處于上下顛倒的狀態,此時突然接入一個電話,很容易導致用戶拿反手機,聽筒在下,話筒在上。
但是iPad的app,基本上需要同時支持4個方向,包括上下顛倒。
編輯storyboard
Xcode創建了一個由single view controller(單視圖控制器)組成的基礎app。回憶一下view controller是用于代表你app上的一個屏幕界面和一個相應的ViewController.swift文件和Main.storyboard
上設計的用戶界面。
這個storyboard將你的app上的所有的view controller的設計包含到一個文件之中,用箭頭展示它們之間的流動。在storyboard的術語中,每一個view controller都是一個場景(scene)。
你已經在BullsEye中使用過了storyboard,但是在本次課程中,你將解鎖storyboard的全部力量。
點擊Main.storyboard打開界面建造器。
這個場景的大小是匹配iPhone6或者iPhone7的。我們曾經使用過底部的View as:面板切換到稍微小一點的iPhone SE上,因為這樣可以節約出一點空間。然而,你在storyboard中選擇哪種類型的設備來編輯都是一樣的:app會隨著iPhone設備的不同,自動重新調整自己的大小。
在綱要面板中選擇View Controller。
小帖士:回憶一下,綱要面板展示了storyboard中所有場景的視圖層級。如果你看不到這個綱要面板,那么就點擊下圖中箭頭指向的小按鈕,這個按鈕負責綱要面板的可視與否。
選擇綱要面板上最上面的View Controller Scene并且點擊鍵盤上的delete刪除它,操作完畢后,綱要面板上會顯示一個大大的“No Scene”。
你刪除它是因為你不需要一個標準的view controller,而是需要一個table view controller。這種特殊的view controller類型在操作table view時會簡單很多。
將ViewController的類型改變為table view controller,首先我們需要編輯一下它的swift文件。
點擊ViewController.swift打開代碼編輯界面,將下面這一行:
class ViewController: UIViewController {
修改為:
class ChecklistsViewController: UITableViewController {
通過這一改動,你告訴swift編譯器你的view controller現在是UITableViewController的對象了,不再是標準的UIViewController。
記住,所有以UI開頭的東西都屬于UIKit。這些預制的組件像模塊一樣為你的app服務。
當Xcode創建工程的時候,它假設你需要一個建立在UIViewController之上的ViewController對象,但是在這里,你用UITableViewController代替了它。
你同時將ViewController重命名為ChecklistsViewController,使得它的描述更加清晰。這是屬于你自己的對象-因為它沒有以UI開頭命名。
在本次課程中,你將添加數據和功能到這個ChecklistsViewController對象,使它能具體完成某些工作。你同時也會添加一些新的view controller到這個app中。
在左邊的工程導航器中,點擊選中ViewController.swift,然后再點擊一次,你就可以編輯它的名稱了。(不要用雙擊,雙擊的話會在一個新的窗口打開這個文件)。
將它的名稱修改為ChecklistViewController.swift:
你也許會看到一個警告:“The document could not be saved. The file has been changed by another application”。點擊Save Anyway,這樣這個警告就離開你的視線了。
回到storyboard,去對象庫(Object Library)中拖出一個Table View Controller到畫布上:
這樣就添加了一個新的Table View Controller場景到storyboard。
點擊選擇黃色圖標的Checklist View Controller,然后打開身份檢查器(Xcode窗口的左邊,檢查器面板的第三個),并且在Custom Class下面的Class選項的文本框中輸入ChecklistViewController(或者使用小的那個下拉箭頭進行選擇)。
小帖士:當你這樣做的時候,確保選中的是Table View Controller而不是其中的Table View。當你確實選中Table View Controller的時候應該會有一個淺藍色的邊框包圍著整個場景。
綱要面板上的場景名稱現在應該變為“Checklist View Controller”了。你成功的將ChecklistViewController從一個標準的view controller改變為一個table view controller了。
就像它的名字一樣,你現在可以在storyboard里看到了,這個view controller包含了一個Table View對象。很快我們會講到controller和view的區別。
如果這里沒有一個指向你的新的table view controller的大箭頭,那么你需要選定ChecklistViewController,然后在其屬性指示器里選中Is Initial View Controller復選框。
初始視圖控制器(Initial View Controller)是你在屏幕上看到的第一個界面。沒有它,當你的app運行時,iOS就不知道應該讀取哪一個view controller,然后你的app將以一個黑屏啟動。
在模擬器中運行app
你應該可以看到一個空的列表。這就是table view。你可以上線滾動這個列表,但是它里面沒有任何數據。
順便說下,你用那個型號的iPhone模擬器都沒關系。Table View會重新調整自己的大小適應任何設備,你在iPhone SE上看到的將和在iPhone7上看到的別無二致。
個人而言,我會使用iPhone SE模擬器,因為它占地比較小,可以節約Mac屏幕的空間,特別是你的屏幕不夠大的話。
??:當你運行app時,會看到一個警告“Prototype table cells must have reuse identifiers”?,F在不用管它,我們后面會處理這個問題。
來解刨一下table view
首先,我們來更多的了解一下table view,它就是一個UITableView的對象,顯示為一個列表。
??:我不太確定為什么它的名字會叫做表格,因為通常來說表格就像excel那樣擁有多行多列,然而UITableView只有一列。比起表格來,稱呼它為列表更加合適,我想我們是無法理解這個名字了。UIKit還提供了一個叫做UICollectionView的對象,它的和UITableView有點類似,但是可以擁有多列。
table view有兩種風格,一種是“plain(無格式)”,另一種是“grouped(分組)”。它們大致相同,只有些小區別。最明顯的一個卻別就是“grouped”模式中,若干行會被會被放進一個淺灰色背景的盒子里分組管理。
plain模式經常被用作容納相同事物的列表,比如通訊錄,或是地址薄,每一行上面都是一個人名或者地址。
grouped模式經常被作來容納不同事物的列表,比如通信錄中的多種屬性,姓、名、座機號碼、手機號碼等。
在我們的Checklists app中,你會同時用到這兩種模式。
table的數據就是每一行中的內容。在Checklists最初的版本里,每一行里會有一個待辦事項,你可以檢查它們是否已經完成。
理論上你可以擁有任意多的行數,比如數千行,雖然并不推薦這樣做。如果你讓你的用戶向下滑動數千行去尋找一個他需要的項目,好無疑問就是秒刪app的節奏。
table在一中叫做cells(細胞、單元格)中展現數據。一個cell關聯一行,單并不總是這樣。一個cell也是一種視圖(view),它在某一行可見時可以展現一行數據。如果你的屏幕大小只能同時容納10行,那么你就只有10個cell,那怕你一共有數千行數據。cell是復用的,它只展示哪些可見的行的數據。
無論何時,當某一行滾動出屏幕不可見時,它的cell就會被一個新的滾動至屏幕上可見的行重新利用。聽起來有點繞,我們看下圖示:
在過去,你需要付出巨大的努力去為你的table創建cell,但是現在,Xcode有一種非常便利的叫做prototype cells的功能,使你可以在界面建造器中可視化的設計你的cell。
打開storyboard,點擊空的cell選定它。
很多時候難以分辨你到底有沒有選中它,這時你可以看看綱要面板上被選中的是不是一個叫Table View Cell的東西被選中了,或者你也可以從綱要面板上直接選擇它,就像上圖中的一樣。
從對象庫里拖拽一個label到cell上。確保將label的兩側拉伸到cell的邊緣,僅留下一點點空白。
除了這個label以外,你還需要添加一個對勾符號到cell中。這個對勾符號由一種叫accessory(配件)的東西提供,一個出現在cell右側的內建的視圖。你可以從幾種標準配件中選擇,或者也可以使用你自定義的。
再次選定Table View Cell。打開屬性檢查器設置Accessory字段為Checkmark:
如果你看不到這個選項,確定你選擇的是Table View Cell而不是其他東西。
這時你的cell看起來是這個樣子:
你需要重新調整一下label的右側邊界,使它不要和這個對勾符號重疊。
你還需要設置cell的重用標示(reuse identifier)。這是當舊的行滾動出屏幕,新的行混動入屏幕并且可見時,table view尋找空閑的cell,并重用它們時使用的內部名稱。
table需要為這些新的行分配cell,此時循環利用存在的cell比重新創建它們要效率快的多。這個技術保證了table view始終可以平滑的滾動。
reuse identifier對你在同一個表里使用不同的cell也是非常重要的。例如,一個cell可能包含圖片和標簽,而另一個cell可能包含標簽和按鈕。你必須給這兩個cell不同的名稱,才能正確的調用它們。
雖然Checklists只包含一種類型的cell,但是你必須還是要給它一個名稱。
還是選中Table View Cell在屬性檢查器中找到Identifier字段,并且鍵入ChecklistItem作為它的重用標示。
運行app,激動嗎,然并卵,你依然看到的是空白的表格。
你僅僅是對表格的cell進行了設計,并不是實際的行。記得嗎,cell僅顯示可見行,cell自己并不是實際的數據。要添加數據到table中,你需要寫點代碼了。
數據來源
打開ChecklistViewController.swift并且添加以下方法到最底部的花括號前面。
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ChecklistItem", for: indexPath)
return cell
}
這些方法比你曾經在BullsEye中見到的方法要復雜的多,它們都各有兩個參數,并且會給調用者返回一個值。除此以外,它們的工作模式和你之前處理過的一模一樣。
這兩個特定的方法是UITableView的data source protocol(數據來源協議)的一部分。
data source是你的數據和table view之間的連接。通常view controller扮演著data source的角色,因此必須執行這些方法。
table view必須知道你一共有多少行數據以及如何展現每一行。但是你不能簡單點將數據倒進去。你不能對它說:“親愛的table view,這里是我的100行數據,你給我把它們放到屏幕里”
取而代之的是,你需要這樣告訴table view:“這個view controller現在是你的數據源(data source)了。你可以在任何你想要的時候向它詢問任何有關數據的事情”
一旦它連接到了數據源—就是你的view controller—table view會發送“ numberOfRowsInSection”信息去尋找那里到底有多少行數據。
當table view需要將具體的某一行顯示到屏幕上時,它會發送一個“cellForRowAt”信息,去為cell匹配數據源。
你每時每刻都可以在iOS中見到這種模式:一個對象委托另一個對象做了某些事情。在我們這個情況中,當table view需要數據源的時候,ChecklistViewController負責這一工作。
你實現的第一個方法tableView(numberOfRowsInSection),返回了一個值1。這樣做可以告訴table view你僅有一行數據。
return語句在swift中十分重要。它使一個方法可以返回數據給它的調用者(caller)。在tableView(numberOfRowsInSection)中,調用者是UITableView的對象,它想知道表中有多少行數據。
一個方法中的語句通常使用實例變量和從參數中接受到的其他數據進行一些計算。當這個方法結束時,return語句的作用就是告訴你:“嗨,我做完了,這是你要的結果”。這個返回值通常被稱作方法的結果(result of the method)。
對于tableView(numberOfRowsInSection)來說,它的回答相當簡單:“這里只有一行,所以我返回1”
現在table view知道了只有一行數據,它開始調用你添加的第二個方法—tableView(cellForRowAt)—來獲得為這一行準備好的cell。這個方法抓取一份prototype cell的拷貝,并且使用一個return語句將它給回到table view。
在tableView(cellForRowAt)中,我們經常做的事情就是把行的數據(row data)放入cell中,但是app仍然沒有任何數據。
運行app,你會看到一個cell,然后啥也沒有:
注意一下,iPhone的狀態欄有部分和table view重疊了。狀態欄沒有屬于劃分給自己的獨立的空間,僅僅是被簡單的放到屏幕的最頂端。稍后,我們會通過放置一個導航欄(navigation bar)到table view的頂部,來解決這個小小的整容問題。
練習:修改app,使它可以顯示5行。
這完全沒多難:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
如果你視圖到storyboard中去復制5個prototype cell出來,那么你完全沒明白我們上面講的內容。
當你使用tableView(numberOfRowsInSection) 返回5時,你就告訴來table view,這里有5行。
然后table view會發送5次“cellForRowAt”消息,每一次請求一個cell。而tableView(cellForRowAt) 當前僅是返回prototype cell的拷貝,所以你會看到5行一模一樣的東西:
在tableView(cellForRowAt) 中創建cell 的方法有許多種,到目前為止你用的是最簡單的一種:
1、在storyboard中為table view添加一個prototype cell;
2、為這個prototype cell設置一個重用標示(reuse identifier);
3、調用tableView.dequeueReusableCell(withIdentifier)。如果必要或者一個存在的cell不再被使用了,回收掉的時候,它會創建一個新的prototype cell的拷貝。
一旦你有了一個cell,你應該從相應的行拿出數據講它填滿并且給回到table view。這時我們下一小節要做的事情。
將行數據放入cell
目前所有行(相當于cell)都包含一個預置的“Label”。讓我們給每一行放置不同的文本。
打開storyboard并且選擇cell中的label。然后打開label的屬性檢查器,將Tag字段設置為1000.
tag(標簽)就是用戶接口控件的一個數字ID,用于標示它的身份,以便將來很容易的可以找到它。為什么是1000呢?其實沒啥特別的理由。它可以是除了0以外的任何數字,因為0是所有tag的默認值。
再次確認一下,你是對Label的tag做的設置,不要點錯設置到Table View Cell或者Content View上去了。選錯地方是經常出現的一個錯誤,它會帶來你意料之外的結果。
打開ChecklistViewController.swift,改變tableView(cellForRowAt)為下面這樣:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ChecklistItem", for: indexPath)
let label = cell.viewWithTag(1000) as! UILabel
if indexPath.row == 0 {
label.text = "Walk the dog"
} else if indexPath.row == 1 {
label.text = "Brush my teeth"
} else if indexPath.row == 2 {
label.text = "Learn iOS development"
} else if indexPath.row == 3 {
label.text = "Soccer practice"
} else if indexPath.row == 4 {
label.text = "Eat ice cream"
}
return cell
}
第一行是之前就有的。它用來獲取一個prototype cell的拷貝,一個新的或者是回收再用的,并且將它放入局部常量cell中。
let cell = tableView.dequeueReusableCell(withIdentifier: "ChecklistItem", for: indexPath)
(回憶一下,cell之所以是個常量,是因為它是用let定義的,不是var。又因為它是在方法內部定義的,所以它是局部的)
那么indexPath又是什么呢?
indexPath是一個指向表中具體某一行的一個簡單的對象。當table view為cell請求數據源時,你可以通過這一行的indexPath.row屬性的內部行號,就可以找到是哪個cell需要數據了。
??:還存在一種情況就是,表格會將若干行分組到一個段里面。比如在通信錄app中,會根據姓名對行進行分組。所有以A開頭的姓名為一個分組,所有以B開頭的姓名為一個分組,等等。
要找出某一行是屬于那一個段的,需要用到indexPath.section屬性。我們的Checklists app不需要這種分組,所以你可以忽略indexPath.section屬性。
你添加的新的代碼中,第一行是:
let label = cell.viewWithTag(1000) as! UILabel
這里你向table view cell請求標示為1000的視圖。這是你剛剛在storyboard中給label設置的標示,所以它會返回一個相應UILabel對象。
在引用UI元素時,使用tag是非常便利的,不需要再弄一個@IBOutlet變量。
練習:為什么你不能直接使用@IBOutlet變量然后直接在storyboard中將view controller和cell中的label連接起來?畢竟,你在BullsEye中就是這樣創建label的引用的,所以為什么這里就不可以呢?
答案:表格中肯定會有一個以上的cell,每個cell都會自己的label。如果你從prototype cell中連接一個label的outlet到view controller,那么這個outlet只能引用其中一個cell中的label,而不能全部引用。自從label屬于cell,而不是view controller中的一個整體開始,你就不能再用在view controller上創建outlet這種做法了。暈了嗎?現在別太操心這件事。
回到代碼。接下來的代碼不應該對你太陌生:
if indexPath.row == 0 {
label.text = "Walk the dog"
} else if indexPath.row == 1 {
label.text = "Brush my teeth"
} else if indexPath.row == 2 {
label.text = "Learn iOS development"
} else if indexPath.row == 3 {
label.text = "Soccer practice"
} else if indexPath.row == 4 {
label.text = "Eat ice cream"
}
你之前見過這種if-else if結構。它只是通過查看包含行號的indexPath.row的值,來改變相應行的label的文本。第一行的cell獲取文本“Walk the dog”,第二行的cell獲取文本“Brush my teeth”,以此類推。
??:任何設計計數的時候,電腦一般都從0開始計數。如果你的列表中有4行,那么它們的行號就分別為0,1,2,3。和我們平時的習慣不太一樣,但是程序通常都這樣運行。
所以第一行的indexPath.row是0,第二行的是1,以此類推。
從0開始計數,對你一開始可能不太適應,但是久而久之它會變成你的本能。
運行app,現在這5行,每一行都有屬于自己的文本了:
這就是使用tableView(cellForRowAt)方法向表格提供數據的方法。你首先得到一個UITableViewCell對象,然后通過indexPath.row改變其中cell的內容。
僅僅是為了好玩,讓我們放入100行試試。
if indexPath.row % 5 == 0 {
label.text = "Walk the dog"
} else if indexPath.row % 5 == 1 {
label.text = "Brush my teeth"
} else if indexPath.row % 5 == 2 {
label.text = "Learn iOS development"
} else if indexPath.row % 5 == 3 {
label.text = "Soccer practice"
} else if indexPath.row % 5 == 4 {
label.text = "Eat ice cream"
}
記得把下面改為return 100
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 100
}
這里你使用了一種叫做求余的運算,就是%這個操作符,來決定行號。(這種運算也叫做取模運算)
它會返回兩個數相除后的余數。例如:13 % 4 = 1,因為13處以4余1.而12 % 4 = 0,因為沒有余數。
所以第一行及第一行后每個5行都顯示“Walk the dog”,而第二行及第二行后每五行都顯示“Brush my teeth”,以此類推。
我想你已經得到了啟示:讓每五行重復出現一次這個工作,比起你自己在100行中慢慢敲,讓電腦自己計算來的快多了。
如果你沒反應過來的話,就先不要管它,等一段時間回來再看看,再想想。
運行app,現在你應該可以看到這種效果了:
??:在模擬其中向下滾動屏幕,就和在手機不太一樣,你需要按下鼠標然后向下拖動,如果你只是在Mac中向下滑動觸摸板,是不會向下滾動的。
練習:你覺得目前table view使用了幾個cell?
答案:雖然這里有100行,但是屏幕上只能顯示14行。如果你去數屏幕上可見的行的話,你會得到13行這個數字,這里存在一種可能性,當你滾動屏幕時有時下面的一行還沒完全出來,而最頂上一行也沒完全消失,還保持在可見狀態。這樣就多出來了一行,所以屏幕上最多可以顯示14行(iPhone6s,7會多一些行數,但是原理是相同的)。
如果你非常快速的滾動的話,我猜table view會多準備幾個臨時的cell,但是這只是猜的,我并不確定。只是這件事情對我們重要嗎?完全不重要,需要多少個cell,你大可以交給table view自己去搞定,你完全不用操心這個事。你所做的全部事情就是當table view需要一個cell時,給它一個填滿數據的cell到相應的行。
通常cell都比行數要少的多。如果哪個app為每一行都準備一個cell,那么iOS系統的內存很快會被耗盡,特別是表格比較大的時候。因為并不是所有行都在屏幕上同時可見,所以為每一行準備一個cell是極大的浪費,并且會使系統很慢。iOS是非常善于持家的,它隨時會在需要的時候對cell進行循環利用。
你現在知道了為什么UITableView可以使每一行都看起來不同,那是因為它們的數據不同,你有很多不同的數據,并且你有cell,可以使數據顯示在屏幕上,雖然cell只有十幾個,但是它們可以循環利用。
(作者在此處自創了一首歌來歌頌cell,應該是根據圣誕歌改編的,說實話,慘不忍睹T T)
奇怪的報錯?
在我們這個課程中,大家最多的問題就是:“我都是照著你說的做的,但是為什么突然我的app崩潰了,神馬鬼?”
如果你也遇到了這個情況,請確認你是不是無意中添加了一個斷點(breakpoint)。斷點是一種聯調工具,它可以使你的app在你指定的一行中斷,并且跳轉到Xcode的調試器。它們的出現方式和app崩潰差不多,但其實只是你的app暫停運行了而已。
斷點的樣子是一個藍色的箭頭,位于代碼編輯窗口最左邊的邊緣:
如果你的app突然崩潰,并且正好在代碼編輯窗口的左邊有哪樣一個藍色的箭頭,那么你可以點擊那個箭頭,然后往別的地方拖拽,隨著一個消失的小氣泡動畫,這個箭頭就沒有了(添加斷點的話,僅僅是在代碼窗口的左邊緣點擊一下,就可以添加一個斷點了,這也是為什么那么容易錯誤的添加斷點的原因)。
順便說一下本書官方論壇的地址:forums.raywenderlich.com,不過上面都是外國人。
點擊每一行
當你點擊屏幕上的任意一行的時候,它會變成淺灰色表明已被選中。但是你手指離開時,它仍然是淺灰色的被選中狀態。我們要對這里做些優化,當你點擊每一行時,可以顯示或者關閉一個對勾符號。
點擊每一行會發生什么,由table veiw的委托(delegate)處理。還記得我之前說過的在iOS系統中你經常會發現一個對象委托另一個對象去做一些事情嗎?數據源(data source)就是一個這樣的例子,table view也有一些依賴于其他人的幫助,那就是table view delegate。
委托的概念在iOS中非常普遍。一個對象經常會依賴于另一個對象去替它完成某些任務。這種獨立關注點的做法使系統保持簡單,因為每一個對象只做自己擅長的事情,除此以外的事情就由別的對象去處理。table view就是一個極好的例子。
因為每一個app都對自己的數據有著不同的需求,這就要table view必須能夠處理各種不同類型的數據。比起將table view復雜化或者讓你自己去修改table view,UIKit采用的方案是選擇一個委托,讓另一個對象去填滿cell的數據,這就是我們的data source(數據源)。
table view本身并不關心數據源是誰以及你的app要處理什么樣的數據,它僅僅是發送 cellForRowAt消息并且接受一個返回的cell。table view組件通過這種將處理數據的責任交給其他對象的方式來保持自身的簡潔。
同樣的,table view知道如何識別用戶點擊了某一行,但是之后對用戶做出何種響應,則由其他對象完成,視每個app具體而定。在我們這個app里你要顯示或者關閉一個對勾符號;其他的app則會做各自完全不同的事。
通常組件只需要一個委托,但是table view把它的委托分裂為兩個獨立的部分:UITableViewDataSource用于把數據放進表格,UITableViewDelegate用于執行點擊每一行后執行的任務。
為了了解這些,我們需要打開storyboard并且按住ctrl點擊table view查看它的連接:
你可以看到table view的data source和delegate都連接到了view controller上。這是UITableViewController的標準慣例。(你可以以這種基本方式使用table view,但是后期我們會手動連接source和delegate到別到地方)
在ChecklistViewController.swift中添加以下方法:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
這個tableView(didSelectRowAt)方法是tableView的delegate method(委托方法)的一種,當用戶點擊某一行的時候被調用。運行app,并且點擊某一行,當你點擊的時候會變成淺灰色,而當你手指離開后會恢復原狀。
我們現在來使tableView(didSelectRowAt)控制顯示或者關閉對勾符號,將這個方法改變成下面這個樣子:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
if cell.accessoryType == .none {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
}
tableView.deselectRow(at: indexPath, animated: true)
}
這個對勾符號是屬于cell的一部分(accessory(配件)屬性,記得嗎?),所以首先你需要找到被點擊的那一行的UITableViewCell對象。你只需要簡單的問table view:“你給我的cell的indexPath是多少?。俊?/p>
因為理論上存在指定的index-path上沒有cell的情況,例如那些不可見的行,所以我們需要使用if let語句。
if let可以告訴swift你僅希望在確實存在一個UITableViewCell對象時執行剩余的代碼。在我們這個app里,其實不存在沒有cell的情況,只是考慮到存在我們未知的情況,才用if let加個保險。
當確定了一個UITableViewCell對象時,你會去查看它的accessory(配件),你可以使用accessoryType成員去查看它。如果配件不存在,則將配件設置為對勾符號;如果已經有一個對勾符號了,則將它設置為空。
??:使用tableView.cellForRow(at)去尋找cell。
tableView.cellForRow(at)和我們之前添加的tableView(cellForRowAt)數據源方法是不一樣的,認識到這一點非常重要。
除去名稱比較相似以外,它們是來自不同對象的不同方法。
數據源方法tableView(cellForRowAt)的目的是當某一行可見時,傳遞一個新的(或者重新利用的)一個cell對象到table view。你從不會手動調用這個方法;只有UITableView可以調用它的數據源方法。
tableView.cellForRow(at)的目的同樣是返回一個cell對象,但是是一個正在顯示的某一行中的一個已存在的cell。它不會創建一個新的cell。如果這一行目前還沒有cell,那么它會返回一個空值nil,意思是沒有找到cell。(你使用if let的目的就是為了避免返回空值時導致app崩潰)
還記得我說的方法必須有一個清晰描述的名稱嗎?UIKit在這方面做的很好,但是在這里我們遇到了兩個名稱非常相似的方法,這可能會導致我們迷惑不解。注意這個陷阱!
運行app看看效果,你應該已經可以控制每一行上的對勾顯示與否了。
??:如果對勾符號不是立即顯示或者關閉,而是當你點擊另外一行時才做出反應,那么你需要確定我們剛才添加的方法是叫做tableView(didSelectRowAt),而不是tableView(didDeselectRowAt),Xcode的自動匹配功能有時會導致你選擇錯誤的方法。
點擊一行,使對勾關閉顯示,然后滾動屏幕使這一行消失,在滾動回來(非??焖俚臐L動,否則看不到這個效果),你會發現這個對勾符號又重新顯示出來了。除此以外,有些行上的對勾還莫名其妙的沒有了,這是神馬鬼?
繼續我們cell和row的故事:你控制的是cell上的對勾符號,也就是cell的配件,但是這個cell也許在你滾動的時候會重新分配給其他行,而原先那一行又得到了一個新的cell。所以,是否顯示這個對勾符號應該是根據row來確定,而不是根據cell確定。
你需要一些方法跟蹤對勾符號在每一行上的狀態,來替代使用cell去判斷是否顯示對勾符號。這意味著我們需要擴展數據源,并且使用合適的數據模型,這是我們下一小節的內容。
??:方法會有多個參數
你在BullsEye中使用到的大多數方法都只有一個參數,或者根本沒有參數,但是這些table view的data soource和delegate方法都有兩個參數:
override func tableView(
_ tableView: UITableView, //參數1
numberOfRowsInSection section: Int) //參數2
-> Int { //返回值
...
}
override func tableView(
_ tableView: UITableView, //參數1
cellForRowAt indexPath: IndexPath) //參數2
-> UITableViewCell { //返回值
...
}
override func tableView(
_ tableView: UITableView, //參數1
didSelectRowAt indexPath: IndexPath) { //參數2
...
}
第一個參數是調用這些方法的UITableView對象。這樣做帶來了便利,你不在需要弄一個@IBOutlet來向table view回傳消息。
對numberOfRowsInSection而言第二個參數是分組數。對cellForRowAt和didSelectRowAt第二參數是index-path。
方法并不僅限于兩個參數,它們可以有許多個參數。但是實際而言兩個或者三個參數已經夠用了,并且如果大多數方法如果都有五個以上參數的話,你早就被嚇跑了。
在其他一些語言里典型的方法也許會是下面這個樣子:
Int numberOfRowsInSection(UITableView tableView,Int section) {
... }
但是swift中有些不一樣,與iOS框架兼容的主要的東西都是用Object-C語言寫成的。
讓我們再看一眼numberOfRowsInSection:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
...
}
這個方法正式的全名叫做tableView(numberOfRowsInSection)。如果你把它讀出來,應該會有點感覺。(中文讀出來就是“表格中每一組的行數”)。它請求具體的某個列表中的具體某一分組中的行數。
其中第一個參數是這個樣子的:
_ tableView: UITableView
參數的名稱是tableView。冒號后面跟著的是參數的類型。過會我會告訴你這個下劃線是什么。
第二個參數是這個樣子的:
numberOfRowsInSection section: Int
這個參數有兩個名字“numberOfRowsInSection”和“ section”。
第一個名字numberOfRowsInSection是在調用這個方法的時候使用。這個名字就是所謂的參數的外部名稱。而在方法的內部,它使用第二個名字“ section”,就是參數的內部名稱,冒號后面的就是參數的類型,這是一個Int型的參數。
當一個參數不需要外部名稱的時候,我們就用一個下劃線替代它。在Object-C中你經常會看到方法的第一個參數是一個下劃線。像這種第一個參數只有一個名字,而第二個參數有兩個名字的方法很奇怪不是嗎?確實很奇怪。
如果你曾經使用過Object-C的話,不要懷疑,它和其他語言比起來就是很奇怪。但是如果你習慣了的話,其實你會發現它的可讀性還是不錯的。
一些有其他語言編程經驗的人會非常困惑不解,因為它們發現ChecklistViewController.swift中有三個方法,它們的名稱一模一樣都是tableView()。但是swfit不這樣認為,在swift中參數的名稱是方法全名的一部分。所以這三個方法實際上名稱是不同的,它們分別是:
tableView(numberOfRowsInSection)
tableView(cellForRowAt)
tableView(didSelectRowAt)
一些開發者在引用這些方法的時候把下劃線和冒號也包括進去了,但是我們不會這樣做,因為那樣太難讀了:
tableView(_: numberOfRowsInSection)
tableView(_: cellForRowAt)
tableView(_: didSelectRowAt)
順便說一下,方法的返回值類型就是跟在 -> 符號之后的。如果沒有這個符號,比如說tableView(didSelectRowAt),那就是說這個方法沒有返回值。
這里的新內容有點多,我希望你還能跟上我的思路。如果你掉隊了的話,就停下來休息一會,然后從頭再來一遍。畢竟你剛被排山倒海而來的新概念洗禮了一遍,迷惑是正常的。
但是千萬別害怕,目前有所疑問是正常的。只要你搞清楚我們要做的事情的大方向,就ok了。