Start Developing iOS Apps (Swift)->創建表視圖(二)

連接Table Cell用戶界面到代碼

在你能夠在table view cell中顯示動態數據之前,你需要創建outlet來連接storyboard中的屬性和在MealTableViewCell.swift文件中代表table view cell的代碼。

連接這些視圖到MealTableViewCell.swift代碼

  1. 在storyboard中,選擇table view cell中的label。
  2. 打開助理編輯器。


    image: ../Art/assistant_editor_toggle_2x.png
  3. 如有必要,盡可能擴展工作區空間。


    image: ../Art/navigator_utilities_toggle_on_2x.png
  4. 在編輯器選擇器欄中,它顯示在助理編輯器的頂部,把助理視圖從預覽視圖切換到切換到Automatic > MealTableViewCell.swift.


    image: ../Art/CTV_assistant_switchtocode_2x.png

    MealTableViewCell.swift顯示在右側的編輯器中。

  5. 在MealTableViewCell.swift中,找到class行:
class MealTableViewCell: UITableViewCell {
  1. 在class行的下面,添加下面注釋:
//MARK: Properties
  1. 按住Control鍵,從畫布中拖拽label到右側編輯器顯示的代碼中,在剛才添加的注釋的下面釋放。


    image: ../Art/CTV_label_dragoutlet_2x.png
  2. 在彈出的對話框中,Name字段鍵入nameLabel。
    讓其他選項保持原樣。你的對話框看起來是這樣的。


    image: ../Art/CTV_label_addoutlet_2x.png
    image: ../Art/CTV_label_addoutlet_2x.png
  3. 點擊Connect。
  4. 在storyboard中,選擇table view cell的image view。
  5. 按住Control鍵,從畫布中拖拽image view到右側編輯器顯示的代碼中,在剛才添加的nameLabel屬性下面釋放。


    image: ../Art/CTV_imageview_dragoutlet_2x.png
  6. 在彈出的對話框中,Name字段鍵入photoImageView。
    讓其他選項保持原樣。并點擊Connect。


    image: ../Art/CTV_imageview_addoutlet_2x.png
  7. 在storyboard中,選擇table view cell的rating控件。
  8. 按住Control鍵,從畫布中拖拽rating控件到右側編輯器顯示的代碼中,在剛才添加的photoImageView屬性下面釋放。


    image: ../Art/CTV_ratingcontrol_dragoutlet_2x.png
    image: ../Art/CTV_ratingcontrol_dragoutlet_2x.png
  9. 在彈出的對話框中,Name字段鍵入ratingControl。
    讓其他選項保持原樣。并點擊Connect。


    image: ../Art/CTV_ratingcontrol_addoutlet_2x.png

在MealTableViewCell.swift中你的outlet看上去應該是這樣的:

        @IBOutlet weak var nameLabel: UILabel!
        @IBOutlet weak var photoImageView: UIImageView!
        @IBOutlet weak var ratingControl: RatingControl!

加載初始數據

為了在你的table cell中顯示真實數據,你需要編寫代碼來加載這些數據。在這點上,你已經有菜品的數據模型了:Meal類。你還需要保存這些菜品的一個列表。跟蹤這個數據的最自然的地方是與菜品列表場景連接的自定義視圖控制器子類。這個視圖控制器將管理顯示菜品列表的視圖,并有一個引用指向在用戶界面顯示的內容背后的數據模型。

首先,創建一個自定義的table view controller子類來管理這個菜品列表場景。

創建一個UITableViewController子類

  1. 選擇File > New > File (或者按下 Command-N)。
  2. 在出現的對話框中,選擇iOS,并且選擇Cocoa Touch Class。
  3. 點擊Next。
  4. 在Class字段,鍵入Meal。
  5. 在Subclass of字段,選擇UITableViewController。
    把類標題改為MealTableViewController。
  6. 確保Also create XIB file選項沒有被選中。
    XIB文件是一個舊的通過視圖控制器設計視圖管理的方式。它們早于storyboard出現,基本相當于代表storyboard上的單一視圖。這個視圖控制器不需要一個XIB文件,因為你已經定義它連接應用的storyboard了。
  7. 確保語言是Swift。
  8. 點擊Next。
    保存位置是項目目錄。
    Group選項為默認的FoodTracker。
    在目標區域,你的應用被選擇,而應用的tests沒有被選擇。
  9. 其他的選項保持不變,點擊Create。
    Xcode創建了MealTableViewController.swift,一個定義自定義table view controller子類的源代碼文件。
  10. 必要時,在Project navigator,拖拽MealTableViewController.swift到和其他的Swift文件一起。

在這個自定義類中,你能定義一個屬性來存儲Meal對象的列表。Swift標準(Swift standard library)包含一個被稱為Array(數組)的結構,它能夠很好的跟蹤列表的項。

加載初始數據

  1. 返回標準編輯器。并盡可能的擴展工作區空間。


    image: ../Art/standard_toggle_2x.png
    image: ../Art/standard_toggle_2x.png
  2. 打開MealTableViewController.swift。
  3. 緊跟著class行添加下面的代碼:
//MARK: Properties   
        var meals = [Meal]()

這代碼在MealTableViewController聲明了一個屬性,并用一個默認值初始化它(一個空的元素項為Meal對象數組)。聲明meals為變量而不是常量,意味著你能在初始化它之后還可以給它添加元素項。

  1. Table view controller模版包含很多存根方法以及對于這些方法的注釋。這些占位的實現方法你可以刪除注釋和擴展(讓它們可用)來定義表的外觀和行為。在你設置完成模型數據后你將看到這些方法。現在,滾動到這些方法的下面,在結束花括號的上面添加如下方法:
//MARK: Private Methods 
        private func loadSampleMeals() {
        }

這是一個輔助方法,用來加載樣本數據到應用。

  1. 在loadSampleMeals()方法中,首先加載下面三個菜品圖片:
let photo1 = UIImage(named: "meal1")
        let photo2 = UIImage(named: "meal2")
        let photo3 = UIImage(named: "meal3")

確保圖片在項目中的名字和在代碼中的一致。

  1. 在加載完這些圖片后,創建三個菜品對象。
guard let meal1 = Meal(name: "Caprese Salad", photo: photo1, rating: 4) else {
            fatalError("Unable to instantiate meal1")
        }
         
        guard let meal2 = Meal(name: "Chicken and Potatoes", photo: photo2, rating: 5) else {
            fatalError("Unable to instantiate meal2")
        }
         
        guard let meal3 = Meal(name: "Pasta with Meatballs", photo: photo3, rating: 3) else {
            fatalError("Unable to instantiate meal2")
        }

因為菜品類的 init!(name:, photo:, rating:)初始化器是可失敗的,所以你需要檢查初始化器返回的結果。在本例中,你傳遞的是有效的參數嗎所以初始化器永遠不會失敗。若果初始化器失敗,你的代碼就有了錯誤。為了幫助你標記和修復錯誤,如果初始化器失敗,fatalError()函數就會在控制臺打印一個錯誤消息,并且應用程序終止。

  1. 在創建Meal對象之后,使用如下方法添加它們到meals數組。
    meals += [meal1, meal2, meal3]
  1. 找到 viewDidLoad()方法。模版的實現看上去是這樣的:
override func viewDidLoad() {
            super.viewDidLoad()
            
            // Uncomment the following line to preserve selection between presentations
            // self.clearsSelectionOnViewWillAppear = false
            
            // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
            // self.navigationItem.rightBarButtonItem = self.editButtonItem()
        }

模版實現的這個方法包含注釋,這些注釋是在Xcode創建MealTableViewController.swift的時候插入的。像這樣的代碼注釋在源代碼文件中提供了提示和上下文信息,但是在本課中你用不到它們。

  1. 在 viewDidLoad()方法中,刪除注釋,并在super.viewDidLoad()后面添加如下方法來加載樣本菜品數據。
// Load the sample data.
        loadSampleMeals()

當視圖加載時,這個代碼調用你剛寫的加載樣本數據的輔助方法。你把它分離到自己的方法中,為的是代碼更加模塊化和易讀。

你的viewDidLoad()方法看上去應該是這樣的:

override func viewDidLoad() {
            super.viewDidLoad()
            
            // Load the sample data.
            loadSampleMeals()
        }]

你的loadSampleMeals()方法看上去應該是這樣的:

private func loadSampleMeals() {
            
            let photo1 = UIImage(named: "meal1")
            let photo2 = UIImage(named: "meal2")
            let photo3 = UIImage(named: "meal3")
            
            guard let meal1 = Meal(name: "Caprese Salad", photo: photo1, rating: 4) else {
                fatalError("Unable to instantiate meal1")
            }
            
            guard let meal2 = Meal(name: "Chicken and Potatoes", photo: photo2, rating: 5) else {
                fatalError("Unable to instantiate meal2")
            }
            
            guard let meal3 = Meal(name: "Pasta with Meatballs", photo: photo3, rating: 3) else {
                fatalError("Unable to instantiate meal2")
            }
            
            meals += [meal1, meal2, meal3]
        }

檢查點:通過 Product > Build.選擇構建項目。你的構建應該時沒有錯誤的。注意,這時候,你或許看到一個關于在應用中無法到達View Controller場景的Xcode警告。你將在下一課修復它。在本課剩下來的部分,先忽略它。

重要
如果你運行出現問題,確保項目中的圖片名字是否真的和在代碼中使用的名字一致,

顯示數據

現在,你的自定義table view controller子類,MealTableViewController,有了一個可變數組,它用一些樣本數據預先做了填充。現在你需要在用戶界面上顯示這些數據。

為了顯示動態數據,一個table view需要兩個重要的幫手:數據源(data source)和委托(delegate)。一個table view 數據源,就像它的名字暗示的那樣,提供這個table view要顯示的數據。一個table view委托幫助table view管理cell的選擇、行高、以及與顯示數據相關的其他方面。默認情況下,UITableViewController及其子類采用必要的協議來讓table view controller成為它關聯的表視圖的數據源(UITableViewDataSource協議)和委托(UITableViewDelegate協議)。你的工作就是在table view controller子類中實現合適的協議方法,這樣你的table view就有了正確的行為。

Table view需要實現三個表視圖數據源方法。

        func numberOfSections(in tableView: UITableView) -> Int
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

第一個 numberOfSections(In:)方法,它高速表視圖有多少section(部分)要顯示。section是表視圖內部cell的可視化分組,它在表視圖里有很多數據的時候特別有用。對于簡單的表視圖,比如FoodTracker 應用,你只需要一個部分顯示就可以了,所以實現這個方法很簡單。

在table view 中顯示一個section

  1. 在 MealTableViewController.swift中,找到numberOfSections(In:)數據源方法。模版實現看上去是這樣的:
override func numberOfSections(in tableView: UITableView) -> Int {
            // #warning Incomplete implementation, return the number of sections
            return 0
        }
  1. 把返回值改為1,刪掉警告注釋。
        override func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }

這個代碼讓table view顯示1個部分。你刪掉的注釋說的是#warning Incomplete implementation(警告 沒有完成實現),但你已經實現了,所以就刪掉了。

接下來的數據源方法,tableView(_:numberOfRowsInSection:),高速表視圖一個給定的部分里面有多少行。你的表視圖只有一個部分,并且每個Meal對象都應該有自己的行。這就意味著行數應該是meals數組里Meal對象的數目。

返回table view的行數

  1. 在MealTableViewController.swift中,找到tableView(_:numberOfRowsInSection:)數據源方法。它的模版實現看上去是這樣的:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            // #warning Incomplete implementation, return the number of rows
            return 0
        }

你要返回你有的菜品樹木。Array有一個屬性稱為count,它返回數組中項目的總數,所以行數就是meals.count。

  1. 改變tableView(_:numberOfRowsInSection:)數據源方法返回合適的行樹,移除警告注釋。
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return meals.count
        }

最后一個數據源方法,tableView(_:cellForRowAt:),為給定的行配置并提供一個cell用來顯示。表視圖中的每個行都有一個cell,這個cell決定行中顯示的內容以及這些內容如何布局。

對于只有少量行的表視圖,所有行或許都在屏幕上,所以這個方法調用表中的每一行。但是有很多行的表視圖,在給定的時間里只有小部分能夠顯示在屏幕上。表視圖如果只請求需要被顯示的行的cell就會大大提高了效率,這就是tableView(_:cellForRowAt:)允許表視圖做的。

對于在table view中任何給定的row,你通過獲取在meals數組中的合適的Meal來配置cell,然后用Meal類的合適值來設置cell的屬性。

在table view中配置和顯示cell

  1. 在MealTableViewController.swift,找到tableView(_:cellForRowAt:) 數據源方法并取消注釋。(要取消注釋,只要刪除圍繞它的 /* 和 */字符。)
    完成后這個這個方法看上去是這樣的:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
            
            // Configure the cell...
            
            return cell
        }

dequeueReusableCell(withIdentifier:for:)方法從這個table view中請求一個cell。作為替代在用戶滾動cell的時候創建新cell并刪除舊cell的方式,表格會盡可能的重用cell。如果沒有可用的cell, dequeueReusableCell(withIdentifier:for:)會實例化一個新的;但是如果cell滾動到屏幕的外面,它們會被重用。標識符(identifier)會告訴dequeueReusableCell(withIdentifier:for:)哪個類型的cell要被創建或重用。
為了這段代碼能夠工作,你需要改變在storyboard中的cell(MealTableViewCell)標識符屬性,然后添加代碼來配置cell。

  1. 在這個方法一開始的地方添加下面的代碼:
// Table view cells are reused and should be dequeued using a cell identifier.
        let cellIdentifier = "MealTableViewCell"

這使用storyboard中設置的標識符創建了一個常量。

  1. 使用cellIdentifier變量更新方法中的標識符,像下面這樣:
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
  1. 因為你創建了一一個你想用的自定義cell類,將cell的類型降級到你自定義的cell的子類,MealTableViewCell。
        guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? MealTableViewCell  else {
            fatalError("The dequeued cell is not an instance of MealTableViewCell.")
        }

這段代碼有很多事要做:

  • as? MealTableViewCell表達式試圖把返回對象從UITableViewCell類降級到MealTableViewCell類。這個返回值是一個可選值(optional)。
  • guard let表達式會安全解包這個可選值。
  • 如果你的storyboard設置正確,并且cellIdentifier匹配storyboard的標識符,那么降級處理將不會失敗。如果降級失敗,fatalError()函數就會在控制臺打印錯誤信息,并終止應用。
  1. 在guard語句后面,添加下面的代碼:
// Fetches the appropriate meal for the data source layout.
        let meal = meals[indexPath.row]

這個代碼從meals數組獲取合適的菜品對象。

  1. 現在,使用meal對象來配置你的cell。用下面的代碼來替換// Configure the cell注釋。
        cell.nameLabel.text = meal.name
        cell.photoImageView.image = meal.photo
        cell.ratingControl.rating = meal.rating

這個代碼為每個在table view cell中的視圖設置合適的數據,這些數據來自meal對象。

你的tableView(_:cellForRowAt:)方法看起來是這樣的:

        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            
            // Table view cells are reused and should be dequeued using a cell identifier.
            let cellIdentifier = "MealTableViewCell"
            
            guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? MealTableViewCell  else {
                fatalError("The dequeued cell is not an instance of MealTableViewCell.")
            }
            
            // Fetches the appropriate meal for the data source layout.
            let meal = meals[indexPath.row]
            
            cell.nameLabel.text = meal.name
            cell.photoImageView.image = meal.photo
            cell.ratingControl.rating = meal.rating
            
            return cell
        }

在用戶界面顯示數據的最后一步是把MealTableViewController.swift中定義代碼連接到菜品列表場景。

將Table View Controller指向MealTableViewController.swift

  1. 打開storyboard。
  2. 通過點擊在場景dock直到整個場景有了一個藍色的輪廓來選擇table view controller。


    image: ../Art/CTV_scenedock_table_2x.png
  3. 打開Identity inspector。
  4. 在Identity inspector中,找到Class字段,并選擇MealTableViewController。


    image: ../Art/CTV_inspector_identity_tablevc_2x.png

檢查點:運行應用。你在 viewDidLoad()方法中添加的項目列表應該顯示在table view的cell上了。你可能注意到在table view cell和狀態欄之間有一個小的重疊——你將在下一課修復它。

image: ../Art/CTV_sim_finalUI_2x.png

為導航準備菜品詳情場景

當你準備在FoodTracker應用中實現導航,你需要刪除一些占位的代碼和你不再需要的的用戶界面。

清除項目不使用的部分

  1. 打開storyboard并查看菜品詳情場景。
    你的菜品詳情場景的用戶界面看上去是這樣的:


    image: ../Art/CTV_mealsceneUI_old_2x.png
  2. 在這個場景中,選擇Meal Name 標簽(label),并按下刪除鍵刪除它。
    在棧視圖中的其他元素會自動重定位。


    image: ../Art/CTV_mealsceneUI_new_2x.png
  3. 打開ViewController.swift。
  4. 在ViewController.swift中,找到textFieldDidEndEditing(_:)方法。
        func textFieldDidEndEditing(_ textField: UITextField) {
            mealNameLabel.text = textField.text
        }
  1. 刪除設置label的text屬性的行。
        mealNameLabel.text = textField.text

你將很快使用新的實現來替換它。

  1. 在ViewController.swift,找到mealNameLabel的outlet,并刪除它。
@IBOutlet weak var mealNameLabel: UILabel!

因為現在你有兩個視圖控制器在項目中,需要給ViewController.swift一個更加有意義的名字。

重命名ViewController.swift文件

  1. 在project navigator,點擊 ViewController.swift文件一次,并按下回車鍵。
    Xcode會允許你為這個文件輸入一個新名字。
  2. 重命名為MealViewController.swift,按下回車鍵。
  3. 在MealViewController.swift中,找到類聲明行
        class ViewController: UIViewController, UITextFieldDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
  1. 改變類名為MealViewController
        class MealViewController: UIViewController, UITextFieldDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
  1. 在文件頂部的注釋中,也把名字從ViewController.swift 改為MealViewController.swift。
  2. 打開storyboard。
  3. 通過點擊它的場景dock選擇視圖控制器。


    image: ../Art/CTV_scenedock_mealscene_2x.png
  4. 選中這個視圖控制器,打開Identity inspector。
  5. 在Identity inspector,在Class字段把ViewController改為MealViewController。


    image: ../Art/CTV_mealscenefinal_2x.png

檢查點:構建或運行應用。一切應該如常。

小結

在本課中,你構建了一個自定義的table view cell。你把模型對象附加到了table view controller。你給模型添加了樣本數據,并且你實現使用模型數據動態的填充表格所需的表視圖控制器代碼。

下一課,你將添加在表視圖和菜品視圖之間導航的功能。

注意
想看本課的完整代碼,下載這個文件并在Xcode中打開。
下載文件

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容