你可以下載the project source from the end of part 1與我們共同來探索
這是你在第一部分結(jié)束時完成的音樂庫App樣品
應用程序的最初設計包括在屏幕的頂端上上水平滾動條的專輯切換。但是為什么不重寫它適配所有view,而不是單一的編輯一個簡單的滾動條?
為了使這個view可重用。關于其內(nèi)容的所有決定都應該留給下一個對象:一個委托。水平滾動條要聲明一個delegate implements為了scroller的工作。類似于UITableView delegate。當我們討論設計模式的時候我們會實現(xiàn)這個。
Adapter允許classes與不兼容的接口一起工作。它圍繞一個對象進行封裝,并公開一個標準接口與該對象進行交互。
如果你很熟悉適配器你將注意到,App使用了略微不同的方式去實現(xiàn)它--App通過協(xié)議來實現(xiàn)它。 你可能會感覺它很像UITableViewDelegate,UIScrollViewDelegate, NSCoding 和 NSCopying。作為一個示例,隨著NSCopying協(xié)議發(fā)展,任何class都能提供一個標準的copy方法。
之前提到的horizontal scroller看起來像這個樣子
開始實現(xiàn)它,在Project Navigator點擊View group 選擇New File…并且選擇iOS > Cocoa Touch class然后點擊Next。建立類名為HorizontalScroller并且繼承于UIView。
打開HorizontalScroller.swift并且加入下面的代碼類:
@objc protocol HorizontalScrollerDelegate {
}
這定義了一個協(xié)議名為HorizontalScrollerDelegate。在聲明協(xié)議之前你包括了@objc所以你能使用@optional delegate 方法。像在 Objective-C。
你定義了所需要的并且選中的委托方法將在大括號之間實現(xiàn)。所以添加以下協(xié)議方法
// ask the delegate how many views he wants to present inside the horizontal scroller
func numberOfViewsForHorizontalScroller(scroller: HorizontalScroller) -> Int
// ask the delegate to return the view that should appear at
func horizontalScrollerViewAtIndex(scroller: HorizontalScroller, index:Int) -> UIView
// inform the delegate what the view at has been clicked
func horizontalScrollerClickedViewAtIndex(scroller: HorizontalScroller, index:Int)
// ask the delegate for the index of the initial view to display. this method is optional
// and defaults to 0 if it's not implemented by the delegate
optional func initialViewIndex(scroller: HorizontalScroller) -> Int
這里有需求和可選擇的方法。所需的方法必須由委托來執(zhí)行,通常包含一些數(shù)據(jù),這是絕對必須的。 在這種情況下,所需的細節(jié)是view數(shù)量,特殊的索引視圖,并且挖掘試圖的行為。這里的可選方法是初始視圖。如果沒有實現(xiàn),HorizontalScroller講默認為第一個索引。
在HorizontalScroller.swift,
將下面的代碼添加到HorizontalScroller的定義。
weak var delegate: HorizontalScrollerDelegate?
你上面創(chuàng)建的創(chuàng)建的屬性被定義為弱引用。這是必要的,以防止保留一個周期。如果一類對它的委托有強引用的話,該委托保持強引用并且返回一個標準的類,你的app將發(fā)生內(nèi)存泄露,因為這兩個類將釋放分配給其他的內(nèi)存。所有的屬性在swift中被默認為強引用
委托是可選的,所以有可能使用這個類的人不提供一個委托。但如果他們這樣做,它將使HorizontalScrollerDelegate一致并且你可以確保協(xié)議方法在那里實現(xiàn)。
Add a few more properties to the class:添加一些屬性到類中:
// 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]()
滾動每一個評論塊:
定義常亮,使其易于在設計時修改布局。視圖的尺寸內(nèi)的滾動條是100 x 100的矩形。
創(chuàng)建一個包含試圖的滾動視圖
創(chuàng)建一個擁有專輯封面的數(shù)組
Next you need to implement the initializers. Add the following methods: 下一步你需要執(zhí)行初始化。添加以下方法:
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)
}
initializers delegate必須工作在initializeScrollView()。這是實現(xiàn)方法:
創(chuàng)建一個新的UIScrollView實例并將其添加到父視圖。
關閉自動調(diào)整尺寸。就是這樣,你可以應用你自己的限制。
應用約束到scrollview。完全填滿HorizontalScroller視圖
創(chuàng)建一個gesture收識別。這個收拾識別檢測涉及的滾動試圖。并且檢查相冊封面是否已經(jīng)被竊聽。如果是這樣的話,他會通知 HorizontalScroller delegate
現(xiàn)在添加這個方法。
func scrollerTapped(gesture: UITapGestureRecognizer) {
let location = gesture.locationInView(gesture.view)
if let delegate = delegate {
for index in 0..
let view = scroller.subviews[index] as! UIView
if CGRectContainsPoint(view.frame, location) {
delegate.horizontalScrollerClickedViewAtIndex(self, index: index)
scroller.setContentOffset(CGPoint(x: view.frame.origin.x - self.frame.size.width/2 + view.frame.size.width/2, y: 0), animated:true)
break
}
}
}
}
手勢作為一個參數(shù)傳入你locationInView()。
Next, you invoke numberOfViewsForHorizontalScroller() on the delegate.
接下來,你調(diào)用委托numberOfViewsForHorizontalScroller()。
Next add the following to access an album cover from the scroller:
接下來添加下面的封面專輯
func viewAtIndex(index :Int) -> UIView {
return viewArray[index]
}
viewatindex僅僅返回視圖在一個特定的指數(shù)。使用此方法,以突出顯示專輯封面。
Now add the following code to reload the scroller: 現(xiàn)在,添加下面的代碼加載滾動:
func reload() {
// 1 - Check if there is a delegate, if not there is nothing to load.
if let delegate = delegate {
//2 - Will keep adding new album views on reload, need to reset.
viewArray = []
let views: NSArray = scroller.subviews
// 3 - remove all subviews
for view in views {
view.removeFromSuperview()
}
// 4 - xValue is the starting point of the views inside the scroller
var xValue = VIEWS_OFFSET
for index in 0..
// 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(CGPoint(x: CGFloat(initialView)*CGFloat((VIEW_DIMENSIONS + (2 * VIEW_PADDING))), y: 0), animated: true)
}
}
}
重載方法仿照UITableView中的reloadData;他重新載入所有用于構建水平滾動條的數(shù)據(jù)
逐句詳解:
在我們重新加載之前檢查是否有一個委托.
清理專輯封面, 你需要重置viewArray.
刪除又有之前添加到滾動試圖中的子視圖.
所有的視圖都是從給定的偏移量開始的。目前是100,但它可以很容易地調(diào)整,通過在文件的頂部變化不斷view_offset
的horizontalscroller為代表之一,同時奠定了他們未來彼此水平與先前定義的填充.
在viewarra在存儲視圖中跟蹤滾動視圖子視圖.
一旦所有的視圖都到位,設置的滾動視圖的內(nèi)容偏移,讓用戶滾動通過所有的專輯封面.
當你的數(shù)據(jù)發(fā)生改變時,你可以重新加載。你也需要調(diào)用這個方法時,你horizontalscroller添加到另一個視圖。將下面的代碼添加到horizontalscroller.swift覆蓋后者:
override func didMoveToSuperview() {
reload()
}
didmovetosuperview查看時,它添加到另一個視圖作為一個視圖。重新規(guī)范內(nèi)容正確的時間。
horizontalscroller的最后一塊拼圖是確保你看的專輯總是集中在滾動視圖。這樣做,你將需要執(zhí)行一些計算,當用戶拖動滾動查看他們的手指。
Add the following method: 添加以下方法:
func centerCurrentView() {
var xFinal = Int(scroller.contentOffset.x) + (VIEWS_OFFSET/2) + VIEW_PADDING
let viewIndex = xFinal / (VIEW_DIMENSIONS + (2*VIEW_PADDING))
xFinal = viewIndex * (VIEW_DIMENSIONS + (2*VIEW_PADDING))
scroller.setContentOffset(CGPoint(x: xFinal, y: 0), animated: true)
if let delegate = delegate {
delegate.horizontalScrollerClickedViewAtIndex(self, index: Int(viewIndex))
}
}
上面的代碼考慮到滾動視圖和視圖的當前偏移量,以及視圖的填充量,以便計算當前視圖從中心的距離。最后一行很重要:一次視圖是居中的,然后通知委托,選定以更改的視圖。
發(fā)現(xiàn)用戶在完成拖動滾動視圖,你需要實現(xiàn)一些uiscrollviewdelegate方法。將下面的類擴展添加到文件底部;請記住,這必須在主類聲明的大括號之后添加!
extension HorizontalScroller: UIScrollViewDelegate {
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
centerCurrentView()
}
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
centerCurrentView()
}
}
scrollviewdidenddragging(_:willdecelerate:)通知委托當用戶完成拖動。如果滾動視圖尚未完全停止來參數(shù)是真實的。當滾動行動結(jié)束,該系統(tǒng)調(diào)scrollviewdidenddecelerating。在這兩種情況下,調(diào)用新方法以當前視圖為中心,因為當前視圖可能在用戶拖動滾動視圖后發(fā)生改變。
最后別忘了設定委托。在initializescrollview()添加以下代碼后
scroller.delegate = self;
scroller = UIScrollView():
你的horizontalscroller準備就緒!瀏覽你剛才寫的代碼;你會看到有沒有一個提到的專輯或albumview類。那是很好的,因為這意味著新的滾動條是真正獨立的和可重復使用
Build your project to make sure everything compiles properly. 建立項目,確保所有編譯正確
現(xiàn)在,horizontalscroller是完整的,它的時間來使用它在您的應用程序。首先,打開main.storyboard。點擊頂部灰色的矩形視圖,點擊identity。改變類的名稱來horizontalscroller如下所示:
接下來,打開助理編輯和控制從灰色的矩形視圖拖到viewcontroller.swift創(chuàng)建一個出口。名稱出口滾動,如下圖所示:
接下來,打開viewcontroller.swift?,F(xiàn)在是時候開始實施的一些horizontalscrollerdelegate方法!
Add the following extension to the bottom of the file: 將下列擴展名添加到文件底部:
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)
}
}
讓我們以下列的方式去執(zhí)行委托方法吧:: 1
首先選定以前的專輯,然后取消選擇專輯封面
Display the data for the new album within the table view.
存儲當前點擊的相冊封面索引
抓住當前選定的相冊封面,并突出顯示選擇。.
在表視圖中顯示新相冊的數(shù)據(jù).
func numberOfViewsForHorizontalScroller(scroller: HorizontalScroller) -> (Int) {
return allAlbums.count
}
正如你所認識的,這是一種在滾動視圖中返回視圖的方法。由于滾動視圖將顯示所有的專輯數(shù)據(jù)的封面,count是專輯記錄的數(shù)量。
Now, add this code:、 現(xiàn)在,添加此代碼:
func horizontalScrollerViewAtIndex(scroller: HorizontalScroller, index: Int) -> (UIView) {
let album = allAlbums[index]
let albumView = AlbumView(frame: CGRect(x: 0, y: 0, width: 100, height: 100), albumCover: album.coverUrl)
if currentAlbumIndex == index {
albumView.highlightAlbum(didHighlightView: true)
} else {
albumView.highlightAlbum(didHighlightView: false)
}
return albumView
}
在這里,你創(chuàng)建一個新的albumview,接下來檢查用戶是否選擇這張專輯。然后,您可以將其設置為突出顯示或不取決于是否選擇相冊。最后,你通過它的horizontalscroller。
這是它!僅僅三個短的方法顯示一個漂亮的水平滾動條的方法。
是的,你還需要創(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)
}
此方法加載相冊數(shù)據(jù)通過libraryapi然后設置當前顯示基于當前視圖索引的當前值。如果當前視圖索引小于0,則表示當前沒有選擇視圖,然后在列表中顯示的第一張專輯。否則,最后一張專輯被顯示。
scroller.delegate = self
reloadScroller()
由于horizontalscroller是創(chuàng)建在storyboard中,所有你需要做的是設置代理,叫reloadscroller(),將負荷的滾動條來顯示專輯數(shù)據(jù)視圖。
由于horizontalscroller是創(chuàng)建在storyboard中,所有你需要做的是設置代理,叫reloadscroller(),將負荷的滾動條來顯示專輯數(shù)據(jù)視圖。
編譯和運行你的項目,新的水平滾動條,看看:
在觀察者模式,一個對象的狀態(tài)變化通知其他任何對象。所涉及的對象不需要知道彼此的,從而鼓勵一個解耦設計。當一個屬性發(fā)生改變時,這個模式最常用于通知感興趣的對象。
通常的實現(xiàn)要求一個觀察者在另一個對象的狀態(tài)寄存器。當狀態(tài)發(fā)生改變時,所有的觀察對象都會被通知。
如果你想堅持MVC的概念(提示:你這樣做),你需要讓模型對象和視圖對象溝通,但他們之間沒有直接的參考。這就是觀察者模式的所在。
Cocoa在兩個熟悉的方式實現(xiàn)觀察者模式:通知和鍵值觀察(KVO)。
不要被混淆與推送本地通知,通知是基于訂閱和發(fā)布模式,允許對象(發(fā)行商)發(fā)送消息到其他對象(用戶/聽眾)。出版商從不需要了解有關用戶的任何事情。
通知被蘋果嚴重使用。例如,當鍵盤顯示/隱藏系統(tǒng)發(fā)送uikeyboardwillshownotification / uikeyboardwillhidenotification,分別。當你的應用程序進入后臺,系統(tǒng)將一個uiapplicationdidenterbackgroundnotification通知。
去albumview.swift在初始化結(jié)束中插入下面的代碼(框架:CGRect,albumcover:初始化字符串):
NSNotificationCenter.defaultCenter().postNotificationName("BLDownloadImageNotification", object: self, userInfo: ["imageView":coverImage, "coverUrl" : albumCover])
這條線穿過NSNotificationCenter發(fā)送通知單。通知信息包含在UIImageView和封面圖像被下載的URL。這是所有的信息,您需要執(zhí)行的封面下載任務。
在libraryapi.swift init中,直接在super.init()后面添加下面一行
NSNotificationCenter.defaultCenter().addObserver(self, selector:"downloadImage:", name: "BLDownloadImageNotification", object: nil
這是等式的另一邊:觀察者。每一次albumview類崗位bldownloadimagenotification通知,自libraryapi注冊同一通知觀察者,系統(tǒng)會通知libraryapi。然后libraryapi通知downloadimage()響應。
然而,在你實現(xiàn)downloadimage()你要記得退訂此通知時候釋放。如果你不正確的退訂通知你們班登記,通知會發(fā)送到回收實例。這可能會導致應用程序崩潰。
Add the following method to LibraryAPI.swift: 添加下面的方法到libraryapi.swift:
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
當這個對象被釋放,它使自己從所有通知已注冊的觀察者。
還有一件事要做。這可能是一個好主意,以節(jié)省下載資源并且覆蓋本地,所以應用程序?qū)⒉恍枰螺d相同的蓋過一遍又一遍。
打開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!)
}
}
這個代碼非常簡單。下載的圖片將被保存在文件目錄,并將getimage()如果匹配的文件不在文件目錄中找到返回nil。
Now add the following method to LibraryAPI.swift: 現(xiàn)在添加下面的方法來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! String
//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 as String)
//4
dispatch_sync(dispatch_get_main_queue(), { () -> Void in
imageViewUnWrapped.image = downloadedImage
self.persistencyManager.saveImage(downloadedImage, filename: coverUrl.lastPathComponent)
})
})
}
}
}
再次,你使用的是隱藏的復雜性,從其他類下載圖像的外觀模式。通知發(fā)件人不關心圖像來自網(wǎng)絡或文件系統(tǒng)。
建立并運行你的應用程序看看美麗的覆蓋在你的horizontalscroller:
停止應用程序和運行它。注意,有沒有延遲加載的封面,因為他們已經(jīng)保存在本地。你甚至可以斷開互聯(lián)網(wǎng)和您的應用程序?qū)⒄9ぷ?。然而,有一個奇怪的點這里:微調(diào)從未停止旋轉(zhuǎn)!發(fā)生了什么事?
你開始旋轉(zhuǎn)時下載的圖像,但是你還沒有實現(xiàn)的邏輯映像下載完成后停止旋轉(zhuǎn)。你可以發(fā)送一個通知,每一次的圖像已被下載,但相反的,你會使用其他觀察者模式,KVO。
Key-Value Observing (KVO) 鍵值觀察(KVO)
在KVO,對象可以被通知到一個特定的財產(chǎn)的任何變化;要么自己或另一個對象。如果你有興趣,你可以更多地了解這個Apple’s KVO Programming Guide。
How to Use the KVO Pattern 如何使用KVO模式
如上所述,該KVO機制允許一個對象觀察變化的屬性。在你的情況,你可以使用KVO觀察到保存圖像的UIImageView圖像屬性。
打開albumview.swift并添加以下代碼以init(框架:albumcover:),只在你添加載體圖像作為子視圖:
coverImage.addObserver(self, forKeyPath: "image", options: nil, context: nil)
這增加了self,這是當前類,對載體圖像圖像特性觀察。
你還需要注銷作為觀察者,仍在albumview.swift,添加以下代碼:
deinit {
coverImage.removeObserver(self, forKeyPath: "image")
}
最后添加此方法
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer) {
if keyPath == "image" {
indicator.stopAnimating()
}
}
你必須在每一個類中實現(xiàn)這種方法作為一個觀察者。系統(tǒng)每一次都會有一次觀測到的性能變化來執(zhí)行這個方法。在上面的代碼中,你停止旋轉(zhuǎn)時的“形象”性質(zhì)的變化。這樣,當一個圖像被加載,微調(diào)將停止轉(zhuǎn)動。
建設和運行您的項目。微調(diào)應該消失:
如果你適當使用和終止它,你會注意到,你的應用程序的狀態(tài)沒有保存。你看的最后一張專輯當應用程序啟動時不會是默認的相冊。
備忘錄模式捕捉和表現(xiàn)對象的內(nèi)部狀態(tài)。換句話說,它可以節(jié)省你的東西。后來,這種外在的狀態(tài)可以在不破壞封裝恢復;即,私有數(shù)據(jù)保密。
在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保存當前專輯指數(shù)NSUserDefaults – NSUserDefaults是一種標準的數(shù)據(jù)存儲提供的iOS應用程序特定的設置和數(shù)據(jù)保存。
loadpreviousstate加載以前保存的指標。這不是該備忘錄模式實現(xiàn)比較充分的,但是你要有。
現(xiàn)在,添加下面一行在viewcontroller.swift viewDidLoad前scroller.delegate =self:
loadPreviousState()
當應用程序啟動時加載先前保存的狀態(tài)。但是,你從哪里來拯救這個應用程序的當前狀態(tài)?你會使用通知來做這個。iOS發(fā)送uiapplicationdidenterbackgroundnotification通知當應用程序進入后臺。你可以使用該通知稱savecurrentstate。那不方便嗎?
Add the following line to the end of viewDidLoad: 添加下面一行到viewDidLoad:
NSNotificationCenter.defaultCenter().addObserver(self, selector:"saveCurrentState", name: UIApplicationDidEnterBackgroundNotification, object: nil)
現(xiàn)在,當應用程序即將進入后臺,視圖會自動調(diào)用的savecurrentstate保存當前狀態(tài)。
向類添加下面的代碼:
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
這將確保你將類作為一個觀察者的時候釋放視圖。
構建和運行您的應用程序。導航到一個相冊,把應用程序的主頁按鈕的背景(命令+ Shift +如果你在模擬器),然后關閉你的應用程序從Xcode。重新啟動,并檢查先前選定的專輯的中心:
它看起來像這張專輯的數(shù)據(jù)是正確的,但不是以正確版本的專輯。給什么?
這是可選的方法initialviewindexforhorizontalscroller的意思是!因為這方法不在委托執(zhí)行,在這種情況下,視圖,初始視圖總是設置為第一視角。
為了解決這個問題,將下面的代碼添加到viewcontroller.swift:
func initialViewIndex(scroller: HorizontalScroller) -> Int {
return currentAlbumIndex
}
現(xiàn)在horizontalscroller第一視角設置為任何專輯由currentalbumindex。這是為了確保應用程序的經(jīng)驗仍然是個人和恢復一個的方式。
再次運行您的應用程序。滾動到一張專輯之前,把應用程序的背景下,停止應用程序,然后重新啟動以保證問題是固定的:
如果你看persistencymanager的初始化,你會注意到這張專輯是硬編碼的數(shù)據(jù)并重新創(chuàng)建每一次persistencymanager創(chuàng)建。但最好在一個文件中創(chuàng)建一個相冊列表。如何將相冊數(shù)據(jù)保存到文件中?
然后他們需要重現(xiàn)重新創(chuàng)建專輯的情況時,一種選擇是遍歷專輯的性質(zhì),將它們保存到一個plist文件。這不是最好的選擇,因為它需要你寫特定的代碼,根據(jù)什么數(shù)據(jù)/屬性是在每個類。例如,如果你創(chuàng)建了一個具有不同性質(zhì)的電影類,保存和加載該數(shù)據(jù)將需要新的代碼。
此外,您將無法為每個類實例保存私有變量,因為它們不能訪問外部類。這正是蘋果創(chuàng)造的歸檔機制的原因。
蘋果的一個專門的implementations是歸檔模式實現(xiàn)。這將一個對象轉(zhuǎn)換為一個流,可以保存和稍后恢復,而不暴露私有屬性到外部類。你可以閱讀更多關于這個功能在iOS 16的6章的教程書。Apple’s Archives and Serializations Programming Guide.
打開album.swift改變班線如下:
class Album: NSObject, NSCoding {
在album.swift添加下面的兩種方法:
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")
}
在NSCoding協(xié)議的一部分,encodewithcoder會給你打電話的時候,問一個專輯實例進行歸檔。相反,init(編碼器)初始化將用來重建或解壓縮從保存的實例。它雖然簡單,但強大。
現(xiàn)在,該相冊類可以被歸檔,添加的代碼,實際上保存和加載的專輯列表。
添加下面的方法到persistencymanager.swift:
func saveAlbums() {
var filename = NSHomeDirectory().stringByAppendingString("/Documents/albums.bin")
let data = NSKeyedArchiver.archivedDataWithRootObject(albums)
data.writeToFile(filename, atomically: true)
}
這將是一種被稱為保存專輯的方法。nskeyedarchiver檔案專輯陣列為一個名為albums.bin。
當你archive的對象包含其他對象,該文檔會自動嘗試遞歸archive的子對象和孩子任何子對象等。在這種情況下,archive開始與專輯,這是一個數(shù)組的相冊實例。由于數(shù)組和專輯都支持NSCopying接口,數(shù)組中的每件事都是archive。
現(xiàn)在替換init在persistencymanager.swift用下面的代碼:
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()
}
你有移動占位符創(chuàng)作專輯代碼可讀性的一個單獨的方法createplaceholderalbum()。在新的代碼,如果它存在的話,nskeyedunarchiver加載相冊數(shù)據(jù)從文件。如果它不存在,它創(chuàng)建的相冊數(shù)據(jù),并立即保存它為下一次推出的應用程序。
你還想保存相冊數(shù)據(jù),每次應用程序進入背景。這似乎不是必要的,但如果你后來添加的選項來改變專輯的數(shù)據(jù)?然后,你會希望這個,以確保所有的變化被保存。
由于主要的應用程序訪問所有服務通過libraryapi,這就是應用程序?qū)⒆宲ersistencymanager知道它需要保存相冊數(shù)據(jù)。
現(xiàn)在添加以下實現(xiàn)方法到 LibraryAPI.swift中
func saveAlbums() {
persistencyManager.saveAlbums()
}
此代碼只會在調(diào)用libraryapi保存相冊上persistencymangaer。
將下面的代碼添加到saveCurrentState在ViewController.swift結(jié)束:
LibraryAPI.sharedInstance.saveAlbums()
和上面的代碼使用libraryapi觸發(fā)數(shù)據(jù)視圖專輯時保存其狀態(tài)的保存。
您將通過允許用戶執(zhí)行刪除操作來刪除一個相冊,或撤消操作以使其改變自己的想法,從而為您的音樂應用程序添加最后的觸摸!
添加以下屬性視圖:
// We will use this array as a stack to push and pop operation for the undo option
var undoStack: [(Album, Int)] = []
這將創(chuàng)建一個空的撤銷堆棧。的undoStack將舉行一個元組的兩參數(shù)。第一張是一張專輯,二是這張專輯的索引。
在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)
上面的代碼創(chuàng)建了一個工具欄,其中有2個按鈕和一個靈活的空間。撤消按鈕在這里被禁用,因為撤消堆棧開始空。注意工具欄已經(jīng)在故事情節(jié)中,所以你需要做的是設置工具欄的項目。
你會加入三個方法在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
}
}
考慮下面的每一個部分:
獲得這張專輯刪除.
創(chuàng)建一個變量稱為undoaction存儲一個元組的專輯,這張專輯的指標。然后將元組添加到堆棧中
使用libraryapi從數(shù)據(jù)結(jié)構中刪除專輯和重載滾動。.
因為在撤消堆棧中有一個動作,您需要啟用撤消按鈕.
最后,添加撤消操作的方法:
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
}
最后考慮上述方法的意見:
該方法將對象從堆棧中彈出,給你一個包含已刪除的相冊及其索引的元組。然后你繼續(xù)增加專輯的背面。
自從你在堆棧中的最后一個對象被刪除時,你就需要檢查堆棧是否為空。如果是,那就意味著沒有更多的動作來撤消。所以你禁用了撤消按鈕
你也知道,既然你毀掉了一個行動,至少應該有一張專輯封面。因此你啟用了垃圾桶。
建立和運行你的應用程序來測試你的撤銷機制,刪除一張專輯(或兩者),并點擊撤消按鈕看到它的動作:
這也是一個很好的地方,以測試是否更改您的相冊數(shù)據(jù)保留在會話之間。現(xiàn)在,如果你刪除了一張專輯,把應用程序發(fā)送到后臺,然后終止應用程序,下一次你啟動應用程序的顯示相冊列表應該反映刪除。
如果你想得到所有的專輯回來,只是刪除應用程序并運行它再從Xcode安裝一個新的副本的入門資料。