看到封面圖就知道,再過幾天就是農曆新年啦!
先祝大家新年快樂,鼠年行大運!!
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
}
}