什么是适配器模式
适配器模式属于结构型模式中的一种,使用的非常多,极为常见,本文通过撸一个滚动图来学习适配器模式,代码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 排版