#34 Imitate Chat APP by using table view (with cell Auto Layout, chat bubble and MVC design pattern)

先前不管用 static cells 或 dynamic prototypes 時,都是直接從元件庫拉 Table View Controller 來使用,這樣的好處是 Apple 已經幫你設定好 data source 和 delegate 了,但其實也可以拉 Table View 和 Table View Cell 到 View Controller ,再自己設定 data source 和 delegate。今天就來試試另一個方式吧~

這次擷取了機智醫生生活中的片段對話,當作模仿聊天 APP 畫面中的聊天內容,同時使用 cell 的 Auto Layout 和 MVC 架構來練習。第一次使用 MVC design pattern ,有任何指教或是使用有誤的地方也歡迎一起討論喔~先來看看動畫~

在文章開始之前,先感謝彼得潘以及學長姐的攻略文,可以直接參考底下連結,都有非常詳盡的說明喔~

Target

  1. 熟練 Table View 和 Table View Cell 的 delegate and data source 設定。
  2. 練習 cell 的 Auto Layout。
  3. 練習 MVC design pattern。
  4. 練習使用 UIBezierPath 客製圖型。

Process

Step 1: Add component from library and set Auto Layout

先把會使用到的元件加到 Main.storyboard,包含設定背景的 Image View, Table View 和兩個 Table View Cell,Auto Layout 設定的部分截圖放大如下,給大家當作參考。

Step 2: Customize TableViewCell and create XIB file

再來就是客製化 Cell ,因為我們有兩個 prototypes ,所以先建立兩個 Cocoa Touch Class ,這次比較不一樣的地方是,我們要同步新增 XIB file,把客製化 cell 的動作從 Main.storyboard 移到新的 Xib file。

這樣的好處是如果畫面複雜, 每個 team member 就可以分工,各自完成自己負責的部分。

這次會分兩個 prototypes 的主要原因是分成自己傳出去的訊息靠右,其他人傳回來的訊息靠左。

自己傳出去的訊息,大頭貼、姓名、訊息都靠右
其他人傳回來的訊息,大頭貼、姓名、訊息都靠左

這裡的重點我紀錄一下,總共有三點:

  1. 由於我們要客製化對話框,所以會將 Text View 加進一個 View 裡面當 Child.
  2. Text View 要隨著文字多寡自動變換高度,所以要把 Attributes -> Scroll View -> Scrolling 的 Scrolling Enabled 取消勾選。
  3. 由於要隨著文字多寡改變寬跟高,所以在 Auto Layout 設定時, Container View 距離右和下(Other People Container View) 或是左和下(Me Container View) 的邊界不能寫死,要用大於等於或小於等於。

我把兩個 cell 的 Auto Layout 設定放大截圖,給大家作參考。

自己傳訊息的 cell 的 Auto Layout 設定
其他人傳訊息的 cell 的 Auto Layout 設定

Step 3: Add IBOutlet and Customize chat bubble

再來就是這次實作最麻煩的地方了,首先我必須再次感謝各位前輩,尤其底下這兩張 Yuwen Chiu 學姊的圖,真的讓我省了很多時間。有了這些座標的示意圖之後,再用 UIBezierPath 設定路徑會讓頭腦清楚很多~

提醒一下,cell 的 IBOutlet 要拉到 cell 各自的 Class 裡面,不是拉到 View Controller Class 喔。

自己的訊息框,取自Yuwen Chiu — #63 情歌對唱的聊天室畫面:Can You Feel The Love Tonight
其他人的訊息框,取自Yuwen Chiu — #63 情歌對唱的聊天室畫面:Can You Feel The Love Tonight

我把程式碼貼在下方供大家參閱。

Step 4: Register Xib file to tableView and assign delegate and data source

override func viewDidLoad() {super.viewDidLoad()let meTableViewCellXib = UINib(nibName: "\(MeTableViewCell.self)", bundle: nil)tableView.register(meTableViewCellXib, forCellReuseIdentifier: "\(MeTableViewCell.self)")let otherPeopleTableCellXib = UINib(nibName: "\(OtherPeopleTableViewCell.self)", bundle: nil)tableView.register(otherPeopleTableCellXib, forCellReuseIdentifier: "\(OtherPeopleTableViewCell.self)")tableView.delegate = selftableView.dataSource = self}

由於我們不是在 Main.storyboard 上直接設計,而是另外新增 Xib file,所以要回到 Main.storyboard 的 viewDidLoad 裡面把 Xib file register,方法如上。

在執行這件事之前,記得要添加 Identifier 到兩個 cell,在 register 時才可以打,也可以跟我一樣將 Identifier 設定成跟檔名一樣,就可以用 .self 來取值,比較不會打錯字。

Step 5: Create data

到這裡 Xcode 應該會提示你應該要 follow protocol 來修正錯誤,不過沒關係先等等,我們先把我們等等要 Show 在表格裡面的資訊完成。這邊我會建立一個新的 Swift file 來寫,而不會全部都寫在 Class View Controller 裡面,造成 code 非常龐大,難以維護。

新建完 Swift file 後,就可以開始 Key 資料啦,這邊我用了 enum 來寫,為什麼用 enum 來寫呢,因為這次的主題裡面有韓文,我總不能每次在打他們的名字的時候都用複製貼上的(因為我不會韓文 (〒︿〒)),太麻煩了。

用 enum 的好處就是,我可以定義每一個 case 的值,之後我可以用選的就好不用每次都手動去複製貼上,最後再用 rowValue 去取值即可。

Step 6: Follow protocol

還記得先前提到彼得說的,要指定代理人時,所需要執行的三大口訣嗎?來複習一下~

  1. 定義型別 A 遵從 protocol. (請 A Follow 被代理人的原則)
  2. 在型別 A 裡定義 protocol 的 function. (被代理人要求代理人要做什麼事)
  3. 指定 data source or delegate 是型別 A 生成的東⻄,也就是指定型別 A 的東⻄是執行 function 的人. (指定誰來當代理人)

我們目前做了第二件事情,但一跟三還沒做,所以當你到這個步驟時 Xcode 應該會提醒你修正。

但為了使程式更容易識讀,可以不用全部都寫在同一個 class 裡面,我們可以另開一個新的 swift file 或是再原本的 VierController 裡利用 Extension 寫在 class ViewController 之外。

extension ViewController: UITableViewDelegate, UITableViewDataSource {}

好,一也完成了,剩下第三件事情。

當你到這一步時,Xcode 會自動提醒你,應該要加入哪些必要的 func 點選提示中的 fix ,就會自動幫你修正了。

這次除了 1 跟 2 必要的兩個 func 必須寫以外,還會用到第三個 func,下面一一紀錄。

1. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {}

這個就是大家熟悉的,一個 section 裡面要有幾列,通常就是看你的 Array 裡面有多少資料,就回傳多少,所以我會如下這樣回傳。

return cellInfos.count

PS. cellInfos 就是我的 Array

2. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {}

這裡就是主要判斷該回傳哪一種 cell 的地方,仔細看 demo 的 gif,可以發現除了區分自己跟他人之外,在他人訊息中還會用顏色來區分性別,所以可想而知我的 Array 裡面應該會有性別的選項,我把完整程式碼貼在下方供大家參考囉~

3. func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {}

第三個 func 雖然對於 protocol 來說不是必要 func,但由於我們要在顯示資料給 end user 之前,根據文字的多寡調整我們 chat bubble 的長度跟高度,所以在『顯示之前』我們要『重新提供一次座標』,來『繪製 chat bubble』

注意到我特別粗體字的三個重點,這三個重點分別帶別我要呼叫的三個 func

顯示之前: 也就是這次的主要 func willDisplay

重新提供一次座標: 這裡會呼叫 Apple 已經寫好的 func layoutIfNeeded()

繪製 chat bubble: 剛剛繪製 chatBubble 的 func,otherPeopleChatBubble() 和 myChatBubble()

到這裡,基本上就完成囉,最後再把檔案分類一下,就差不多了~檔案整整齊齊的歸類讓人看了心情也會開心起來~

Free talk

這次作業最難的地方真的是在畫 chat bubble,花了超久的時間。還有一個地方也卡很久,就是在設定 Auto Layout 時,忘記先把 scrolling enabled 取消勾選,結果一直無法成功設定 Aauto Layout,卡了我一整個下午,還一度以為是不是 Xcode 的問題,結果有問題的是我的腦袋啊…

--

--