介紹iOS設計模式1

你可以下載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ù)視圖。

編譯和運行你的項目,新的水平滾動條,看看:

The Observer Pattern

在觀察者模式,一個對象的狀態(tài)變化通知其他任何對象。所涉及的對象不需要知道彼此的,從而鼓勵一個解耦設計。當一個屬性發(fā)生改變時,這個模式最常用于通知感興趣的對象。

通常的實現(xiàn)要求一個觀察者在另一個對象的狀態(tài)寄存器。當狀態(tài)發(fā)生改變時,所有的觀察對象都會被通知。

如果你想堅持MVC的概念(提示:你這樣做),你需要讓模型對象和視圖對象溝通,但他們之間沒有直接的參考。這就是觀察者模式的所在。

Cocoa在兩個熟悉的方式實現(xiàn)觀察者模式:通知和鍵值觀察(KVO)。

Notifications

不要被混淆與推送本地通知,通知是基于訂閱和發(fā)布模式,允許對象(發(fā)行商)發(fā)送消息到其他對象(用戶/聽眾)。出版商從不需要了解有關用戶的任何事情。

通知被蘋果嚴重使用。例如,當鍵盤顯示/隱藏系統(tǒng)發(fā)送uikeyboardwillshownotification / uikeyboardwillhidenotification,分別。當你的應用程序進入后臺,系統(tǒng)將一個uiapplicationdidenterbackgroundnotification通知。

How to Use Notifications

去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)沒有保存。你看的最后一張專輯當應用程序啟動時不會是默認的相冊。

The Memento Pattern 備忘錄模式

備忘錄模式捕捉和表現(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)造的歸檔機制的原因。

Archiving

蘋果的一個專門的implementations是歸檔模式實現(xiàn)。這將一個對象轉(zhuǎn)換為一個流,可以保存和稍后恢復,而不暴露私有屬性到外部類。你可以閱讀更多關于這個功能在iOS 16的6章的教程書。Apple’s Archives and Serializations Programming Guide.

如何使用Archiving

打開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)的保存。

Final Touches

您將通過允許用戶執(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安裝一個新的副本的入門資料。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容