V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
iOS 开发实用技术导航
NSHipster 中文版
http://nshipster.cn/
cocos2d 开源 2D 游戏引擎
http://www.cocos2d-iphone.org/
CocoaPods
http://cocoapods.org/
Google Analytics for Mobile 统计解决方案
http://code.google.com/mobile/analytics/
WWDC
https://developer.apple.com/wwdc/
Design Guides and Resources
https://developer.apple.com/design/
Transcripts of WWDC sessions
http://asciiwwdc.com
Cocoa with Love
http://cocoawithlove.com/
Cocoa Dev Central
http://cocoadevcentral.com/
NSHipster
http://nshipster.com/
Style Guides
Google Objective-C Style Guide
NYTimes Objective-C Style Guide
Useful Tools and Services
Charles Web Debugging Proxy
Smore
banxi1988
V2EX  ›  iDev

[Swift] UITableView:从牛刀到小刀 (1)

  •  
  •   banxi1988 ·
    banxi1988 · 2016-12-28 21:28:06 +08:00 · 3099 次点击
    这是一个创建于 2890 天前的主题,其中的信息可能已经有所发展或是发生改变。

    对于 iOS 开发来说, UITableView 无疑是一把牛刀. 今天我想介绍的是怎么让这把牛刀像小刀一样好使.

    我们想实现的目标是, 对于一些简单的场景,比如一个小的列表,可以像如下这么使用:

     let dataSource = ListDataSource(dataItems:[item1,item2,...])
     tableView.dataSource = dataSource
    

    OK, 我们开始吧.

    创建一个实现了 UITableViewDataSource 协议的类.

    由于 UITableViewDataSource 协议扩展了 NSObjectProtocl, 所以我们的类最好继承自 NSObject.

    class ListDataSource: NSObject, UITableViewDataSource{
      
    }
    

    实现 UITableViewDataSource 要求的两个方法:

    class ListDataSource: NSObject, UITableViewDataSource{
     
      public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
        return 0
      }
      
      public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
        return UITableViewCell(style: .default, reuseIdentifier: "cell")
      }
      
    }
    

    这里就遇到问题了,我们的数据是动态的,不能直接返回 0. 我们的 cell 也不是这样直接一个 UITableViewCell 类啊. 问题是,我们不知道数据是什么,怎么构造 Cell?

    引入泛型数组.

    • 先来处理 numberOfRowsInSection 这个方法. 这里我们只用到一个 Section, 我们的数据最常见的是一个数组. 那返回这个数组的大小即可. 但是数组类型我们不知道, 于是我们声明为泛型 . 如下:

    于是 ListDataSource 变成了这样:

    public class ListDataSource<T>: NSObject, UITableViewDataSource{
      public private(set) var dataItems: [T] = []
     
      public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
        return dataItems.count
      }
      
      public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
        return UITableViewCell(style: .default, reuseIdentifier: "cell")
      }
      
    }
    

    引入泛型 UITableViewCell 子类.

    解决了上面问题, 同理我们的 UITableViewCell 是不是也可以声明为泛型呢?

    于是变成这样:

    public class ListDataSource<T,Cell: UITableViewCell>: NSObject, UITableViewDataSource{
      public private(set) var dataItems: [T] = []
     
      public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
        return dataItems.count
      }
      
      public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
        return Cell(style: .default, reuseIdentifier: "cell")
      }
      
    }
    
    

    上面我们对于 Cell 泛型也增加了基本的限制,就是 Cell 是继承自 UITableViewCell 的.

    引入 Cell 数据绑定协议

    但是这里有一个问题就是, 我们的数据跟 Cell 还没有关联起来呢! 也就是说我们需要 Cell 有关联 数据模型 T 的能力. 上面这个需求体现了两点, 一表明某个能力, 即满足某协议约束. 对于泛型来说,可以增加需要实现的协议作为约束.

    于是我们要求,Cell 应该实现如下协议, 可以绑定某一个数据.

    public protocol Bindable{
      associatedtype DataItem
      func bind(to item: DataItem)
    }
    

    因为我们不知道要绑定的数据类型是什么, 所以我们只能先给一个名字. DataItem. 由实现者确定类型是什么. 也相当是一种泛型.

    同时这个 DataItem 应该等于之前的数据泛型 T. 这样,我们得到如下的 ListDataSource

    public protocol Bindable{
      associatedtype DataItem
      func bind(to item: DataItem)
    }
    
    public class ListDataSource<T,Cell: UITableViewCell>: NSObject, UITableViewDataSource where Cell: Bindable, Cell.DataItem == T{
      public private(set) var dataItems: [T] = []
     
      public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
        return dataItems.count
      }
      
      public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
        let cell =  Cell(style: .default, reuseIdentifier: "cell")
        cell.bind(to: dataItems[indexPath.row])
        return cell
      }
      
    }
    

    重点在于 where Cell: Bindable, Cell.DataItem == T 也就是我们上面所说的. 要求 Cell 实现 Bindable 协议.

    简化 泛型声明. 去掉泛型 T 声明.

    之前由于不确定数据模型的类型,所以我们引入了 T 作为泛型名. 但是后来我们又添加了 Bindable, 其中使用 DataItem 名来标识数据类型. 这里两者相等,看起来重复了. 简化后如下:

    public protocol Bindable{
      associatedtype DataItem
      func bind(to item: DataItem)
    }
    
    public class ListDataSource<Cell: UITableViewCell>: NSObject, UITableViewDataSource where Cell: Bindable{
      public private(set) var dataItems: [Cell.DataItem] = []
     
      public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
        return dataItems.count
      }
      
      public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
        let cell =  Cell(style: .default, reuseIdentifier: "cell")
        cell.bind(to: dataItems[indexPath.row])
        return cell
      }
      
    }
    
    

    完善一下.

    • 支持 ReuseableCell 模型.
    • 支持 自动注册重用 Cell. 完善后的代码如下:
    public protocol Bindable{
      associatedtype DataItem
      func bind(to item: DataItem)
    }
    
    extension String{
      public static let listCellReuseIdentifier = "listCell"
    }
    
    public class ListDataSource<Cell: UITableViewCell>: NSObject, UITableViewDataSource where Cell: Bindable{
      public private(set) var dataItems: [Cell.DataItem] = []
      
      private weak var tableView:UITableView?
      
      public init(dataItems: [Cell.DataItem]){
        self.dataItems = dataItems
      }
      
      public func bind(to tableView: UITableView){
        self.tableView = tableView
        tableView.dataSource = self
        tableView.register(Cell.classForCoder(), forCellReuseIdentifier: .listCellReuseIdentifier)
      }
     
      public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
        return dataItems.count
      }
      
      public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
        let cell =  tableView.dequeueReusableCell(withIdentifier: .listCellReuseIdentifier, for: indexPath) as! Cell
        cell.bind(to: dataItems[indexPath.row])
        return cell
      }
      
    }
    

    使用示例

    有了上面的代码之后, 我们再显示一个 List 就简单得多了. 例如我们显示一个菜单列表.

    // 先定义菜单 Cell, 这是数据模型类型是 String
    class MenuCell: UITableViewCell,Bindable{
      typealias DataItem = String
      func bind(to item: String) {
        textLabel?.text = item
      }
    }
    
    // 实例化一个 ListDataSource
    let dataSource = ListDataSource<MenuCell>(dataItems: [
        "排骨面","红椒面", "青椒面"
      ])
    
    let tableView = UITableView(frame: .zero, style: .plain)
    // 将 DataSource 与 UITableView 绑定.
    dataSource.bind(to: tableView)
    

    怎么样? 最后实现的结果 ,跟我们的希望应该是差不多的了. 当然这只是一个开始, 为了适用你项目的使用, 你可以在上面的代码的基础上. 添加修改. Hope to see your advice!

    4 条回复    2016-12-29 09:22:05 +08:00
    HuangLibo
        1
    HuangLibo  
       2016-12-28 21:48:09 +08:00
    table view 是牛刀, 那 collection view 岂不是核武器了
    FreshOldMan
        2
    FreshOldMan  
       2016-12-28 21:49:10 +08:00
    大哥,每天一篇啊
    banxi1988
        3
    banxi1988  
    OP
       2016-12-28 21:59:25 +08:00
    @HuangLibo 怎么说呢. UICollectionView 从机制或者说潜力上来说, 比 TableView 强大.
    但是不如 UITableView 易用. UITableView 在很多使用场景下,基本是开箱即用. 而 UICollectionView 还需要我们自己多加加工.
    HuangLibo
        4
    HuangLibo  
       2016-12-29 09:22:05 +08:00   ❤️ 1
    @banxi1988 开箱即用,我推荐 IGListKit ,封装了 collection view
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5458 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 07:34 · PVG 15:34 · LAX 23:34 · JFK 02:34
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.