成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

類型化卻不失靈活的 Table View Controller

移動開發
本文介紹了解決這個問題的三種途徑。每種方案都試圖修復前一種方案中導致的問題。第一種方法在許多 O-C 代碼庫中都很常見。第二種方法利用了枚舉,但仍然不是最好的解決辦法。第三種方法的實現使用了協議和泛型——它們是 Swift 提供給我們的神兵利器。

[[163982]]

對(幾乎)所有的 iOS 開發者來說,UITableView 就像是面包和黃油一樣必不可少。大部分情況下,我們用一個 UITableViewCell 展現一種數據類型,然后通過 Identifier 來重用單元格。在 objc.io 中介紹了這種技術。當我們想在一個 Table View 中使用多個不同類型的 cell 時,情況則復雜的多。cell 的不一致讓我們很難處理。

本文介紹了解決這個問題的三種途徑。每種方案都試圖修復前一種方案中導致的問題。***種方法在許多 O-C 代碼庫中都很常見。第二種方法利用了枚舉,但仍然不是***的解決辦法。第三種方法的實現使用了協議和泛型——它們是 Swift 提供給我們的神兵利器。

基礎

我會帶你完成一個 demo 項目(github 地址),在這個例子中,我們創建了一個包含兩種不同 cell 的 Table View:一種 cell 用于顯示文本,一種 cell 用于顯示圖片,如下圖所示:

顯示兩種數據(文字和圖片)的 UITableView

在渲染視圖時,我喜歡用值類型來封裝數據。我把這個稱作 view data。這里,我們使用了兩個 view data:

  1. struct TextCellViewData { 
  2.     let title: String 
  3.   
  4. struct ImageCellViewData { 
  5.     let image: UIImage 

(在真實項目中可能會有更多屬性;image 屬性應該聲明為 NSURL ,以免對 UIKit 產生依賴)。對應地,我們也需要兩種 cell 來展現這兩種 view data:

  1. class TextTableViewCell: UITableViewCell { 
  2.     func updateWithViewData(viewData: TextCellViewData) { 
  3.         textLabel?.text = viewData.title 
  4.     } 
  5.   
  6. class ImageTableViewCell: UITableViewCell { 
  7.     func updateWithViewData(viewData: ImageCellViewData) { 
  8.         imageView?.image = viewData.image 
  9.     } 

然后,我們開始進入 View Controller 中。

***種方法:簡單方法

我不喜歡一開始就講很復雜的東西,一開始,先講一個簡單的實現,用于顯示一點東西在屏幕上。

我們想讓 Table View 受數組中的數據驅動(準確地說是 items 數組)。因為我們的數據是完全不同的兩種結構體,所以數組的類型只能是 [Any]。在 registerCells() 方法中我們使用標準的 cell 重用機制提前注冊了 cell。在 tableView(_:cellForRowAtIndexPath:) 方法中我們根據指定 IndexPath 所對應的 view data 的類型來創建 cell。我們的 View Controller 的完整實現非常簡單(為簡便起見,我們用 ViewController 作為 Table View 的數據源。在真實項目中,我們可能需要將數據源抽離到一個單獨的對象中。):

  1. class ViewController: UIViewController { 
  2.   
  3.     @IBOutlet weak var tableView: UITableView! 
  4.   
  5.     var items: [Any] = [ 
  6.         TextCellViewData(title: "Foo"), 
  7.         ImageCellViewData(image: UIImage(named: "Apple")!), 
  8.         ImageCellViewData(image: UIImage(named: "Google")!), 
  9.         TextCellViewData(title: "Bar"), 
  10.     ] 
  11.   
  12.     override func viewDidLoad() { 
  13.         super.viewDidLoad() 
  14.   
  15.         tableView.dataSource = self 
  16.         registerCells() 
  17.     } 
  18.   
  19.     func registerCells() { 
  20.         tableView.registerClass(TextTableViewCell.self, forCellReuseIdentifier: textCellIdentifier) 
  21.         tableView.registerClass(ImageTableViewCell.self, forCellReuseIdentifier: imageCellIdentifier) 
  22.     } 
  23.   
  24. extension ViewController: UITableViewDataSource { 
  25.   
  26.     func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 
  27.         return items.count 
  28.     } 
  29.   
  30.     func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 
  31.         let viewData = items[indexPath.row] 
  32.   
  33.         if (viewData is TextCellViewData) { 
  34.             let cell = tableView.dequeueReusableCellWithIdentifier(textCellIdentifier) as! TextTableViewCell 
  35.             cell.updateWithViewData(viewData as! TextCellViewData) 
  36.             return cell 
  37.         } else if (viewData is ImageCellViewData) { 
  38.             let cell = tableView.dequeueReusableCellWithIdentifier(imageCellIdentifier) as! ImageTableViewCell 
  39.             cell.updateWithViewData(viewData as! ImageCellViewData) 
  40.             return cell 
  41.         } 
  42.   
  43.         fatalError() 
  44.     } 

這個方法是可行的,但至少有以下幾個原因讓我不太滿意:

  • 我們無法重用這個 ViewController。如果我們想再加入一種新的 cell,比如用于顯示視頻,我們不得不在三個地方修改代碼:

1. 加入一個新的可重用 Identifier

2. 修改 registerCells() 方法

3. 修改 tableView(\_:cellForRowAtIndexPath:) 方法

  • 如果我們修改 items,提供給它一種 view data,而這種 view data 類型是我們無法處理的,則我們會觸發 tableView(\_:cellForRowAtIndexPath:) 方法中的 fatalError()。

  • 在 view data 和 cell 之間存在關聯性,但在類型系統中卻無法體現這種關聯性。

第二種方法:枚舉

我們可以添加一個 TableViewItem 枚舉類型來從某種程度上解決這些問題,在枚舉中,我們將 view data 所支持的所有類型都列舉進去:

  1. enum TableViewItem { 
  2.     case Text(viewData: TextCellViewData) 
  3.     case Image(viewData: ImageCellViewData) 

然后將 items 屬性的類型修改為 [TableViewItem]:

  1. var items: [TableViewItem] = [ 
  2.     .Text(viewData: TextCellViewData(title: "Foo")), 
  3.     .Image(viewData: ImageCellViewData(image: UIImage(named: "Apple")!)), 
  4.     .Image(viewData: ImageCellViewData(image: UIImage(named: "Google")!)), 
  5.     .Text(viewData: TextCellViewData(title: "Bar")), 

再修改 registerCells() 方法:

  1. func registerCells() { 
  2.     for item in items { 
  3.         let cellClass: AnyClass 
  4.         let identifier: String 
  5.           
  6.         switch(item) { 
  7.         case .Text(viewData: _): 
  8.             cellClass = TextTableViewCell.self 
  9.             identifier = textCellIdentifier 
  10.         case .Image(viewData: _): 
  11.             cellClass = ImageTableViewCell.self 
  12.             identifier = imageCellIdentifier 
  13.         } 
  14.           
  15.         tableView.registerClass(cellClass, forCellReuseIdentifier: identifier) 
  16.     } 

***,修改 tableView(_:cellForRowAtIndexPath:) 方法:

  1. func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 
  2.     let item = items[indexPath.row] 
  3.       
  4.     switch(item) { 
  5.     case let .Text(viewData: viewData): 
  6.         let cell = tableView.dequeueReusableCellWithIdentifier(textCellIdentifier) as! TextTableViewCell 
  7.         cell.updateWithViewData(viewData) 
  8.         return cell 
  9.     case let .Image(viewData: viewData): 
  10.         let cell = tableView.dequeueReusableCellWithIdentifier(imageCellIdentifier) as! ImageTableViewCell 
  11.         cell.updateWithViewData(viewData) 
  12.         return cell 
  13.     } 

不可否認,這種方法比上一種方法更好:

  • View Controller 只能提供枚舉中指定的 view data 類型。

  • 用 switch 語句代替了煩人的 if 語句,同時可以去掉 fatalError()。

然后我們還可以改進這個實現,比如將單元格的重用和設置修改為:

  1. func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 
  2.     let item = items[indexPath.row] 
  3.       
  4.     switch(item) { 
  5.     case let .Text(viewData: viewData): 
  6.         return tableView.dequeueCellWithViewData(viewData) as TextTableViewCell 
  7.     case let .Image(viewData: viewData): 
  8.         return tableView.dequeueCellWithViewData(viewData) as ImageTableViewCell 
  9.     } 

但是悲催的是,我們得在所有的地方都要加入這些 switch 語句。到目前為止,我們只在兩個地方使用了 switch 語句,但不難想象絕不僅限于此。例如,當自動布局將變得不可用我們必須使用手動布局時,我們必須在 tableView(\_:heightForRowAtIndexPath:) 中再使用一個 switch 語句。

這個方法不是不可以使用,但我始終對那些 switch 語句耿耿于懷,于是我打算更進一步。

第三種(***)方法:協議和泛型

讓我們徹底推翻前兩種解決辦法,另起爐灶。

聲明 Updatable 協議

我們的 cell 是根據 view data 來呈現不同界面的,因此我們定義一個 Updatable 協議,同時讓它和一個類型 ViewData 進行綁定:

  1. protocol Updatable: class { 
  2.     typealias ViewData 
  3.       
  4.     func updateWithViewData(viewData: ViewData) 

然后讓我們的自定義單元格實現該協議:

  1. extension TextTableViewCell: Updatable { 
  2.     typealias ViewData = TextCellViewData 
  3.   
  4. extension ImageTableViewCell: Updatable { 
  5.     typealias ViewData = ImageCellViewData 

看過前兩種方法之后,我們不難發現,對于 items 中的每個 view data 對象,我們都需要:

1. 找出要使用哪一種 cell 類

2. 找出要使用哪一個重用 Identifier

3. 用 veiw data 渲染 cell

定義 CellConfigurator 結構

因此,我們另外聲明一個結構來包裝 view data。用結構來提供更多的屬性和功能。不妨把這個結構命名為 CellConfigurator:

  1. struct CellConfigurator<cell where cell: updatable, cell: uitableviewcell> { 
  2.   
  3.     let viewData: Cell.ViewData 
  4.     let reuseIdentifier: String = NSStringFromClass(Cell) 
  5.     let cellClass: AnyClass = Cell.self 
  6.       
  7.     ...</cell where cell: updatable, cell: uitableviewcell> 

這個是一個泛型結構,使用類型參數 Cell。Cell 有兩個約束:首先必須實現了 Updatable 協議,其次它必須是 UITableViewCell 子類。

CellConfigurator 有三個屬性: viewData, reuseIdentifier 和 cellClass。viewData 的類型取決于 Cell 的類型,它沒有默認值。其他兩個屬性的值則取決于 Cell 的具體類型(這是 Swift 中的新特性,它真的很棒!)。

  1. ... 
  2.    // further part of CellConfigurator 
  3.    func updateCell(cell: UITableViewCell) { 
  4.        if let cell = cell as? Cell { 
  5.            cell.updateWithViewData(viewData) 
  6.        } 
  7.    } 

***,我們將 UITableViewCell 實例傳給 updateCell() 方法,就可以將 viewData 渲染到 cell 上。這里,我們不需要用到 Cell 的類型,因為 UITableViewCell 對象是通過 dequeueReusableCellWithIdentifier(_:forIndexPath:) 方法返回的。呼,這么短的實現,解釋起來這么費勁。

然后,在 items 數組中生成 CellConfigurator 實例:

  1. let items = [ 
  2.     CellConfigurator<texttableviewcell>(viewData: TextCellViewData(title: "Foo")), 
  3.     CellConfigurator<imagetableviewcell>(viewData: ImageCellViewData(image: UIImage(named: "Apple")!)), 
  4.     CellConfigurator<imagetableviewcell>(viewData: ImageCellViewData(image: UIImage(named: "Google")!)), 
  5.     CellConfigurator<texttableviewcell>(viewData: TextCellViewData(title: "Bar")), 
  6. ]</texttableviewcell></imagetableviewcell></imagetableviewcell></texttableviewcell> 

等等,怎么回事?居然出現一個編譯時錯誤?

Type of expression is ambiguous without more context

那是因為 CellConfigurator 是泛型,但 Swift 數組只能保存相同類型,我們不能簡單地把 CellConfigurator 和 CellConfigurator 放到同一個數組中。這是對的,但卻不是我們想要的。

啊哈,稍等一會,馬上搞定。Cell 類型參數實際上只在聲明 viewData 的時候用到。因此,我們在 CellConfigurator 中可以不需要指明 Cell 的真實類型。新聲明一個非泛型協議:

  1. protocol CellConfiguratorType { 
  2.     var reuseIdentifier: String { get } 
  3.     var cellClass: AnyClass { get } 
  4.       
  5.     func updateCell(cell: UITableViewCell) 

修改 CellConfigurator,讓它實現 CellConfiguratorType:

  1. extension CellConfigurator: CellConfiguratorType { 

現在可以將 items 的類型聲明為:

  1. let items: [CellConfiguratorType] 

編譯通過。

View Controller

我們現在開始修改 View Controller。 registerCells() 可以變得更簡單:

  1. func registerCells() { 
  2.     for cellConfigurator in items { 
  3.         tableView.registerClass(cellConfigurator.cellClass, forCellReuseIdentifier: cellConfigurator.reuseIdentifier) 
  4.     } 

tableView(_:cellForRowAtIndexPath:) 方法也變得更簡單了,這真是一個好消息:

  1. func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 
  2.     let cellConfigurator = items[indexPath.row] 
  3.     let cell = tableView.dequeueReusableCellWithIdentifier(cellConfigurator.reuseIdentifier, forIndexPath: indexPath) 
  4.     cellConfigurator.updateCell(cell) 
  5.     return cell 

為能重用 View Controller,我們還必須再做一些工作。比如讓 items 能夠從類外部修改。這里就不再多說了,你可以在 GitHub 上參考最終實現的這個框架和 demo:ConfigurableTableViewController

結束語

我們來看一下,***一個方案和前兩種方案相比有什么不同:

1. 當我們想增加一種新的 cell 時,不需要修改 View Controller

2. View Controller 是類型安全的。如果我們增加一種 cell 根本不支持的 view data 時,我們會得到一個編譯錯誤。

3. 刷新 cell 時不需要 switch 語句。

看來第三種方法解決了我們所有的問題。我想我們又前進了一步。一種更好的解決辦法往往是“眾里尋它千百度,驀然回首,那人卻在燈火闌刪處”。

感謝 Maciej Konieczny 和 Kamil Ko?odziejczyk 為本文審稿。

如果你喜歡本文,請關注我的 Twitter,或者訂閱我的 RSS

責任編輯:倪明 來源: CocoaChina
相關推薦

2020-11-04 15:07:39

人臉識別指紋識別生物識別

2021-02-20 10:56:30

人工智能人臉識別

2009-06-03 09:08:20

ScalaJava類型

2016-12-05 15:48:37

2009-01-22 19:03:32

服務器虛擬化VMware

2013-10-31 13:47:23

CloudaAPI

2012-05-10 11:40:49

存儲虛擬化

2013-04-03 10:42:46

iOS開發調試運行代碼

2012-02-24 09:03:11

云計算虛擬化

2023-12-05 10:25:24

Python類型注解

2020-01-18 20:23:35

AI 數據人工智能

2021-07-20 11:49:23

UPS電源數據中心電源管理

2011-10-20 14:02:11

虛擬化基礎架構服務器

2009-12-11 09:36:50

ASP.NET MVC

2012-09-20 10:27:05

VSphere兼容View

2019-05-20 08:11:02

淘寶個性化推薦

2015-12-18 14:32:12

寶德國產化

2009-04-15 19:09:37

Vmwareesx虛擬化

2013-11-21 15:37:22

思杰

2010-11-30 14:50:45

桌面虛擬化
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 一区二区三区四区日韩 | 欧美在线观看网站 | 国产在线中文字幕 | 日韩av成人| av大片| 伊人青青久久 | 国产视频欧美 | 成人一区二区三区在线观看 | 精品视频免费 | 日韩aⅴ视频 | 91香蕉视频在线观看 | 激情毛片 | 国产精品色 | 亚洲在线一区二区三区 | av中文在线 | 日韩精品一区二区三区老鸭窝 | 一本色道久久综合亚洲精品高清 | 国产精品一区二区免费看 | 欧美精品在线一区 | 亚洲国产中文字幕 | 中文字幕97 | 亚洲精品欧美一区二区三区 | 视频一区二区在线观看 | 精品一区二区三区四区 | 色综合九九 | 超碰97干| 美女爽到呻吟久久久久 | 国产男女视频网站 | 欧美成年黄网站色视频 | 中文字幕高清 | 国产精品视频久久久 | 激情一区 | 午夜激情小视频 | 日韩精品一区二区三区视频播放 | 国产亚洲欧美日韩精品一区二区三区 | 中文字字幕在线中文乱码范文 | 精品少妇一区二区三区在线播放 | 欧美一区二区三区久久精品 | 怡红院成人在线视频 | 日日操夜夜操视频 | 黄瓜av |