Edited at

[Swift]UITableViewからDataSourceを分離する

More than 1 year has passed since last update.


前置き

iOSアプリの高速化を測れないか、と上からボソッと言われ考えていた。そこで画像表示が遅いのでそこを高速化しようと思い、その前段階として分離を行おうと思っていた。

また、何回かに一回はセルに広告を表示する仕様になっているが、前任者の実装の仕方が依存関係が強いものであり、いろいろと問題が起っていたので、TableViewDataSource切り離しを行いたかった。

Rxとか流行ってますので、今更感ですが備忘録として。


実装

基本的には「UITableViewを軽量化する!(Swift版)」を参考にしていますが、

カスタムセルが複数ある場合に対応できないので、以下ではそれも加味したものを掲載します。


1.現状

書き換え前の状態を書いていきます。

例として、実際のものに近い簡易的なコードを記述していきます。


・ViewModel

画像とラベルのシンプルなもの


CommonViewModel.swift

struct CommonViewModel {

var icon: URL?
var title: String?
}


・Xib

上記のモデルを実際にセットするもの

XxxTableViewCell.xib

cell.png


・TableViewCell

上記のXibに対応するセル


XxxTableViewCell.swift

class XxxTableViewCell: UITableViewCell {

// MARK: IBOutlet Variables
@IBOutlet private weak var iconView: UIImageView!
@IBOutlet private weak var title: UILabel!

// MARK: Override Methods
override func awakeFromNib() {
super.awakeFromNib()
}

// MARK: Internal Methods
func setupCell(with viewModel: CommonViewModel) {
if let title = viewModel.title {
title.text = title
}
if let url = viewModel.icon {
iconView.setImage(withURL: url) // ライブラリNukeのメソッド
}
}

}



・TableView

UIViewController で実装していた場合での想定で。

例としてXxxTableViewCell,YyyTableViewCellの二つのセルを使う想定。

(上記ではYyyTableViewCellは記載ないですが、同じViewModelを使うものを想定している。)


XxxViewController.swift

class XxxViewController: UIViewController {

// MARK: IBOutlet Variables
@IBOutlet fileprivate weak var tableview: UITableView! {
didSet {
tableview.dataSource = self
tableview.delegate = self

// 複数セル
tableView.register(UINib(nibName: "XxxTableViewCell", bundle:nil), forCellReuseIdentifier: "任意のセルID xxx")
tableView.register(UINib(nibName: "YyyTableViewCell", bundle:nil), forCellReuseIdentifier: "任意のセルID yyy")
}
}

// MARK: Fileprivate Variables
fileprivate var viewModels: [CommonViewModel] = [] {
didSet {
tableview.reloadData()
}
}

// MARK: Override Methods
override func viewDidLoad() {
super.viewDidLoad()

setupView()
}

// MARK: Private Methods
private func setupView() {

// do some setup methods

callForAPI()
}

private func callForAPI() {
// 通信メソッド 今回は成功失敗を返すものとしている。
APIManager.~~~~~~ { [weak self] response in
switch response {
case .success(let result):
// 通信成功時にviewModelsの配列に変換している前提
self?.viewModels = result

case .failure(let error):
// do some error handling
}
}
}

// MARK: Fileprivate Methods
fileprivate func getXxxCell() -> XxxTableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "任意のセルID xxx",
for: indexPath
) as! XxxTableViewCell

// do some custmaize

return cell

}

fileprivate func getYyyCell() -> YyyTableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "任意のセルID yyy",
for: indexPath
) as! YyyTableViewCell

// do some customize

return cell
}
}

// MARK: - DataSource
extension XxxViewController: UITableViewDataSource {

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

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

// 条件によってセルが変わる想定
if /* 条件式 */ {
return getXxxCell() // 上記の XxxTableViewCell セル

} else {
return getYyyCell() // 上記とはまた別のセル

}
}
}

// MARK: - Delegate
extension XxxViewController: UITableViewDelegate {
/* 省略 */
}


先にも書きましたが、簡略化して書いています。

APIを叩いてviewModelsをセットし、テーブルを更新する作りになっている。

UITableViewDelegateは記載しても仕方ないので省略。


2.DataSourceの切り出し

上記の現状を踏まえて行っていきます。


DataSource管理クラスの作成


XxxDataSource.swift

class XxxDataSource: NSObject {

typealias Element = [CommonViewModel]

var items: Element

init(items: Element) {
self.items = items
}
}



DataSourceのメソッド追加

UITableViewDataSource を継承し、メソッド追加。

また、セルの取得メソッドをこちらに移行します。


XxxDataSource.swift

extension XxxDataSource: UITableViewDataSource {

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

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

// 条件によってセルが変わる想定
if /* 条件式 */ {
return getXxxCell() // 上記の XxxTableViewCell セル

} else {
return getYyyCell() // 上記とはまた別のセル

}
}

// MARK: Private Methods
private func getXxxCell() -> XxxTableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "任意のセルID xxx",
for: indexPath
) as! XxxTableViewCell

// do some custmaize

return cell

}

private func getYyyCell() -> YyyTableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "任意のセルID yyy",
for: indexPath
) as! YyyTableViewCell

// do some costomize

return cell
}

}



TableViewDataSourceを追加

先ほどのUIViewControllerに以下のコード追加・更新を行う

class XxxViewController: UIViewController {

// DataSourceを追加
private var dataSource: XxxDataSource = XxxDataSource(items: [])

// self → dataSource に変更
@IBOutlet weak var tableView: UITableView! {
didSet {
tableView.dataSource = dataSource // 変更部分
tableview.delegate = self
tableView.register(UINib(nibName: "XxxTableViewCell", bundle:nil), forCellReuseIdentifier: "任意のセルID")
tableView.register(UINib(nibName: "YyyTableViewCell", bundle:nil), forCellReuseIdentifier: "任意のセルID")
}
}

// DataSourceにModelを渡す部分を追加
private var viewModels: [XxxViewModel] = [] {
didSet {
dataSource.items = viewModels // 追加部分
tableView.reloadData()
}
}

/* 中略 */

}

切り出し完了です!

viewModelsが更新されると自動的にdidSetdataSourceの中身が更新される仕組みです。

追記


おまけ


RxDataSourceの導入

ライブラリのインストールは、carthageなりで各自お願いします。

今までの流れで作成していれば、以下をだけです!

RxTableViewDataSourceTypeを継承

②メソッドの追加

class XxxDataSource: NSObject, RxTableViewDataSourceType {

typealias Element = [CommonViewModel]

var items: Element

init(items: Element) {
self.items = items
}

// 追加部分
func tableView(_ tableView: UITableView, observedEvent: Event<Element>) {
// 各々処理
}
}


後書き

ViewControllerの肥大化を抑えられる点は大きいですね。

実際のコードを載せるわけにいかないので、名前の置き換え等で記述が間違っているところがあるかもしれないので、ご指摘あればお願いします。


参考

swiftでDelegate/DataSourceを分離したいときに気をつけること

UITableViewを軽量化する!(Swift版)