iOS
UITableView
デザインパターン
Swift
decorator

UITableViewに広告を挟み込むためのDecoratorパターン

More than 3 years have passed since last update.


目的


  • UITableViewに広告を挟み込みたい

  • 本文のコンテンツを返すDelegate/DataSourceを汚染したくない

  • 広告を挟み込む部分は別の箇所でも使いまわしたい


インターフェース


Decoratorパターンを実現するためのDataSource


DecoratorUITableViewDataSource

protocol DecoratorUITableViewDataSource : UITableViewDataSource {

// 外側の作用によって、再利用するためのindexPathとデータのindexPathがズレているので引数として両方を受け取っている
func getCell(tableView: UITableView, reuseIndexPath: NSIndexPath, dataIndexPath: NSIndexPath) -> UITableViewCell
}


Decoratorパターンを実現するためのDelegate


DecoratorUITableViewDelegate

protocol DecoratorUITableViewDelegate : UITableViewDelegate {

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
// 外側の作用によって、再利用するためのindexPathとデータのindexPathがズレているので引数として両方を受け取っている
func didSelect(tableView: UITableView, originalIndexPath: NSIndexPath, dataIndexPath: NSIndexPath)
}


DataSource/Delegateの実装


本文側


DeatSource


MainTableViewDataSource

class MainTableViewDataSourceImpl : NSObject, DecoratorUITableViewDataSource {

let models: [MainModel]
init(models: [MainModel]) { self.models = models }

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

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return getCell(tableView, reuseIndexPath: indexPath, dataIndexPath: indexPath)
}

func getCell(tableView: UITableView, reuseIndexPath: NSIndexPath, dataIndexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(MainCell.CellIdentifier, forIndexPath: reuseIndexPath) as? MainCell
cell?.model = model[dataIndexPath.row]
return cell ?? UITableViewCell.emptyCell()
}
}



Delegate


MainTableViewDelegate

class MainTableViewDelegateImpl : NSObject, DecoratorUITableViewDelegate {

let models: [MainModel]
init(models: [MainModel]) { self.models = models }

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
guard models.count > dataIndexPath.row else { return 0.0 }
selectedModel = models[dataIndexPath.row]
// 高さを計算して返す
}

func didSelect(tableView: UITableView, originalIndexPath: NSIndexPath, dataIndexPath: NSIndexPath) {
guard models.count > dataIndexPath.row else { return }
selectedModel = models[dataIndexPath.row]
// 選択されたモデルに応じた処理
}
}



広告側

private func getInnerIndexPath(indexPath: NSIndexPath) -> NSIndexPath?

private func getAdditionalIndexPath(indexPath: NSIndexPath) -> NSINdexPath
private func getAdditionalCount(innerNumber: Int) -> Int

の3つのメソッドはそれぞれprivateで持たせてしまっているが、AdditionalLogicというクラスに切り出して共通化するか、AdditionalCalcというprotocol extensionを用いた方がいいと思う。サンプルとしてはクラスが増えすぎるとややこしいので・・・


DeatSource


AdditionalTableViewDataSource

class AdditionalTableViewDataSourceImpl : NSObject, DecoratorUITableViewDataSource {

let inner: DecoratorUITableViewDataSource
let models: [AdditionalModel]
init(inner: DecoratorUITableViewDataSource, models: [AdditionalModel]) {
self.inner = inner
self.models = models
}

private func getInnerIndexPath(indexPath: NSIndexPath) -> NSIndexPath? {
// 広告を差し込むロジックによって差し込んだ結果内部の何番目を表示すればいいかを返す
// 広告を表示すべき位置であればnilを返す
}

private func getAdditionalIndexPath(indexPath: NSIndexPath) -> NSINdexPath {
// 広告を差し込むロジックによって差し込むべき広告が何番目かを返す
}

func getCell(tableView: UITableView, reuseIndexPath: NSIndexPath, dataIndexPath: NSIndexPath) -> UITableViewCell {
guard models.count > 0 else { return inner.getCell(tableView, reuseIndexPath: reuseIndexPath, dataIndexPath: dataIndexPath) }

if let innerIndexPath = getInnerIndexPath(dataIndexPath) {
return inner.getCell(tableView, reuseIndexPath: reuseIndexPath, dataIndexPath: innerIndexPath)
} else {
let cell = tableView.dequeueReusableCellWithIdentifier(AdditionalCell.CellIdentifier, forIndexPath: reuseIndexPath) as? AdditionalCell
let additionalIndex = getAdditionalIndexPath(dataIndexPath)
cell?.model = model[additionalIndex]
return cell ?? UITableViewCell.emptyCell()
}
}

private func getAdditionalCount(innerNumber: Int) -> Int {
// 広告を差し込むロジックによって、最終的にいくつのセルになるかを返す
// 通常は innerNumber + models.count
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let innerNumber = inner.tableView(tableView, numberOfRowsInSection: section)
return models.count < 1 ? innerNumber : getAdditionalCount(innerNumber)
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return getCell(tableView, reuseIndexPath: indexPath, dataIndexPath: indexPath)
}
}



Delegate


AdditionalTableViewDelegate

class AdditionalTableViewDelegateImpl : NSObject, DecoratorUITableViewDelegate {

let inner: DecoratorUITableViewDelegate
let models: [AdditionalModel]
init(inner: DecoratorUITableViewDelegate, models: [AdditionalModel]) {
self.inner = inner
self.models = models
}

private func getInnerIndexPath(indexPath: NSIndexPath) -> NSIndexPath? {
// 広告を差し込むロジックによって差し込んだ結果内部の何番目を表示すればいいかを返す
// 広告を表示すべき位置であれば nil を返す
}

private func getAdditionalIndexPath(indexPath: NSIndexPath) -> NSINdexPath {
// 広告を差し込むロジックによって差し込むべき広告が何番目かを返す
}

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if models.count < 1 {
return inner.tableView(tableView, heightForRowAtIndexPath: indexPath)
} else {
if let innerIndexPath = getInnerIndexPath(dataIndexPath) {
return inner.tableView(tableView, heightForRowAtIndexPath: innerIndexPath)
} else {
let additionalIndex = getAdditionalIndexPath(dataIndexPath)
// 高さを計算して返す
}
}
}

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
didSelect(tableView, originalIndexPath: indexPath, dataIndexPath: indexPath)
}

func didSelect(tableView: UITableView, originalIndexPath: NSIndexPath, dataIndexPath: NSIndexPath) {
if models.count < 1 {
inner.didSelect(tableView, originalIndexPath: originalIndexPath, dataIndexPath: dataIndexPath)
} else {
if let innerIndexPath = getInnerIndexPath(dataIndexPath) {
inner.didSelect(tableView, originalIndexPath: originalIndexPath, dataIndexPath: innerIndexPath)
} else {
let additionalIndex = getAdditionalIndexPath(dataIndexPath)
guard models.count > additionalIndex.row else { return }
selectedModel = models[additionalIndex.row]
// 選択されたモデルに応じた処理
}
}
}



TableViewControllerの実装


TableViewController

class TableViewController: UIViewController {

@IBOutlet weak var tableView: UITableView!
private var mainModels: [MainModel]?
private var additionalModels: [AdditionalModel]?
private var dataSource: DecoratorUITableViewDataSource?
private var delegate: DecoratorUITableViewDelegate?

override func viewDidLoad() {
// mainModelsとadditionalModelsをロードする

tableView.registerNibFromClass(MainCell.self, forCellReuseIdentifier: MainCell.CellIdentifier)
tableView.registerNibFromClass(AdditionalCell.self, forCellReuseIdentifier: AdditionalCell.CellIdentifier)

self.dataSource = AdditionalTableViewDataSourceImpl(inner: MainTableViewDataSourceImpl(inner: mainModels ?? []), models: additionalModels ?? [])
self.delegate = AdditionalTableViewDelegateImpl(inner: MainTableViewDelegateImpl(inner: mainModels ?? []), models: additionalModels ?? [])

tableView.dataSource = dataSource
tableView.delegate = delegate
}
}



所感

以上のようにしておけば


  • UITableViewに広告を挟み込みたい -> AdditionalのDataSourceとDelegateによって実現される

  • 本文のコンテンツを返すDelegate/DataSourceを汚染したくない -> MainのDataSourceとDelegateはsimple

  • 広告を挟み込む部分は別の箇所でも使いまわしたい -> AdditionalはMainに依存していないので使いまわせる

と本来の目的が実現できていると思う。広告をTableViewに挟み込むもっといいやり方があったら教えて下さい。もしかして、ちゃんと探したらそういうライブラリあったりしたのかなぁ。複数の種類のデータとCellを簡単に扱えるTableViewみたいなのが。。。