適配器把自己封裝起來然后暴露統一的接口給其他類,這樣即使其他類的接口各不相同,也能相安無事,一起工作。
如果你熟悉適配器模式,那么你會發現蘋果在實現適配器模式的方式稍有不同:蘋果通過委托實現了適配器模式。委托相信大家都不陌生。舉個例子,如果一個類遵循了?`NSCoying`?的協議,那么它一定要實現?`copy`?方法。
### 如何使用適配器模式
橫滑的滾動欄理論上應該是這個樣子的:
[](http://cdn2.raywenderlich.com/wp-content/uploads/2014/11/swiftDesignPattern7.png)
新建一個 Swift 文件:`HorizontalScroller.swift`?,作為我們的橫滑滾動控件,`HorizontalScroller`?繼承自?`UIView`?。
打開?`HorizontalScroller.swift`?文件并添加如下代碼:
~~~
@objc protocol HorizontalScrollerDelegate {
}
~~~
這行代碼定義了一個新的協議:?`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`?標記的方法是必須實現的,一般來說包括那些用來顯示的必須數據,比如如何展示數據,有多少數據需要展示,點擊事件如何處理等等,不可或缺;有?`option`?標記的方法為可選實現的,相當于是一些輔助設置和功能,就算沒有實現也有默認值進行處理。
在?`HorizontalScroller`?類里添加一個新的委托對象:
~~~
weak var delegate: HorizontalScrollerDelegate?
~~~
為了避免循環引用的問題,委托是?`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]()
~~~
上面標注的三點分別做了這些事情:
* 定義一個常量,用來方便的改變布局。現在默認的是顯示的內容長寬為100,間隔為10。
* 創建一個?`UIScrollView`?作為容器。
* 創建一個數組用來存放需要展示的數據
接下來實現初始化方法:
~~~
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`?作為一個參數傳了進來,這樣就可以獲取點擊的具體坐標了。
接下來我們調用了?`numberOfViewsForHorizontalScroller`?方法,`HorizontalScroller`?不知道自己的?`delegate`?具體是誰,但是知道它一定實現了?`HorizontalScrollerDelegate`?協議,所以可以放心的調用。
對于?`scroll view`?中的?`view`?,通過?`CGRectContainsPoint`?進行點擊檢測,從而獲知是哪一個?`view`?被點擊了。當找到了點擊的?`view`?的時候,則會調用委托方法里的`horizontalScrollerClickedViewAtIndex`?方法通知委托。在跳出?`for`?循環之前,先把點擊到的?`view`?居中。
接下來我們再加個方法獲取數組里的?`view`?:
~~~
func viewAtIndex(index :Int) -> UIView {
return viewArray[index]
}
~~~
這個方法很簡單,只是用來更方便獲取數組里的?`view`?而已。在后面實現高亮選中專輯的時候會用到這個方法。
添加如下代碼用來重新加載?`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`?方法,它會重新加載所有數據。
一段一段的看下上面的代碼:
* 在調用?`reload`?之前,先檢查一下是否有委托。
* 既然要清除專輯封面,那么也需要重新設置?`viewArray`?,要不然以前的數據會累加進來。
* 移除先前加入到?`scrollview`?的子視圖。
* 所有的?`view`?都有一個偏移量,目前默認是100,我們可以修改?`VIEW_OFFSET`?這個常量輕松的修改它。
* `HorizontalScroller`?通過委托獲取對應位置的?`view`?并且把它們放在對應的位置上。
* 把?`view`?存進?`viewArray`?以便后面的操作。
* 當所有?`view`?都安放好了,再設置一下?`content size`?這樣才可以進行滑動。
* `HorizontalScroller`?檢查一下委托是否實現了?`initialViewIndex()`?這個可選方法,這種檢查十分必要,因為這個委托方法是可選的,如果委托沒有實現這個方法則用0作為默認值。最終設置?`scroll view`?將初始的?`view`?放置到居中的位置。
當數據發生改變的時候,我們需要調用?`reload`?方法。當?`HorizontalScroller`?被加到其他頁面的時候也需要調用這個方法,我們在?`HorizontalScroller.swift`?里面加入如下代碼:
~~~
override func didMoveToSuperview() {
reload()
}
~~~
在當前?`view`?添加到其他?`view`?里的時候就會自動調用?`didMoveToSuperview`?方法,這樣可以在正確的時間重新加載數據。
`HorizontalScroller`?的最后一部分是用來確保當前瀏覽的內容時刻位于正中心的位置,為了實現這個功能我們需要在用戶滑動結束的時候做一些額外的計算和修正。
添加下面這個方法:
~~~
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))
}
}
~~~
上面的代碼計算了當前視圖里中心位置距離多少,然后算出正確的居中坐標并滑動到那個位置。最后一行是通知委托所選視圖已經發生了改變。
為了檢測到用戶滑動的結束時間,我們還需要實現?`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`?已經可以使用了!回頭看看前面寫的代碼,你會看到我們并沒有涉及什么?`Album`?或者?`AlbumView`?的代碼。這是極好的,因為這樣意味著這個?`scroller`?是完全獨立的,可以復用。
運行一下你的項目,確保編譯通過。
這樣,我們的?`HorizontalScroller`?就完成了,接下來我們就要把它應用到我們的項目里了。首先,打開?`Main.Sstoryboard`?文件,點擊上面的灰色矩形,設置?`Class`?為`HorizontalScroller`?:
[](http://cdn5.raywenderlich.com/wp-content/uploads/2014/11/swiftDesignPattwern9-700x414.png)
接下來,在?`assistant editor`?模式下向?`ViewController.swift`?拖拽生成 outlet ,命名為`scroller`?:
[](http://cdn5.raywenderlich.com/wp-content/uploads/2014/11/swiftDesignPattern10-700x411.png)
接下來打開?`ViewController.swift`?文件,是時候實現?`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)
}
}
~~~
讓我們一行一行的看下這個委托的實現:
* 獲取上一個選中的相冊,然后取消高亮
* 存儲當前點擊的相冊封面
* 獲取當前選中的相冊,設置為高亮
* 在?`table view`?里面展示新數據
接下來在擴展里添加如下方法:
~~~
func numberOfViewsForHorizontalScroller(scroller: HorizontalScroller) -> (Int) {
return allAlbums.count
}
~~~
這個委托方法返回?`scroll vew`?里面的視圖數量,因為是用來展示所有的專輯的封面,所以數目也就是專輯數目。
然后添加如下代碼:
~~~
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
}
~~~
我們創建了一個新的?`AlbumView`?,然后檢查一下是不是當前選中的專輯,如果是則設為高亮,最后返回結果。
是的就是這么簡單!三個方法,完成了一個橫向滾動的瀏覽視圖。
我們還需要創建這個滾動視圖并把它加到主視圖里,但是在這之前,先添加如下方法:
~~~
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`?加載專輯數據,然后根據?`currentAlbumIndex`?的值設置當前視圖。在設置之前先進行了校正,如果小于0則設置第一個專輯為展示的視圖,如果超出了范圍則設置最后一個專輯為展示的視圖。
接下來只需要指定委托就可以了,在?`viewDidLoad`?最后加入一下代碼:
~~~
scroller.delegate = self
reloadScroller()
~~~
因為?`HorizontalScroller`?是在?`StoryBoard`?里初始化的,所以我們需要做的只是指定委托,然后調用?`reloadScroller()`?方法,從而加載所有的子視圖并且展示專輯數據。
標注:如果協議里的方法過多,可以考慮把它分解成幾個更小的協議。`UITableViewDelegate`?和`UITableViewDataSource`?就是很好的例子,它們都是?`UITableView`?的協議。嘗試去設計你自己的協議,讓每個協議都單獨負責一部分功能。
運行一下當前項目,看一下我們的新頁面:
[](http://cdn1.raywenderlich.com/wp-content/uploads/2014/11/swiftDesignPattern12-288x320.png)
等下,滾動視圖顯示出來了,但是專輯的封面怎么不見了?
啊哈,是的。我們還沒完成下載部分的代碼,我們需要添加下載圖片的方法。因為我們所有的訪問都是通過?`LibraryAPI`?實現的,所以很顯然我們下一步應該去完善這個類了。不過在這之前,我們還需要考慮一些問題:
* `AlbumView`?不應該直接和?`LibraryAPI`?交互,我們不應該把視圖的邏輯和業務邏輯混在一起。
* 同樣,?`LibraryAPI`?也不應該知道?`AlbumView`?這個類。
* 如果?`AlbumView`?要展示封面,`LibraryAPI`?需要告訴?`AlbumView`?圖片下載完成。
看起來好像很難的樣子?別絕望,接下來我們會用觀察者模式 (`Observer Pattern`) 解決這個問題!:]