(闊別一個多月。。終於完成了。。)html
翻譯自 Introducing iOS Design Patterns in Swift – Part 2/2 ,本教程 objc 版本的做者是 Eli Ganem ,由 Vincent Ngo 更新爲 Swift 版本。ios
歡迎來到教程的第二部分!這是本系列教程的最後一部分,在這一章的學習裏,咱們會更加深刻的學習一些 iOS 開發中常見的設計模式:適配器模式 (Adapter),觀察者模式 (Observer),備忘錄模式 (Memento)。git
開始吧少年們!github
你能夠先下載上一章結束時的項目源碼 。編程
在第一部分的教程裏,咱們完成了這樣一個簡單的應用:swift
咱們的原計劃是在上面的空白處放一個能夠橫滑瀏覽專輯的視圖。其實仔細想一想,這個控件是能夠應用在其餘地方的,咱們不妨把它作成一個可複用的視圖。設計模式
爲了讓這個視圖能夠複用,顯示內容的工做都只能交給另外一個對象來完成:它的委託。這個橫滑頁面應該聲明一些方法讓它的委託去實現,就像是 UITableView
的 UITableViewDelegate
同樣。咱們將會在下一個設計模式中實現這個功能。數組
適配器把本身封裝起來而後暴露統一的接口給其餘類,這樣即便其餘類的接口各不相同,也能相安無事,一塊兒工做。緩存
若是你熟悉適配器模式,那麼你會發現蘋果在實現適配器模式的方式稍有不一樣:蘋果經過委託實現了適配器模式。委託相信你們都不陌生。舉個例子,若是一個類遵循了 NSCoying
的協議,那麼它必定要實現 copy
方法。網絡
橫滑的滾動欄理論上應該是這個樣子的:
新建一個 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]()
上面標註的三點分別作了這些事情:
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
:
接下來,在 assistant editor
模式下向 ViewController.swift
拖拽生成 outlet ,命名爲 scroller
:
接下來打開 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
的協議。嘗試去設計你本身的協議,讓每一個協議都單獨負責一部分功能。
運行一下當前項目,看一下咱們的新頁面:
等下,滾動視圖顯示出來了,可是專輯的封面怎麼不見了?
啊哈,是的。咱們還沒完成下載部分的代碼,咱們須要添加下載圖片的方法。由於咱們全部的訪問都是經過 LibraryAPI
實現的,因此很顯然咱們下一步應該去完善這個類了。不過在這以前,咱們還須要考慮一些問題:
AlbumView
不該該直接和 LibraryAPI
交互,咱們不該該把視圖的邏輯和業務邏輯混在一塊兒。LibraryAPI
也不該該知道 AlbumView
這個類。AlbumView
要展現封面,LibraryAPI
須要告訴 AlbumView
圖片下載完成。看起來好像很難的樣子?別絕望,接下來咱們會用觀察者模式 (Observer Pattern
) 解決這個問題!:]
在觀察者模式裏,一個對象在狀態變化的時候會通知另外一個對象。參與者並不須要知道其餘對象的具體是幹什麼的 - 這是一種下降耦合度的設計。這個設計模式經常使用於在某個屬性改變的時候通知關注該屬性的對象。
常見的使用方法是觀察者註冊監聽,而後再狀態改變的時候,全部觀察者們都會收到通知。
在 MVC 裏,觀察者模式意味着須要容許 Model
對象和 View
對象進行交流,而不能有直接的關聯。
Cocoa
使用兩種方式實現了觀察者模式: Notification
和 Key-Value Observing (KVO)
。
不要把這裏的通知和推送通知或者本地通知搞混了,這裏的通知是基於訂閱-發佈模型的,即一個對象 (發佈者) 向其餘對象 (訂閱者) 發送消息。發佈者永遠不須要知道訂閱者的任何數據。
Apple
對於通知的使用很頻繁,好比當鍵盤彈出或者收起的時候,系統會分別發送 UIKeyboardWillShowNotification/UIKeyboardWillHideNotification
的通知。當你的應用切到後臺的時候,又會發送 UIApplicationDidEnterBackgroundNotification
的通知。
注意:打開 UIApplication.swift
文件,在文件結尾你會看到二十多種系統發送的通知。
打開 AlbumView.swift
而後在 init
的最後插入以下代碼:
NSNotificationCenter.defaultCenter().postNotificationName("BLDownloadImageNotification", object: self, userInfo: ["imageView":coverImage, "coverUrl" : albumCover])
這行代碼經過 NSNotificationCenter
發送了一個通知,通知信息包含了 UIImageView
和圖片的下載地址。這是下載圖像須要的全部數據。
而後在 LibraryAPI.swift
的 init
方法的 super.init()
後面加上以下代碼:
NSNotificationCenter.defaultCenter().addObserver(self, selector:"downloadImage:", name: "BLDownloadImageNotification", object: nil)
這是等號的另外一邊:觀察者。每當 AlbumView
發出一個 BLDownloadImageNotification
通知的時候,因爲 LibraryAPI
已經註冊了成爲觀察者,因此係統會調用 downloadImage()
方法。
可是,在實現 downloadImage()
以前,咱們必須先在 dealloc
裏取消監聽。若是沒有取消監聽消息,消息會發送給一個已經銷燬的對象,致使程序崩潰。
在 LibaratyAPI.swift
里加上取消訂閱的代碼:
deinit { NSNotificationCenter.defaultCenter().removeObserver(self) }
當對象銷燬的時候,把它從全部消息的訂閱列表裏去除。
這裏還要作一件事情:咱們最好把圖片存儲到本地,這樣能夠避免一次又一次下載相同的封面。
打開 PersistencyManager.swift
添加以下代碼:
func saveImage(image: UIImage, filename: String) { let path = NSHomeDirectory().stringByAppendingString("/Documents/\(filename)") let data = UIImagePNGRepresentation(image) data.writeToFile(path, atomically: true) } func getImage(filename: String) -> UIImage? { var error: NSError? let path = NSHomeDirectory().stringByAppendingString("/Documents/\(filename)") let data = NSData(contentsOfFile: path, options: .UncachedRead, error: &error) if let unwrappedError = error { return nil } else { return UIImage(data: data!) } }
代碼很簡單直接,下載的圖片會存儲在 Documents
目錄下,若是沒有檢查到緩存文件, getImage()
方法則會返回 nil
。
而後在 LibraryAPI.swift
添加以下代碼:
func downloadImage(notification: NSNotification) { //1 let userInfo = notification.userInfo as [String: AnyObject] var imageView = userInfo["imageView"] as UIImageView? let coverUrl = userInfo["coverUrl"] as NSString //2 if let imageViewUnWrapped = imageView { imageViewUnWrapped.image = persistencyManager.getImage(coverUrl.lastPathComponent) if imageViewUnWrapped.image == nil { //3 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in let downloadedImage = self.httpClient.downloadImage(coverUrl) //4 dispatch_sync(dispatch_get_main_queue(), { () -> Void in imageViewUnWrapped.image = downloadedImage self.persistencyManager.saveImage(downloadedImage, filename: coverUrl.lastPathComponent) }) }) } } }
拆解一下上面的代碼:
downloadImage
經過通知調用,因此這個方法的參數就是 NSNotification
自己。 UIImageView
和 URL
均可以從其中獲取到。PersistencyManager
裏獲取緩存。HTTPClient
獲取。PersistencyManager
存儲到本地。再回顧一下,咱們使用外觀模式隱藏了下載圖片的複雜程度。通知的發送者並不在意圖片是如何從網上下載到本地的。
運行一下項目,能夠看到專輯封面已經顯示出來了:
關了應用再從新運行,注意此次沒有任何延時就顯示了全部的圖片,由於咱們已經有了本地緩存。咱們甚至能夠在沒有網絡的狀況下正常使用咱們的應用。不過出了問題:這個用來提示加載網絡請求的小菊花怎麼一直在顯示!
咱們在下載圖片的時候開啓了這個白色小菊花,可是在圖片下載完畢的時候咱們並無停掉它。咱們能夠在每次下載成功的時候發送一個通知,可是咱們不這樣作,此次咱們來用用另外一個觀察者模式: KVO 。
在 KVO 裏,對象能夠註冊監放任何屬性的變化,無論它是否持有。若是感興趣的話,能夠讀一讀蘋果 KVO 編程指南。
正如前面所說起的, 對象能夠關注任何屬性的變化。在咱們的例子裏,咱們能夠用 KVO 關注 UIImageView
的 image
屬性變化。
打開 AlbumView.swift
文件,找到 init(frame:albumCover:)
方法,在把 coverImage
添加到 subView
的代碼後面添加以下代碼:
coverImage.addObserver(self, forKeyPath: "image", options: nil, context: nil)
這行代碼把 self
(也就是當前類) 添加到了 coverImage
的 image
屬性的觀察者裏。
在銷燬的時候,咱們也須要取消觀察。仍是在 AlbumView.swift
文件裏,添加以下代碼:
deinit { coverImage.removeObserver(self, forKeyPath: "image") }
最終添加以下方法:
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) { if keyPath == "image" { indicator.stopAnimating() } }
必須在全部的觀察者裏實現上面的代碼。在檢測到屬性變化的時候,系統會自動調用這個方法。在上面的代碼裏,咱們在圖片加載完成的時候把那個提示加載的小菊花去掉了。
再次運行項目,你會發現一切正常了:
注意:必定要記得移除觀察者,不然若是對象已經銷燬了還給它發送消息會致使應用崩潰。
此時你能夠把玩一下當前的應用而後再關掉它,你會發現你的應用的狀態並無存儲下來。最後看見的專輯並不會再下次打開應用的時候出現。
爲了解決這個問題,咱們可使用下一種模式:備忘錄模式。
備忘錄模式捕捉而且具象化一個對象的內在狀態。換句話說,它把你的對象存在了某個地方,而後在之後的某個時間再把它恢復出來,而不會打破它自己的封裝性,私有數據依舊是私有數據。
在 ViewController.swift
里加上下面兩個方法:
//MARK: Memento Pattern func saveCurrentState() { // When the user leaves the app and then comes back again, he wants it to be in the exact same state // he left it. In order to do this we need to save the currently displayed album. // Since it's only one piece of information we can use NSUserDefaults. NSUserDefaults.standardUserDefaults().setInteger(currentAlbumIndex, forKey: "currentAlbumIndex") } func loadPreviousState() { currentAlbumIndex = NSUserDefaults.standardUserDefaults().integerForKey("currentAlbumIndex") showDataForAlbum(currentAlbumIndex) }
saveCurrentState
把當前相冊的索引值存到 NSUserDefaults
裏。NSUserDefaults
是 iOS 提供的一個標準存儲方案,用於保存應用的配置信息和數據。
loadPreviousState
方法加載上次存儲的索引值。這並非備忘錄模式的完整實現,可是已經離目標不遠了。
接下來在 viewDidLoad
的 scroller.delegate = self
前面調用:
loadPreviousState()
這樣在剛初始化的時候就加載了上次存儲的狀態。可是何時存儲當前狀態呢?這個時候咱們能夠用通知來作。在應用進入到後臺的時候, iOS 會發送一個 UIApplicationDidEnterBackgroundNotification
的通知,咱們能夠在這個通知裏調用 saveCurrentState
這個方法。是否是很方便?
在 viewDidLoad
的最後加上以下代碼:
NSNotificationCenter.defaultCenter().addObserver(self, selector:"saveCurrentState", name: UIApplicationDidEnterBackgroundNotification, object: nil)
如今,當應用即將進入後臺的時候,ViewController
會調用 saveCurrentState
方法自動存儲當前狀態。
固然也別忘了取消監聽通知,添加以下代碼:
deinit { NSNotificationCenter.defaultCenter().removeObserver(self) }
這樣就確保在 ViewController
銷燬的時候取消監聽通知。
這時再運行程序,隨意移到某個專輯上,而後按下 Home 鍵把應用切換到後臺,再在 Xcode 上把 App 關閉。從新啓動,會看見上次記錄的專輯已經存了下來併成功還原了:
看起來專輯數據好像是對了,可是上面的滾動條彷佛出了問題,沒有居中啊!
這時 initialViewIndex
方法就派上用場了。因爲在委託裏 (也就是 ViewController
) 還沒實現這個方法,因此初始化的結果老是第一張專輯。
爲了修復這個問題,咱們能夠在 ViewController.swift
裏添加以下代碼:
func initialViewIndex(scroller: HorizontalScroller) -> Int { return currentAlbumIndex }
如今 HorizontalScroller
能夠根據 currentAlbumIndex
自動滑到相應的索引位置了。
再次重複上次的步驟,切到後臺,關閉應用,重啓,一切順利:
回頭看看 PersistencyManager
的 init
方法,你會發現專輯數據是咱們硬編碼寫進去的,並且每次建立 PersistencyManager
的時候都會再建立一次專輯數據。而實際上一個比較好的方案是隻建立一次,而後把專輯數據存到本地文件裏。咱們如何把專輯數據存到文件裏呢?
一種方案是遍歷 Album
的屬性而後把它們寫到一個 plist
文件裏,而後若是須要的時候再從新建立 Album
對象。這並非最好的選擇,由於數據和屬性不一樣,你的代碼也就要相應的產生變化。舉個例子,若是咱們之後想添加 Movie
對象,它有着徹底不一樣的屬性,那麼存儲和讀取數據又須要重寫新的代碼。
何況你也沒法存儲這些對象的私有屬性,由於其餘類是沒有訪問權限的。這也就是爲何 Apple 提供了 歸檔 的機制。
蘋果經過歸檔的方法來實現備忘錄模式。它把對象轉化成了流而後在不暴露內部屬性的狀況下存儲數據。你能夠讀一讀 《iOS 6 by Tutorials》 這本書的第 16 章,或者看下蘋果的歸檔和序列化文檔。
首先,咱們須要讓 Album
實現 NSCoding
協議,聲明這個類是可被歸檔的。打開 Album.swift
在 class
那行後面加上 NSCoding
:
class Album: NSObject, NSCoding {
而後添加以下的兩個方法:
required init(coder decoder: NSCoder) { super.init() self.title = decoder.decodeObjectForKey("title") as String? self.artist = decoder.decodeObjectForKey("artist") as String? self.genre = decoder.decodeObjectForKey("genre") as String? self.coverUrl = decoder.decodeObjectForKey("cover_url") as String? self.year = decoder.decodeObjectForKey("year") as String? } func encodeWithCoder(aCoder: NSCoder) { aCoder.encodeObject(title, forKey: "title") aCoder.encodeObject(artist, forKey: "artist") aCoder.encodeObject(genre, forKey: "genre") aCoder.encodeObject(coverUrl, forKey: "cover_url") aCoder.encodeObject(year, forKey: "year") }
encodeWithCoder
方法是 NSCoding
的一部分,在被歸檔的時候調用。相對的, init(coder:)
方法則是用來解檔的。很簡單,很強大。
如今 Album
對象能夠被歸檔了,添加一些代碼來存儲和加載 Album
數據。
在 PersistencyManager.swift
裏添加以下代碼:
func saveAlbums() { var filename = NSHomeDirectory().stringByAppendingString("/Documents/albums.bin") let data = NSKeyedArchiver.archivedDataWithRootObject(albums) data.writeToFile(filename, atomically: true) }
這個方法能夠用來存儲專輯。 NSKeyedArchiver
把專輯數組歸檔到了 albums.bin
這個文件裏。
當咱們歸檔一個包含子對象的對象時,系統會自動遞歸的歸檔子對象,而後是子對象的子對象,這樣一層層遞歸下去。在咱們的例子裏,咱們歸檔的是 albums
由於 Array
和 Album
都是實現 NSCopying
接口的,因此數組裏的對象均可以自動歸檔。
用下面的代碼取代 PersistencyManager
中的 init
方法:
override init() { super.init() if let data = NSData(contentsOfFile: NSHomeDirectory().stringByAppendingString("/Documents/albums.bin")) { let unarchiveAlbums = NSKeyedUnarchiver.unarchiveObjectWithData(data) as [Album]? if let unwrappedAlbum = unarchiveAlbums { albums = unwrappedAlbum } } else { createPlaceholderAlbum() } } func createPlaceholderAlbum() { //Dummy list of albums let album1 = Album(title: "Best of Bowie", artist: "David Bowie", genre: "Pop", coverUrl: "http://www.coversproject.com/static/thumbs/album/album_david%20bowie_best%20of%20bowie.png", year: "1992") let album2 = Album(title: "It's My Life", artist: "No Doubt", genre: "Pop", coverUrl: "http://www.coversproject.com/static/thumbs/album/album_no%20doubt_its%20my%20life%20%20bathwater.png", year: "2003") let album3 = Album(title: "Nothing Like The Sun", artist: "Sting", genre: "Pop", coverUrl: "http://www.coversproject.com/static/thumbs/album/album_sting_nothing%20like%20the%20sun.png", year: "1999") let album4 = Album(title: "Staring at the Sun", artist: "U2", genre: "Pop", coverUrl: "http://www.coversproject.com/static/thumbs/album/album_u2_staring%20at%20the%20sun.png", year: "2000") let album5 = Album(title: "American Pie", artist: "Madonna", genre: "Pop", coverUrl: "http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png", year: "2000") albums = [album1, album2, album3, album4, album5] saveAlbums() }
咱們把建立專輯數據的方法放到了 createPlaceholderAlbum
裏,這樣代碼可讀性更高。在新的代碼裏,若是存在歸檔文件, NSKeyedUnarchiver
從歸檔文件加載數據;不然就建立歸檔文件,這樣下次程序啓動的時候能夠讀取本地文件加載數據。
咱們還想在每次程序進入後臺的時候存儲專輯數據。看起來如今這個功能並非必須的,可是若是之後咱們加了編輯功能,這樣作仍是頗有必要的,那時咱們確定但願確保新的數據會同步到本地的歸檔文件。
由於咱們的程序經過 LibraryAPI
來訪問全部服務,因此咱們須要經過 LibraryAPI
來通知 PersistencyManager
存儲專輯數據。
在 LibraryAPI
裏添加存儲專輯數據的方法:
func saveAlbums() { persistencyManager.saveAlbums() }
這個方法很簡單,就是把 LibraryAPI
的 saveAlbums
方法傳遞給了 persistencyManager
的 saveAlbums
方法。
而後在 ViewController.swift
的 saveCurrentState
方法的最後加上:
LibraryAPI.sharedInstance.saveAlbums()
在 ViewController
須要存儲狀態的時候,上面的代碼經過 LibraryAPI
歸檔當前的專輯數據。
運行一下程序,檢查一下沒有編譯錯誤。
不幸的是彷佛沒什麼簡單的方法來檢查歸檔是否正確完成。你能夠檢查一下 Documents
目錄,看下是否存在歸檔文件。若是要查看其餘數據變化的話,還須要添加編輯專輯數據的功能。
不過和編輯數據相比,彷佛加個刪除專輯的功能更好一點,若是不想要這張專輯直接刪除便可。再進一步,萬一誤刪了話,是否是還能夠再加個撤銷按鈕?
如今咱們將添加最後一個功能:容許用戶刪除專輯,以及撤銷上次的刪除操做。
在 ViewController
裏添加以下屬性:
// 爲了實現撤銷功能,咱們用數組做爲一個棧來 push 和 pop 用戶的操做 var undoStack: [(Album, Int)] = []
而後在 viewDidLoad
的 reloadScroller()
後面添加以下代碼:
let undoButton = UIBarButtonItem(barButtonSystemItem: .Undo, target: self, action:"undoAction") undoButton.enabled = false; let space = UIBarButtonItem(barButtonSystemItem: .FlexibleSpace, target:nil, action:nil) let trashButton = UIBarButtonItem(barButtonSystemItem: .Trash, target:self, action:"deleteAlbum") let toolbarButtonItems = [undoButton, space, trashButton] toolbar.setItems(toolbarButtonItems, animated: true)
上面的代碼建立了一個 toolbar
,上面有兩個按鈕,在 undoStack
爲空的狀況下, undo
的按鈕是不可用的。注意 toolbar
已經在 storyboard
裏了,咱們須要作的只是配置上面的按鈕。
咱們須要在 ViewController.swift
裏添加三個方法,用來處理專輯的編輯事件:增長,刪除,撤銷。
先寫添加的方法:
func addAlbumAtIndex(album: Album,index: Int) { LibraryAPI.sharedInstance.addAlbum(album, index: index) currentAlbumIndex = index reloadScroller() }
作了三件事:添加專輯,設爲當前的索引,從新加載滾動條。
接下來是刪除方法:
func deleteAlbum() { //1 var deletedAlbum : Album = allAlbums[currentAlbumIndex] //2 var undoAction = (deletedAlbum, currentAlbumIndex) undoStack.insert(undoAction, atIndex: 0) //3 LibraryAPI.sharedInstance.deleteAlbum(currentAlbumIndex) reloadScroller() //4 let barButtonItems = toolbar.items as [UIBarButtonItem] var undoButton : UIBarButtonItem = barButtonItems[0] undoButton.enabled = true //5 if (allAlbums.count == 0) { var trashButton : UIBarButtonItem = barButtonItems[2] trashButton.enabled = false } }
挨個看一下各個部分:
undoAction
對象,用元組存儲 Album
對象和它的索引值。而後把這個元組加到了棧裏。LibraryAPI
刪除專輯數據,而後從新加載滾動條。最後添加撤銷按鈕:
func undoAction() { let barButtonItems = toolbar.items as [UIBarButtonItem] //1 if undoStack.count > 0 { let (deletedAlbum, index) = undoStack.removeAtIndex(0) addAlbumAtIndex(deletedAlbum, index: index) } //2 if undoStack.count == 0 { var undoButton : UIBarButtonItem = barButtonItems[0] undoButton.enabled = false } //3 let trashButton : UIBarButtonItem = barButtonItems[2] trashButton.enabled = true }
照着備註的三個步驟再看一下撤銷方法裏的代碼:
pop
出一個對象,這個對象就是咱們當初塞進去的元祖,存有刪除的 Album
對象和它的索引位置。而後咱們把取出來的對象放回了數據源裏。enabled
。這時再運行應用,試試刪除和插銷功能,彷佛一切正常了:
咱們也能夠趁機測試一下,看看是否及時存儲了專輯數據的變化。好比刪除一個專輯,而後切到後臺,強關應用,再從新開啓,看看是否是刪除操做成功保存了。
若是想要恢復全部數據,刪除應用而後從新安裝便可。
最終項目的源代碼能夠在 BlueLibrarySwift-Final 下載。
經過這兩篇設計模式的學習,咱們接觸到了一些基礎的設計模式和概念:Singleton、MVC、Delegation、Protocols、Facade、Observer、Memento 。
這篇文章的目的,並非推崇每行代碼都要用設計模式,而是但願你們在考慮一些問題的時候,能夠參考設計模式提出一些合理的解決方案,尤爲是應用開發的起始階段,思考和設計尤其重要。
若是想繼續深刻學習設計模式,推薦設計模式的經典書籍:Design Patterns: Elements of Reusable Object-Oriented Software。
若是想看更多的設計模式相關的代碼,推薦這個神奇的項目: Swift 實現的種種設計模式。
接下來你能夠看看這篇:Swift 設計模式中級指南,學習更多的設計模式。
玩的開心。 :]
原文連接: