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

# 目的
- UITableViewに広告を挟み込みたい
- 本文のコンテンツを返すDelegate/DataSourceを汚染したくない
- 広告を挟み込む部分は別の箇所でも使いまわしたい
 
# インターフェース

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

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

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

```swift: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

```swift: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

```swift: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

```swift: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

```swift: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の実装

```swift: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みたいなのが。。。