看到封面圖就知道,再過幾天就是農曆新年啦!
先祝大家新年快樂,鼠年行大運!!


TableView Delegate & TableView Datasource

TableView 應該是所有 iOS 工程師最常用到的元件之一吧!
相信對他的 delegate & datasource 應該是不陌生才是。

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return dataSources.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCellIdentifier", for: indexPath) as! CustomCell
    // Do something ....
    return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    // tap cell 
}

這三個方法應該是最最常用的,但是每次都要在 UIViewController 裡寫上這麼多方法,感覺很辛苦啊!
更不用說他還有其他哩哩摳摳的方法~~

再者因為先前很習慣 RxDatasource 的寫法,很想要把那一套簡潔的寫法拿來用,
所以就衍生出這篇文章啦!!

由於現在這個新專案,很多UI都是用code寫出來了,所以我就先放上用code寫的程式碼,
如果之後改成用Storyboard的話再來更新。


實作開始

首先先把原本的 TableView 的方法實作出來,確定他是可以動之後再來改寫喔:

class SJTableView: UIView {  //這個 view 是透過 xib 生成的喔
    @IBOutlet weak var tableView: UITableView! 

    lazy var xibView:UIView = {
        return Bundle.main.loadNibNamed("SJTableView", owner: self, options: nil)?.first as! UIView
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupView()
    }
    
    private func setupView() {
        xibView.frame = bounds
        addSubview(xibView)
        initTableView()
    }
    
    private func initTableView() {
        tableView.delegate = self
        tableView.dataSource = self
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCellIdentifier", for: indexPath) as! CustomCell
        // Do something ....
        return cell
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // tap cell 
    }
}

private var sj_item_selected:((_ index:IndexPath) -> Void)?
private var sj_configure_cell:((_ tb:UITableView, _ index:IndexPath) -> UITableViewCell?)?

我加了兩個 Private 的 block,一個是攔截 didSelectRowAt ,一個是生成 cellForRowAt 的時候使用,至於怎麼用就等會說明。

func registerClassCell(any:[AnyClass]) -> SJTableView {
    for cell in any {
        tableView.register(cell.self, forCellReuseIdentifier: String(describing: cell.self))
    }
    return self
}

func registerNibCell(cellIDs:[String]) -> SJTableView {
    for cellID in cellIDs {
        tableView.register(UINib(nibName: cellID, bundle: nil), forCellReuseIdentifier: cellID)
    }
    return self
}

再加上兩個註冊 cell 的方法,可以用 Class 或是 Nib 的方式,端看個人需求而定。

func configuratorItemCell(configureCell:@escaping ((_ tb:UITableView, _ index:IndexPath) -> UITableViewCell? )) -> SJTableView {
    sj_configure_cell = configureCell
    return self
}

func itemDidSelected(completed:@escaping ((_ index:IndexPath) -> Void)) -> SJTableView {
    sj_item_selected = completed
    return self
}

這邊我又加了兩個方法,主要是要讓用這個元件的人可以選擇用我原本寫好的 Cell ,也可以自己另外新增自己的 Cell。
到這邊算是已經完成九成了唷。最後一個步驟就是在原本 (tableView:cellForRowAt) 以及 (tableView:didSelectRowAt) 加上我們一開始宣告的 block:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard sj_configure_cell == nil else {
        return (sj_configure_cell?(tableView, indexPath))!
    }

    let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCellIdentifier", for: indexPath) as! CustomCell
    // Do something ....
    return cell
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    guard sj_item_selected == nil else {
        sj_item_selected?(indexPath)
        return
    }
    // tap cell 
}

寫到這邊是不是很好奇那原本的 UIViewController 的寫法會變成怎樣呢?

tableView = tableView.configuratorItemCell(configureCell: { (tb, index) -> UITableViewCell? in
        let cell = tb.dequeueReusableCell(withIdentifier: "cell", for: index)
        return cell
    }).itemDidSelected(completed: { (index) in
        print(index.row)
    })

這邊保留了一點彈性,如果你不需要製作新的 TableViewCell,想要用原本提供的 cell 就好。
改成這樣寫就只會拿到使用者點擊 Cell 的事件:

tableView = tableView.itemDidSelected(completed: { (index) in
    print(index.row)
})

同樣的,如果不需要點擊事件,就不要 call itemDidSelected 這個方法就好囉!


ReloadData

在我寫第一版的時候,還不是很確定要怎麼樣讓 tableView 做 reload,所以我就在 api callback 的時候,把整理好的資料丟進 tableview 的 function:

api.getDatas(completed: { [unowned self] (result) in 
    self.tableView.setupData(result)
}) { [unowned self] (error) in 
    self.showError(error.localizedDescription)
}

但是這樣一點都不直覺啊,而且其他 viewController 在用這個套件的時候一定會忘記寫這邊的程式碼。
所以我後來研究之後覺得改成另一種方式也許會好用一點。
在原本的 class 外面先定義一個 block type:typealias BindType = ((_ dataSources:[Any]) -> Void),再定義一個 protocol 讓要用這個套件的人要去實作這個 protocol 裡的 function / proprety,

protocol SJTableViewAble {
    var onBind:BindType? { get set }
}

我們在 SJTableView 這個 Class 裡新增幾個 function,當 onBind 這個 block 發生變化的時候可以回到 SJTableView 做 reloadData。

func getBind() -> BindType {
    return sj_bind_data
}

func setDelegate(_ mDelegate:SJTableViewAble) -> SJTableView {
    delegate = mDelegate
    return self
}

private var sj_bind_data:BindType = { (_ ,_ ) in }

private func initTableView() {
    tableView.delegate = self
    tableView.dataSource = self
    sj_bind_data = { (datas,) in
        // do refresh tableView
    }
}

在原本使用 SJTableView 的 viewController 裡面,就要把我們剛剛新增的 delegate 在這個 viewController 實作。

class ViewController: UIViewController,SJTableViewAble {
    var onBind: BindType?
    override func viewDidLoad() {
        tableView = tableView
                .itemDidSelected { (index) in
                    print(index.row)
                }.setDelegate(self)
        onBind = mainTableView.getBind()
        getData()
    }

    func getData() {
        api.getDatas(completed: { [unowned self] (result) in 
            // self.tableView.setupData(result)
            self.onBind?(result) //這邊改成由本身的閉包去推送資料回到 tableView 裡面,就不直接去 call tableView 的 reload
        }) { (error) in 
            // show error
        }
    }
}

以上就是把一個 TableView 做簡單的封裝,但同時也保留一些彈性讓其他的使用者改成他想要的功能。
是不是很簡單。
完整的程式碼請見下方區域,希望這篇文章有幫助到你喲!!


ViewController.swift

class ViewController: UIViewController,SJTableViewAble {
    var onBind: BindType?
    private lazy var tableView:SJTableView = {
        return SJTableView(frame: self.view.frame)
    }()

    override func viewDidLoad() {
        tableView = tableView
                .itemDidSelected { (index) in
                    print(index.row)
                }.setDelegate(self)
        onBind = mainTableView.getBind()
        getData()
    }

    func getData() {
        api.getDatas(completed: { [unowned self] (result) in 
            self.onBind?(result) 
        }) { (error) in 
            // show error
        }
    }
}

SJTableView.swift

typealias BindType = ((_ dataSources:[Any]) -> Void)

protocol SJTableViewAble {
    var onBind:BindType? { get set }
}

class SJTableView: UIView {

    @IBOutlet weak var tableView: UITableView!
    
    func registerClassCell(any:[AnyClass]) -> SJTableView {
        for cell in any {
            tableView.register(cell.self, forCellReuseIdentifier: String(describing: cell.self))
        }
        return self
    }
    
    func registerNibCell(cellIDs:[String]) -> SJTableView {
        for cellID in cellIDs {
            tableView.register(UINib(nibName: cellID, bundle: nil), forCellReuseIdentifier: cellID)
        }
        return self
    }
    
    func configuratorItemCell(configureCell:@escaping ((_ tb:UITableView, _ index:IndexPath) -> UITableViewCell? )) -> SJTableView {
        sj_configure_cell = configureCell
        return self
    }
    
    func itemDidSelected(completed:@escaping ((_ index:IndexPath) -> Void)) -> SJTableView {
        sj_item_selected = completed
        return self
    }
    
    func getBind() -> BindType {
        return sj_bind_data
    }

    func setDelegate(_ mDelegate:SJTableViewAble) -> SJTableView {
        delegate = mDelegate
        return self
    }
    
    //MARK: - private objective and proprety
    private var datas:[Any] = []
    private var sj_item_selected:((_ index:IndexPath) -> Void)?
    private var sj_configure_cell:((_ tb:UITableView, _ index:IndexPath) -> UITableViewCell?)?
    private var sj_bind_data:BindType = { (_ ,_ ) in }
    private var delegate:SJTableViewAble?
    
    lazy var xibView:UIView = {
        return Bundle.main.loadNibNamed("SJTableView", owner: self, options: nil)?.first as! UIView
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupView()
    }
    
    private func setupView() {
        xibView.frame = bounds
        addSubview(xibView)
        initTableView()
    }
    
    private func initTableView() {
        tableView.delegate = self
        tableView.dataSource = self
        sj_bind_data = { [unowned self] (dataSource) in
            self.datas = dataSource
            self.tableView.reloadData()
        }
    }
}

extension SJTableView: UITableViewDelegate, UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.datas.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard sj_configure_cell == nil else {
            return (sj_configure_cell?(tableView, indexPath))!
        }
        let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCellIdentifier", for: indexPath) as! CustomCell
        return cell
    }
}

補上 GitHub 連結