🐻通过撸一个滚动图来学习适配器模式

1,653 阅读2分钟

什么是适配器模式

适配器模式属于结构型模式中的一种,使用的非常多,极为常见,本文通过撸一个滚动图来学习适配器模式,代码Swifty。

适配器模式,重点适配二字,Apple的做法和原本有些不同,体现在UITableViewDelegate、UITableViewDataSource中。

需求

需求如下:

  • 点击顶部图片时,下方的tableview展示对应的信息
  • 传入的图片数量不固定
  • 需要有reload操作

使用适配器模式实现

定义协议 DataSource、Delegate

数据源

protocol HorizontalScrollerViewDataSource: class {
  // Ask the data source how many views it wants to present inside the horizontal scroller
  func numberOfViews(in horizontalScrollerView: HorizontalScrollerView) -> Int
  // Ask the data source to return the view that should appear at <index>
  func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView, viewAt index: Int) -> UIView
}

交互

protocol HorizontalScrollerViewDelegate: class {
  // inform the delegate that the view at <index> has been selected
  func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView, didSelectViewAt index: Int)
}

完整的 HorizontalScrollerView

class HorizontalScrollerView: UIView {
  weak var dataSource: HorizontalScrollerViewDataSource?
  weak var delegate: HorizontalScrollerViewDelegate?
  // 1
  private enum ViewConstants {
    static let Padding: CGFloat = 10
    static let Dimensions: CGFloat = 100
    static let Offset: CGFloat = 100
  }
  // 2
  private let scroller = UIScrollView()
  // 3
  private var contentViews = [UIView]()
  override init(frame: CGRect) {
    super.init(frame: frame)
    initializeScrollView()
  }
  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    initializeScrollView()
  }
  func initializeScrollView() {
    scroller.delegate = self
    //1
    addSubview(scroller)
    //2
    scroller.translatesAutoresizingMaskIntoConstraints = false
    //3
    NSLayoutConstraint.activate([
      scroller.leadingAnchor.constraint(equalTo: self.leadingAnchor),
      scroller.trailingAnchor.constraint(equalTo: self.trailingAnchor),
      scroller.topAnchor.constraint(equalTo: self.topAnchor),
      scroller.bottomAnchor.constraint(equalTo: self.bottomAnchor)
      ])
    //4
    let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(scrollerTapped(gesture:)))
    scroller.addGestureRecognizer(tapRecognizer)
  }
  func scrollToView(at index: Int, animated: Bool = true) {
    let centralView = contentViews[index]
    let targetCenter = centralView.center
    let targetOffsetX = targetCenter.x - (scroller.bounds.width / 2)
    scroller.setContentOffset(CGPoint(x: targetOffsetX, y: 0), animated: animated)
  }
  @objc func scrollerTapped(gesture: UITapGestureRecognizer) {
    let location = gesture.location(in: scroller)
    guard
      let index = contentViews.index(where: { $0.frame.contains(location)})
      else { return }
    delegate?.horizontalScrollerView(self, didSelectViewAt: index)
    scrollToView(at: index)
  }
  func view(at index :Int) -> UIView {
    return contentViews[index]
  }
  func reload() {
    // 1 - Check if there is a data source, if not there is nothing to load.
    guard let dataSource = dataSource else {
      return
    }
    //2 - Remove the old content views
    contentViews.forEach { $0.removeFromSuperview() }
    // 3 - xValue is the starting point of each view inside the scroller
    var xValue = ViewConstants.Offset
    // 4 - Fetch and add the new views
    contentViews = (0..<dataSource.numberOfViews(in: self)).map {
      index in
      // 5 - add a view at the right position
      xValue += ViewConstants.Padding
      let view = dataSource.horizontalScrollerView(self, viewAt: index)
      view.frame = CGRect(x: CGFloat(xValue), y: ViewConstants.Padding, width: ViewConstants.Dimensions, height: ViewConstants.Dimensions)
      scroller.addSubview(view)
      xValue += ViewConstants.Dimensions + ViewConstants.Padding
      // 6 - Store the view so we can reference it later
      return view
    }
    // 7
    scroller.contentSize = CGSize(width: CGFloat(xValue + ViewConstants.Offset), height: frame.size.height)
  }
  private func centerCurrentView() {
    let centerRect = CGRect(
      origin: CGPoint(x: scroller.bounds.midX - ViewConstants.Padding, y: 0),
      size: CGSize(width: ViewConstants.Padding, height: bounds.height)
    )
    guard let selectedIndex = contentViews.index(where: { $0.frame.intersects(centerRect) })
      else { return }
    let centralView = contentViews[selectedIndex]
    let targetCenter = centralView.center
    let targetOffsetX = targetCenter.x - (scroller.bounds.width / 2)
    scroller.setContentOffset(CGPoint(x: targetOffsetX, y: 0), animated: true)
    delegate?.horizontalScrollerView(self, didSelectViewAt: selectedIndex)
  }
}
extension HorizontalScrollerView: UIScrollViewDelegate {
  func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if !decelerate {
      centerCurrentView()
    }
  }
  func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    centerCurrentView()
  }
}

外部使用

设置代理 与数据刷新

horizontalScrollerView.dataSource = self
horizontalScrollerView.delegate = self
horizontalScrollerView.reload()

滚动到某一行

horizontalScrollerView.scrollToView(at: currentAlbumIndex, animated: false)

总结

适配是一种映射关系,逻辑如下:

A --->  适配器 ---> A'

传统做法:通常情况下适配器模式是:A提供的接口不满足B,使用适配器C,来转换A的接口,使得B能够满足。

apple的做法:通过协议向适配器中传入参数,从而得到想要的结果,省略了C,B直接操作A的接口,就能达到适配的目的。

如果让你做这个需求的话,你怎么设计?会使用适配器模式吗?如有不同见解入群solo。

其他

本文使用 mdnice 排版