Issue #567

Code

Make open Adapter

import UIKit

public protocol AdapterDelegate: class {

  /// Apply model to view
  func configure(model: Any, view: UIView, indexPath: IndexPath)

  /// Handle view selection
  func select(model: Any)

  /// Size the view
  func size(model: Any, containerSize: CGSize) -> CGSize
}

/// Act as DataSource and Delegate for UICollectionView, UITableView
open class Adapter: NSObject,
  UICollectionViewDataSource, UICollectionViewDelegateFlowLayout,
UITableViewDataSource, UITableViewDelegate {

  public var sections: [Section] = []
  public weak var collectionView: UICollectionView?
  public weak var tableView: UITableView?
  public weak var delegate: AdapterDelegate?

  let registryService = RegistryService()

  // MARK: - Initialiser
  public required init(collectionView: UICollectionView) {
    self.collectionView = collectionView
    super.init()
  }

  public required init(tableView: UITableView) {
    self.tableView = tableView
    super.init()
  }

  // MARK: - UICollectionViewDataSource
  open func numberOfSections(in collectionView: UICollectionView) -> Int {
    return sections.count
  }

  open func collectionView(_ collectionView: UICollectionView,
                           numberOfItemsInSection section: Int) -> Int {
    return sections[section].items.count
  }

  open func collectionView(_ collectionView: UICollectionView,
                           cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let item = sections[indexPath.section].items[indexPath.row]
    let cell = collectionView.dequeueReusableCell(
      withReuseIdentifier: item.cellType.typeName,
      for: indexPath)

    delegate?.configure(model: item.model, view: cell, indexPath: indexPath)

    return cell
  }

  open func collectionView(
    _ collectionView: UICollectionView,
    viewForSupplementaryElementOfKind kind: String,
    at indexPath: IndexPath) -> UICollectionReusableView {

    if let header = sections[indexPath.section].header,
      kind == UICollectionElementKindSectionHeader {

      let view = collectionView.dequeueReusableSupplementaryView(
        ofKind: UICollectionElementKindSectionHeader,
        withReuseIdentifier: header.viewType.typeName,
        for: indexPath
      )

      delegate?.configure(model: header.model, view: view, indexPath: indexPath)
      return view
    } else if let footer = sections[indexPath.section].footer,
      kind == UICollectionElementKindSectionFooter {

      let view = collectionView.dequeueReusableSupplementaryView(
        ofKind: UICollectionElementKindSectionFooter,
        withReuseIdentifier: footer.viewType.typeName,
        for: indexPath
      )

      delegate?.configure(model: footer.model, view: view, indexPath: indexPath)
      return view
    } else {
      let view = DummyReusableView()
      view.isHidden = true
      return view
    }
  }

  // MARK: - UICollectionViewDelegate
  open func collectionView(
    _ collectionView: UICollectionView,
    didSelectItemAt indexPath: IndexPath) {

    let item = sections[indexPath.section].items[indexPath.row]
    delegate?.select(model: item.model)
    collectionView.deselectItem(at: indexPath, animated: true)
  }

  open func collectionView(
    _ collectionView: UICollectionView,
    layout collectionViewLayout: UICollectionViewLayout,
    sizeForItemAt indexPath: IndexPath) -> CGSize {

    let item = sections[indexPath.section].items[indexPath.row]
    if let size = delegate?.size(model: item.model, containerSize: collectionView.frame.size) {
      return size
    }

    if let size = (collectionViewLayout as? UICollectionViewFlowLayout)?.itemSize {
      return size
    }

    return collectionView.frame.size
  }

  open func collectionView(
    _ collectionView: UICollectionView,
    layout collectionViewLayout: UICollectionViewLayout,
    referenceSizeForHeaderInSection section: Int) -> CGSize {

    guard let header = sections[section].header else {
      return .zero
    }

    guard let size = delegate?.size(model: header.model, containerSize: collectionView.frame.size) else {
      return .zero
    }

    return size
  }

  open func collectionView(
    _ collectionView: UICollectionView,
    layout collectionViewLayout: UICollectionViewLayout,
    referenceSizeForFooterInSection section: Int) -> CGSize {

    guard let footer = sections[section].footer else {
      return .zero
    }

    guard let size = delegate?.size(model: footer.model, containerSize: collectionView.frame.size) else {
      return .zero
    }

    return size
  }

  // MARK: - Reload
  open func reload(sections: [Section]) {
    // Registry
    registryService.registerIfNeeded(
      collectionView: collectionView,
      tableView: tableView,
      sections: sections
    )

    self.sections = sections
    collectionView?.reloadData()
    tableView?.reloadData()
  }

  // MARK: - UITableViewDataSource
  open func numberOfSections(in tableView: UITableView) -> Int {
    return sections.count
  }

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

  open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let item = sections[indexPath.section].items[indexPath.row]
    let cell = tableView.dequeueReusableCell(
      withIdentifier: item.cellType.typeName,
      for: indexPath
    )

    delegate?.configure(model: item.model, view: cell, indexPath: indexPath)

    return cell
  }

  // MARK: - UITableViewDelegate
  open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let item = sections[indexPath.section].items[indexPath.row]
    delegate?.select(model: item.model)
    tableView.deselectRow(at: indexPath, animated: true)
  }

  open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    let item = sections[indexPath.section].items[indexPath.row]
    if let size = delegate?.size(model: item.model, containerSize: tableView.frame.size) {
      return size.height
    }

    return 0
  }

  open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    guard let header = sections[section].header else {
      return 0
    }

    guard let size = delegate?.size(model: header.model, containerSize: tableView.frame.size) else {
      return 0
    }

    return size.height
  }

  open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
    guard let footer = sections[section].footer else {
      return 0
    }

    guard let size = delegate?.size(model: footer.model, containerSize: tableView.frame.size) else {
      return 0
    }

    return size.height
  }

  open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    guard let header = sections[section].header else {
      return nil
    }

    guard let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: header.viewType.typeName) else {
      return nil
    }

    delegate?.configure(model: header.model, view: view, indexPath: IndexPath(row: 0, section: section))
    return view
  }

  open func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
    guard let footer = sections[section].footer else {
      return nil
    }

    guard let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: footer.viewType.typeName) else {
      return nil
    }

    delegate?.configure(model: footer.model, view: view, indexPath: IndexPath(row: 0, section: section))
    return view
  }
}

Declare data

let sections: [Section] = [
  Section(
    header: Header(model: Model.header("Information"), viewType: HeaderView.self),
    items: [
      Item(model: Model.avatar(avatarUrl), cellType: AvatarCell.self),
      Item(model: Model.name("Thor"), cellType: NameCell.self),
      Item(model: Model.location("Asgard"), cellType: NameCell.self)
    ]
  ),
  Section(
    header: Header(model: Model.header("Skills"), viewType: HeaderView.self),
    items: [
      Item(model: Model.skill("iOS"), cellType: SkillCell.self),
      Item(model: Model.skill("Android"), cellType: SkillCell.self)
    ]
  )
]

adapter.reload(sections: sections)

Configure required blocks

extension ProfileViewController: AdapterDelegate {
  func configure(model: Any, view: UIView, indexPath: IndexPath) {
    guard let model = model as? Model else {
      return
    }

    switch (model, view) {
    case (.avatar(let string), let cell as Avatarcell):
      cell.configure(string: string)
    case (.name(let name), let cell as NameCell):
      cell.configure(string: name)
    case (.header(let string), let view as HeaderView):
      view.configure(string: string)
    default:
      break
    }
  }

  func select(model: Any) {
    guard let model = model as? Model else {
      return
    }

    switch model {
    case .skill(let skill):
      let skillController = SkillController(skill: skill)
      navigationController?.pushViewController(skillController, animated: true)
    default:
      break
    }
  }

  func size(model: Any, containerSize: CGSize) -> CGSize {
    guard let model = model as? Model else {
      return .zero
    }

    switch model {
    case .name:
      return CGSize(width: containerSize.width, height: 40)
    case .avatar:
      return CGSize(width: containerSize.width, height: 200)
    case .header:
      return CGSize(width: containerSize.width, height: 30)
     default:
       return .zero
    }
  }
}

Extending Manager

class AccordionManager<T>: Manager<T> {
  private var collapsedSections = Set<Int>()
 
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return collapsedSections.contains(section)
      ? 0 : sections[section].items.count
  }
 
  func toggle(section: Int) {
    if collapsedSections.contains(section) {
      collapsedSections.remove(section)
    } else {
      collapsedSections.insert(section)
    }
 
    let indexSet = IndexSet(integer: section)
    tableView?.reloadSections(indexSet, with: .automatic)
  }
}