適配器模式

2018-08-12 21:55 更新

適配器模式 - Adapter

適配器把自己封裝起來然后暴露統(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]()

上面標注的三點分別做了這些事情:

  • 定義一個常量,用來方便的改變布局。現(xiàn)在默認的是顯示的內容長寬為100,間隔為10。
  • 創(chuàng)建一個 UIScrollView 作為容器。
  • 創(chuàng)建一個數(shù)組用來存放需要展示的數(shù)據(jù)

接下來實現(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)
}

上面的代碼做了如下工作:

  • 創(chuàng)建一個 UIScrollView 對象并且把它加到父視圖中。
  • 關閉 autoresizing masks ,從而可以使用 AutoLayout 進行布局。
  • scrollview 添加約束。我們希望 scrollview 能填滿 HorizontalScroller 。
  • 創(chuàng)建一個點擊事件,檢測是否點擊到了專輯封面,如果確實點擊到了專輯封面,我們需要通知 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ù)。

一段一段的看下上面的代碼:

  • 在調用 reload 之前,先檢查一下是否有委托。
  • 既然要清除專輯封面,那么也需要重新設置 viewArray ,要不然以前的數(shù)據(jù)會累加進來。
  • 移除先前加入到 scrollview 的子視圖。
  • 所有的 view 都有一個偏移量,目前默認是100,我們可以修改 VIEW_OFFSET 這個常量輕松的修改它。
  • HorizontalScroller 通過委托獲取對應位置的 view 并且把它們放在對應的位置上。
  • 把 view 存進 viewArray 以便后面的操作。
  • 當所有 view 都安放好了,再設置一下 content size 這樣才可以進行滑動。
  • HorizontalScroller 檢查一下委托是否實現(xiàn)了 initialViewIndex() 這個可選方法,這種檢查十分必要,因為這個委托方法是可選的,如果委托沒有實現(xiàn)這個方法則用0作為默認值。最終設置 scroll view 將初始的 view 放置到居中的位置。

當數(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):

  • 獲取上一個選中的相冊,然后取消高亮
  • 存儲當前點擊的相冊封面
  • 獲取當前選中的相冊,設置為高亮
  • 在 table view 里面展示新數(shù)據(jù)

接下來在擴展里添加如下方法:

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é)議。UITableViewDelegateUITableViewDataSource 就是很好的例子,它們都是 UITableView 的協(xié)議。嘗試去設計你自己的協(xié)議,讓每個協(xié)議都單獨負責一部分功能。

運行一下當前項目,看一下我們的新頁面:

等下,滾動視圖顯示出來了,但是專輯的封面怎么不見了?

啊哈,是的。我們還沒完成下載部分的代碼,我們需要添加下載圖片的方法。因為我們所有的訪問都是通過 LibraryAPI 實現(xiàn)的,所以很顯然我們下一步應該去完善這個類了。不過在這之前,我們還需要考慮一些問題:

  • AlbumView 不應該直接和 LibraryAPI 交互,我們不應該把視圖的邏輯和業(yè)務邏輯混在一起。
  • 同樣, LibraryAPI 也不應該知道 AlbumView 這個類。
  • 如果 AlbumView 要展示封面,LibraryAPI 需要告訴 AlbumView 圖片下載完成。

看起來好像很難的樣子?別絕望,接下來我們會用觀察者模式 (Observer Pattern) 解決這個問題!

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號