?通過擼一個滾動圖來學習介面卡模式
阿新 • • 發佈:2020-06-24
什麼是介面卡模式
介面卡模式屬於結構型模式中的一種,使用的非常多,極為常見,本文通過擼一個滾動圖來學習介面卡模式,程式碼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,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,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 排版