適配器把自己封裝起來然后暴露統(tǒng)一的接口給其他類,這樣即使其他類的接口各不相同,也能相安無事,一起工作。
如果你熟悉適配器模式,那么你會發(fā)現(xiàn)蘋果在實現(xiàn)適配器模式的方式稍有不同:蘋果通過委托實現(xiàn)了適配器模式。委托相信大家都不陌生。舉個例子,如果一個類遵循了 NSCoying
的協(xié)議,那么它一定要實現(xiàn) copy
方法。
橫滑的滾動欄理論上應該是這個樣子的:
新建一個 Swift 文件:HorizontalScroller.swift
,作為我們的橫滑滾動控件, HorizontalScroller
繼承自 UIView
。
打開 HorizontalScroller.swift
文件并添加如下代碼:
@objc protocol HorizontalScrollerDelegate {
}
這行代碼定義了一個新的協(xié)議: HorizontalScrollerDelegate
。我們在前面加上了 @objc
的標記,這樣我們就可以像在 objc 里一樣使用 @optional
的委托方法了。
接下來我們在大括號里定義所有的委托方法,包括必須的和可選的:
// 在橫滑視圖中有多少頁面需要展示
func numberOfViewsForHorizontalScroller(scroller: HorizontalScroller) -> Int
// 展示在第 index 位置顯示的 UIView
func horizontalScrollerViewAtIndex(scroller: HorizontalScroller, index:Int) -> UIView
// 通知委托第 index 個視圖被點擊了
func horizontalScrollerClickedViewAtIndex(scroller: HorizontalScroller, index:Int)
// 可選方法,返回初始化時顯示的圖片下標,默認是0
optional func initialViewIndex(scroller: HorizontalScroller) -> Int
其中,沒有 option
標記的方法是必須實現(xiàn)的,一般來說包括那些用來顯示的必須數(shù)據(jù),比如如何展示數(shù)據(jù),有多少數(shù)據(jù)需要展示,點擊事件如何處理等等,不可或缺;有 option
標記的方法為可選實現(xiàn)的,相當于是一些輔助設置和功能,就算沒有實現(xiàn)也有默認值進行處理。
在 HorizontalScroller
類里添加一個新的委托對象:
weak var delegate: HorizontalScrollerDelegate?
為了避免循環(huán)引用的問題,委托是 weak
類型。如果委托是 strong
類型的,當前對象持有了委托的強引用,委托又持有了當前對象的強引用,這樣誰都無法釋放就會導致內存泄露。
委托是可選類型,所以很有可能當前類的使用者并沒有指定委托。但是如果指定了委托,那么它一定會遵循 HorizontalScrollerDelegate
里約定的內容。
再添加一些新的屬性:
// 1
private let VIEW_PADDING = 10
private let VIEW_DIMENSIONS = 100
private let VIEWS_OFFSET = 100
// 2
private var scroller : UIScrollView!
// 3
var viewArray = [UIView]()
上面標注的三點分別做了這些事情:
UIScrollView
作為容器。接下來實現(xiàn)初始化方法:
override init(frame: CGRect) {
super.init(frame: frame)
initializeScrollView()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeScrollView()
}
func initializeScrollView() {
//1
scroller = UIScrollView()
addSubview(scroller)
//2
scroller.setTranslatesAutoresizingMaskIntoConstraints(false)
//3
self.addConstraint(NSLayoutConstraint(item: scroller, attribute: .Leading, relatedBy: .Equal, toItem: self, attribute: .Leading, multiplier: 1.0, constant: 0.0))
self.addConstraint(NSLayoutConstraint(item: scroller, attribute: .Trailing, relatedBy: .Equal, toItem: self, attribute: .Trailing, multiplier: 1.0, constant: 0.0))
self.addConstraint(NSLayoutConstraint(item: scroller, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1.0, constant: 0.0))
self.addConstraint(NSLayoutConstraint(item: scroller, attribute: .Bottom, relatedBy: .Equal, toItem: self, attribute: .Bottom, multiplier: 1.0, constant: 0.0))
//4
let tapRecognizer = UITapGestureRecognizer(target: self, action:Selector("scrollerTapped:"))
scroller.addGestureRecognizer(tapRecognizer)
}
上面的代碼做了如下工作:
UIScrollView
對象并且把它加到父視圖中。autoresizing masks
,從而可以使用 AutoLayout
進行布局。scrollview
添加約束。我們希望 scrollview
能填滿 HorizontalScroller
。HorizontalScroller
的委托。添加委托方法:
func scrollerTapped(gesture: UITapGestureRecognizer) {
let location = gesture.locationInView(gesture.view)
if let delegate = self.delegate {
for index in 0..<delegate.numberOfViewsForHorizontalScroller(self) {
let view = scroller.subviews[index] as UIView
if CGRectContainsPoint(view.frame, location) {
delegate.horizontalScrollerClickedViewAtIndex(self, index: index)
scroller.setContentOffset(CGPointMake(view.frame.origin.x - self.frame.size.width/2 + view.frame.size.width/2, 0), animated:true)
break
}
}
}
}
我們把 gesture 作為一個參數(shù)傳了進來,這樣就可以獲取點擊的具體坐標了。
接下來我們調用了 numberOfViewsForHorizontalScroller 方法,HorizontalScroller 不知道自己的 delegate 具體是誰,但是知道它一定實現(xiàn)了 HorizontalScrollerDelegate 協(xié)議,所以可以放心的調用。
對于 scroll view 中的 view ,通過 CGRectContainsPoint 進行點擊檢測,從而獲知是哪一個 view 被點擊了。當找到了點擊的 view 的時候,則會調用委托方法里的 horizontalScrollerClickedViewAtIndex 方法通知委托。在跳出 for 循環(huán)之前,先把點擊到的 view 居中。
接下來我們再加個方法獲取數(shù)組里的 view :
func viewAtIndex(index :Int) -> UIView {
return viewArray[index]
}
這個方法很簡單,只是用來更方便獲取數(shù)組里的 view
而已。在后面實現(xiàn)高亮選中專輯的時候會用到這個方法。
添加如下代碼用來重新加載 scroller
:
func reload() {
// 1 - Check if there is a delegate, if not there is nothing to load.
if let delegate = self.delegate {
//2 - Will keep adding new album views on reload, need to reset.
viewArray = []
let views: NSArray = scroller.subviews
// 3 - remove all subviews
views.enumerateObjectsUsingBlock {
(object: AnyObject!, idx: Int, stop: UnsafeMutablePointer<ObjCBool>) -> Void in
object.removeFromSuperview()
}
// 4 - xValue is the starting point of the views inside the scroller
var xValue = VIEWS_OFFSET
for index in 0..<delegate.numberOfViewsForHorizontalScroller(self) {
// 5 - add a view at the right position
xValue += VIEW_PADDING
let view = delegate.horizontalScrollerViewAtIndex(self, index: index)
view.frame = CGRectMake(CGFloat(xValue), CGFloat(VIEW_PADDING), CGFloat(VIEW_DIMENSIONS), CGFloat(VIEW_DIMENSIONS))
scroller.addSubview(view)
xValue += VIEW_DIMENSIONS + VIEW_PADDING
// 6 - Store the view so we can reference it later
viewArray.append(view)
}
// 7
scroller.contentSize = CGSizeMake(CGFloat(xValue + VIEWS_OFFSET), frame.size.height)
// 8 - If an initial view is defined, center the scroller on it
if let initialView = delegate.initialViewIndex?(self) {
scroller.setContentOffset(CGPointMake(CGFloat(initialView)*CGFloat((VIEW_DIMENSIONS + (2 * VIEW_PADDING))), 0), animated: true)
}
}
}
這個 reload 方法有點像是 UITableView 里面的 reloadData 方法,它會重新加載所有數(shù)據(jù)。
一段一段的看下上面的代碼:
當數(shù)據(jù)發(fā)生改變的時候,我們需要調用 reload 方法。當 HorizontalScroller 被加到其他頁面的時候也需要調用這個方法,我們在 HorizontalScroller.swift 里面加入如下代碼:
override func didMoveToSuperview() {
reload()
}
在當前 view 添加到其他 view 里的時候就會自動調用 didMoveToSuperview 方法,這樣可以在正確的時間重新加載數(shù)據(jù)。
HorizontalScroller 的最后一部分是用來確保當前瀏覽的內容時刻位于正中心的位置,為了實現(xiàn)這個功能我們需要在用戶滑動結束的時候做一些額外的計算和修正。
添加下面這個方法:
func centerCurrentView() {
var xFinal = scroller.contentOffset.x + CGFloat((VIEWS_OFFSET/2) + VIEW_PADDING)
let viewIndex = xFinal / CGFloat((VIEW_DIMENSIONS + (2*VIEW_PADDING)))
xFinal = viewIndex * CGFloat(VIEW_DIMENSIONS + (2*VIEW_PADDING))
scroller.setContentOffset(CGPointMake(xFinal, 0), animated: true)
if let delegate = self.delegate {
delegate.horizontalScrollerClickedViewAtIndex(self, index: Int(viewIndex))
}
}
上面的代碼計算了當前視圖里中心位置距離多少,然后算出正確的居中坐標并滑動到那個位置。最后一行是通知委托所選視圖已經(jīng)發(fā)生了改變。
為了檢測到用戶滑動的結束時間,我們還需要實現(xiàn) UIScrollViewDelegate 的方法。在文件結尾加上下面這個擴展:
extension HorizontalScroller: UIScrollViewDelegate {
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
centerCurrentView()
}
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
centerCurrentView()
}
}
當用戶停止滑動的時候,scrollViewDidEndDragging(_:willDecelerate:) 這個方法會通知委托。如果滑動還沒有停止,decelerate 的值為 true 。當滑動完全結束的時候,則會調用 scrollViewDidEndDecelerating 這個方法。在這兩種情況下,你都應該把當前的視圖居中,因為用戶的操作可能會改變當前視圖。
你的 HorizontalScroller 已經(jīng)可以使用了!回頭看看前面寫的代碼,你會看到我們并沒有涉及什么 Album 或者 AlbumView 的代碼。這是極好的,因為這樣意味著這個 scroller 是完全獨立的,可以復用。
運行一下你的項目,確保編譯通過。
這樣,我們的 HorizontalScroller 就完成了,接下來我們就要把它應用到我們的項目里了。首先,打開 Main.Sstoryboard 文件,點擊上面的灰色矩形,設置 Class 為 HorizontalScroller :
接下來,在 assistant editor 模式下向 ViewController.swift 拖拽生成 outlet ,命名為 scroller :
接下來打開 ViewController.swift 文件,是時候實現(xiàn) HorizontalScrollerDelegate 委托里的方法啦!
添加如下擴展:
extension ViewController: HorizontalScrollerDelegate {
func horizontalScrollerClickedViewAtIndex(scroller: HorizontalScroller, index: Int) {
//1
let previousAlbumView = scroller.viewAtIndex(currentAlbumIndex) as AlbumView
previousAlbumView.highlightAlbum(didHighlightView: false)
//2
currentAlbumIndex = index
//3
let albumView = scroller.viewAtIndex(index) as AlbumView
albumView.highlightAlbum(didHighlightView: true)
//4
showDataForAlbum(index)
}
}
讓我們一行一行的看下這個委托的實現(xiàn):
接下來在擴展里添加如下方法:
func numberOfViewsForHorizontalScroller(scroller: HorizontalScroller) -> (Int) {
return allAlbums.count
}
這個委托方法返回 scroll vew
里面的視圖數(shù)量,因為是用來展示所有的專輯的封面,所以數(shù)目也就是專輯數(shù)目。
然后添加如下代碼:
func horizontalScrollerViewAtIndex(scroller: HorizontalScroller, index: Int) -> (UIView) {
let album = allAlbums[index]
let albumView = AlbumView(frame: CGRectMake(0, 0, 100, 100), albumCover: album.coverUrl)
if currentAlbumIndex == index {
albumView.highlightAlbum(didHighlightView: true)
} else {
albumView.highlightAlbum(didHighlightView: false)
}
return albumView
}
我們創(chuàng)建了一個新的 AlbumView
,然后檢查一下是不是當前選中的專輯,如果是則設為高亮,最后返回結果。
是的就是這么簡單!三個方法,完成了一個橫向滾動的瀏覽視圖。
我們還需要創(chuàng)建這個滾動視圖并把它加到主視圖里,但是在這之前,先添加如下方法:
func reloadScroller() {
allAlbums = LibraryAPI.sharedInstance.getAlbums()
if currentAlbumIndex < 0 {
currentAlbumIndex = 0
} else if currentAlbumIndex >= allAlbums.count {
currentAlbumIndex = allAlbums.count - 1
}
scroller.reload()
showDataForAlbum(currentAlbumIndex)
}
這個方法通過 LibraryAPI
加載專輯數(shù)據(jù),然后根據(jù) currentAlbumIndex
的值設置當前視圖。在設置之前先進行了校正,如果小于0則設置第一個專輯為展示的視圖,如果超出了范圍則設置最后一個專輯為展示的視圖。
接下來只需要指定委托就可以了,在 viewDidLoad
最后加入一下代碼:
scroller.delegate = self
reloadScroller()
因為 HorizontalScroller
是在 StoryBoard
里初始化的,所以我們需要做的只是指定委托,然后調用 reloadScroller()
方法,從而加載所有的子視圖并且展示專輯數(shù)據(jù)。
標注:如果協(xié)議里的方法過多,可以考慮把它分解成幾個更小的協(xié)議。UITableViewDelegate
和 UITableViewDataSource
就是很好的例子,它們都是 UITableView
的協(xié)議。嘗試去設計你自己的協(xié)議,讓每個協(xié)議都單獨負責一部分功能。
運行一下當前項目,看一下我們的新頁面:
等下,滾動視圖顯示出來了,但是專輯的封面怎么不見了?
啊哈,是的。我們還沒完成下載部分的代碼,我們需要添加下載圖片的方法。因為我們所有的訪問都是通過 LibraryAPI
實現(xiàn)的,所以很顯然我們下一步應該去完善這個類了。不過在這之前,我們還需要考慮一些問題:
AlbumView
不應該直接和 LibraryAPI
交互,我們不應該把視圖的邏輯和業(yè)務邏輯混在一起。LibraryAPI
也不應該知道 AlbumView
這個類。AlbumView
要展示封面,LibraryAPI
需要告訴 AlbumView
圖片下載完成。看起來好像很難的樣子?別絕望,接下來我們會用觀察者模式 (Observer Pattern
) 解決這個問題!
更多建議: