介紹iOS設(shè)計(jì)模式1

你可以下載the project source from the end of part 1與我們共同來(lái)探索

這是你在第一部分結(jié)束時(shí)完成的音樂(lè)庫(kù)App樣品

應(yīng)用程序的最初設(shè)計(jì)包括在屏幕的頂端上上水平滾動(dòng)條的專(zhuān)輯切換。但是為什么不重寫(xiě)它適配所有view,而不是單一的編輯一個(gè)簡(jiǎn)單的滾動(dòng)條?

為了使這個(gè)view可重用。關(guān)于其內(nèi)容的所有決定都應(yīng)該留給下一個(gè)對(duì)象:一個(gè)委托。水平滾動(dòng)條要聲明一個(gè)delegate implements為了scroller的工作。類(lèi)似于UITableView delegate。當(dāng)我們討論設(shè)計(jì)模式的時(shí)候我們會(huì)實(shí)現(xiàn)這個(gè)。

適配器模式

Adapter允許classes與不兼容的接口一起工作。它圍繞一個(gè)對(duì)象進(jìn)行封裝,并公開(kāi)一個(gè)標(biāo)準(zhǔn)接口與該對(duì)象進(jìn)行交互。

如果你很熟悉適配器你將注意到,App使用了略微不同的方式去實(shí)現(xiàn)它--App通過(guò)協(xié)議來(lái)實(shí)現(xiàn)它。 你可能會(huì)感覺(jué)它很像UITableViewDelegate,UIScrollViewDelegate, NSCoding 和 NSCopying。作為一個(gè)示例,隨著NSCopying協(xié)議發(fā)展,任何class都能提供一個(gè)標(biāo)準(zhǔn)的copy方法。

怎樣使用適配器

之前提到的horizontal scroller看起來(lái)像這個(gè)樣子

開(kāi)始實(shí)現(xiàn)它,在Project Navigator點(diǎn)擊View group 選擇New File…并且選擇iOS > Cocoa Touch class然后點(diǎn)擊Next。建立類(lèi)名為HorizontalScroller并且繼承于UIView。

打開(kāi)HorizontalScroller.swift并且加入下面的代碼類(lèi):

@objc protocol HorizontalScrollerDelegate {

}

這定義了一個(gè)協(xié)議名為HorizontalScrollerDelegate。在聲明協(xié)議之前你包括了@objc所以你能使用@optional delegate 方法。像在 Objective-C。

你定義了所需要的并且選中的委托方法將在大括號(hào)之間實(shí)現(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

這里有需求和可選擇的方法。所需的方法必須由委托來(lái)執(zhí)行,通常包含一些數(shù)據(jù),這是絕對(duì)必須的。 在這種情況下,所需的細(xì)節(jié)是view數(shù)量,特殊的索引視圖,并且挖掘試圖的行為。這里的可選方法是初始視圖。如果沒(méi)有實(shí)現(xiàn),HorizontalScroller講默認(rèn)為第一個(gè)索引。

在HorizontalScroller.swift,

將下面的代碼添加到HorizontalScroller的定義。

weak var delegate: HorizontalScrollerDelegate?

你上面創(chuàng)建的創(chuàng)建的屬性被定義為弱引用。這是必要的,以防止保留一個(gè)周期。如果一類(lèi)對(duì)它的委托有強(qiáng)引用的話,該委托保持強(qiáng)引用并且返回一個(gè)標(biāo)準(zhǔn)的類(lèi),你的app將發(fā)生內(nèi)存泄露,因?yàn)檫@兩個(gè)類(lèi)將釋放分配給其他的內(nèi)存。所有的屬性在swift中被默認(rèn)為強(qiáng)引用

委托是可選的,所以有可能使用這個(gè)類(lèi)的人不提供一個(gè)委托。但如果他們這樣做,它將使HorizontalScrollerDelegate一致并且你可以確保協(xié)議方法在那里實(shí)現(xiàn)。

Add a few more properties to the class:添加一些屬性到類(lèi)中:

// 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]()

滾動(dòng)每一個(gè)評(píng)論塊:

定義常亮,使其易于在設(shè)計(jì)時(shí)修改布局。視圖的尺寸內(nèi)的滾動(dòng)條是100 x 100的矩形。

創(chuàng)建一個(gè)包含試圖的滾動(dòng)視圖

創(chuàng)建一個(gè)擁有專(zhuān)輯封面的數(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()。這是實(shí)現(xiàn)方法:

創(chuàng)建一個(gè)新的UIScrollView實(shí)例并將其添加到父視圖。

關(guān)閉自動(dòng)調(diào)整尺寸。就是這樣,你可以應(yīng)用你自己的限制。

應(yīng)用約束到scrollview。完全填滿HorizontalScroller視圖

創(chuàng)建一個(gè)gesture收識(shí)別。這個(gè)收拾識(shí)別檢測(cè)涉及的滾動(dòng)試圖。并且檢查相冊(cè)封面是否已經(jīng)被竊聽(tīng)。如果是這樣的話,他會(huì)通知 HorizontalScroller delegate

現(xiàn)在添加這個(gè)方法。

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ì)作為一個(gè)參數(shù)傳入你locationInView()。

Next, you invoke numberOfViewsForHorizontalScroller() on the delegate.

接下來(lái),你調(diào)用委托numberOfViewsForHorizontalScroller()。

Next add the following to access an album cover from the scroller:

接下來(lái)添加下面的封面專(zhuān)輯

func viewAtIndex(index :Int) -> UIView {

return viewArray[index]

}

viewatindex僅僅返回視圖在一個(gè)特定的指數(shù)。使用此方法,以突出顯示專(zhuān)輯封面。

Now add the following code to reload the scroller: 現(xiàn)在,添加下面的代碼加載滾動(dòng):

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;他重新載入所有用于構(gòu)建水平滾動(dòng)條的數(shù)據(jù)

逐句詳解:

在我們重新加載之前檢查是否有一個(gè)委托.

清理專(zhuān)輯封面, 你需要重置viewArray.

刪除又有之前添加到滾動(dòng)試圖中的子視圖.

所有的視圖都是從給定的偏移量開(kāi)始的。目前是100,但它可以很容易地調(diào)整,通過(guò)在文件的頂部變化不斷view_offset

的horizontalscroller為代表之一,同時(shí)奠定了他們未來(lái)彼此水平與先前定義的填充.

在viewarra在存儲(chǔ)視圖中跟蹤滾動(dòng)視圖子視圖.

一旦所有的視圖都到位,設(shè)置的滾動(dòng)視圖的內(nèi)容偏移,讓用戶(hù)滾動(dòng)通過(guò)所有的專(zhuān)輯封面.

當(dāng)你的數(shù)據(jù)發(fā)生改變時(shí),你可以重新加載。你也需要調(diào)用這個(gè)方法時(shí),你horizontalscroller添加到另一個(gè)視圖。將下面的代碼添加到horizontalscroller.swift覆蓋后者:

override func didMoveToSuperview() {

reload()

}

didmovetosuperview查看時(shí),它添加到另一個(gè)視圖作為一個(gè)視圖。重新規(guī)范內(nèi)容正確的時(shí)間。

horizontalscroller的最后一塊拼圖是確保你看的專(zhuān)輯總是集中在滾動(dòng)視圖。這樣做,你將需要執(zhí)行一些計(jì)算,當(dāng)用戶(hù)拖動(dòng)滾動(dòng)查看他們的手指。

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))

}

}

上面的代碼考慮到滾動(dòng)視圖和視圖的當(dāng)前偏移量,以及視圖的填充量,以便計(jì)算當(dāng)前視圖從中心的距離。最后一行很重要:一次視圖是居中的,然后通知委托,選定以更改的視圖。

發(fā)現(xiàn)用戶(hù)在完成拖動(dòng)滾動(dòng)視圖,你需要實(shí)現(xiàn)一些uiscrollviewdelegate方法。將下面的類(lèi)擴(kuò)展添加到文件底部;請(qǐng)記住,這必須在主類(lèi)聲明的大括號(hào)之后添加!

extension HorizontalScroller: UIScrollViewDelegate {

func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {

if !decelerate {

centerCurrentView()

}

}

func scrollViewDidEndDecelerating(scrollView: UIScrollView) {

centerCurrentView()

}

}

scrollviewdidenddragging(_:willdecelerate:)通知委托當(dāng)用戶(hù)完成拖動(dòng)。如果滾動(dòng)視圖尚未完全停止來(lái)參數(shù)是真實(shí)的。當(dāng)滾動(dòng)行動(dòng)結(jié)束,該系統(tǒng)調(diào)scrollviewdidenddecelerating。在這兩種情況下,調(diào)用新方法以當(dāng)前視圖為中心,因?yàn)楫?dāng)前視圖可能在用戶(hù)拖動(dòng)滾動(dòng)視圖后發(fā)生改變。

最后別忘了設(shè)定委托。在initializescrollview()添加以下代碼后

scroller.delegate = self;

scroller = UIScrollView():

你的horizontalscroller準(zhǔn)備就緒!瀏覽你剛才寫(xiě)的代碼;你會(huì)看到有沒(méi)有一個(gè)提到的專(zhuān)輯或albumview類(lèi)。那是很好的,因?yàn)檫@意味著新的滾動(dòng)條是真正獨(dú)立的和可重復(fù)使用

Build your project to make sure everything compiles properly. 建立項(xiàng)目,確保所有編譯正確

現(xiàn)在,horizontalscroller是完整的,它的時(shí)間來(lái)使用它在您的應(yīng)用程序。首先,打開(kāi)main.storyboard。點(diǎn)擊頂部灰色的矩形視圖,點(diǎn)擊identity。改變類(lèi)的名稱(chēng)來(lái)horizontalscroller如下所示:

接下來(lái),打開(kāi)助理編輯和控制從灰色的矩形視圖拖到viewcontroller.swift創(chuàng)建一個(gè)出口。名稱(chēng)出口滾動(dòng),如下圖所示:

接下來(lái),打開(kāi)viewcontroller.swift。現(xiàn)在是時(shí)候開(kāi)始實(shí)施的一些horizontalscrollerdelegate方法!

Add the following extension to the bottom of the file: 將下列擴(kuò)展名添加到文件底部:

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

首先選定以前的專(zhuān)輯,然后取消選擇專(zhuān)輯封面

Display the data for the new album within the table view.

存儲(chǔ)當(dāng)前點(diǎn)擊的相冊(cè)封面索引

抓住當(dāng)前選定的相冊(cè)封面,并突出顯示選擇。.

在表視圖中顯示新相冊(cè)的數(shù)據(jù).

func numberOfViewsForHorizontalScroller(scroller: HorizontalScroller) -> (Int) {

return allAlbums.count

}

正如你所認(rèn)識(shí)的,這是一種在滾動(dòng)視圖中返回視圖的方法。由于滾動(dòng)視圖將顯示所有的專(zhuān)輯數(shù)據(jù)的封面,count是專(zhuān)輯記錄的數(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)建一個(gè)新的albumview,接下來(lái)檢查用戶(hù)是否選擇這張專(zhuān)輯。然后,您可以將其設(shè)置為突出顯示或不取決于是否選擇相冊(cè)。最后,你通過(guò)它的horizontalscroller。

這是它!僅僅三個(gè)短的方法顯示一個(gè)漂亮的水平滾動(dòng)條的方法。

是的,你還需要?jiǎng)?chuàng)建滾動(dòng)條,并把它添加到你的主要觀點(diǎn),但在這之前,將下面的方法添加到主類(lèi)的定義:

func reloadScroller() {

allAlbums = LibraryAPI.sharedInstance.getAlbums()

if currentAlbumIndex < 0 {

currentAlbumIndex = 0

} else if currentAlbumIndex >= allAlbums.count {

currentAlbumIndex = allAlbums.count - 1

}

scroller.reload()

showDataForAlbum(currentAlbumIndex)

}

此方法加載相冊(cè)數(shù)據(jù)通過(guò)libraryapi然后設(shè)置當(dāng)前顯示基于當(dāng)前視圖索引的當(dāng)前值。如果當(dāng)前視圖索引小于0,則表示當(dāng)前沒(méi)有選擇視圖,然后在列表中顯示的第一張專(zhuān)輯。否則,最后一張專(zhuān)輯被顯示。

scroller.delegate = self

reloadScroller()

由于horizontalscroller是創(chuàng)建在storyboard中,所有你需要做的是設(shè)置代理,叫reloadscroller(),將負(fù)荷的滾動(dòng)條來(lái)顯示專(zhuān)輯數(shù)據(jù)視圖。

由于horizontalscroller是創(chuàng)建在storyboard中,所有你需要做的是設(shè)置代理,叫reloadscroller(),將負(fù)荷的滾動(dòng)條來(lái)顯示專(zhuān)輯數(shù)據(jù)視圖。

編譯和運(yùn)行你的項(xiàng)目,新的水平滾動(dòng)條,看看:

The Observer Pattern

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

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

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

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

Notifications

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

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

How to Use Notifications

去albumview.swift在初始化結(jié)束中插入下面的代碼(框架:CGRect,albumcover:初始化字符串):

NSNotificationCenter.defaultCenter().postNotificationName("BLDownloadImageNotification", object: self, userInfo: ["imageView":coverImage, "coverUrl" : albumCover])

這條線穿過(guò)NSNotificationCenter發(fā)送通知單。通知信息包含在UIImageView和封面圖像被下載的URL。這是所有的信息,您需要執(zhí)行的封面下載任務(wù)。

在libraryapi.swift init中,直接在super.init()后面添加下面一行

NSNotificationCenter.defaultCenter().addObserver(self, selector:"downloadImage:", name: "BLDownloadImageNotification", object: nil

這是等式的另一邊:觀察者。每一次albumview類(lèi)崗位bldownloadimagenotification通知,自libraryapi注冊(cè)同一通知觀察者,系統(tǒng)會(huì)通知libraryapi。然后libraryapi通知downloadimage()響應(yīng)。

然而,在你實(shí)現(xiàn)downloadimage()你要記得退訂此通知時(shí)候釋放。如果你不正確的退訂通知你們班登記,通知會(huì)發(fā)送到回收實(shí)例。這可能會(huì)導(dǎo)致應(yīng)用程序崩潰。

Add the following method to LibraryAPI.swift: 添加下面的方法到libraryapi.swift:

deinit {

NSNotificationCenter.defaultCenter().removeObserver(self)

}

當(dāng)這個(gè)對(duì)象被釋放,它使自己從所有通知已注冊(cè)的觀察者。

還有一件事要做。這可能是一個(gè)好主意,以節(jié)省下載資源并且覆蓋本地,所以應(yīng)用程序?qū)⒉恍枰螺d相同的蓋過(guò)一遍又一遍。

打開(kāi)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!)

}

}

這個(gè)代碼非常簡(jiǎn)單。下載的圖片將被保存在文件目錄,并將getimage()如果匹配的文件不在文件目錄中找到返回nil。

Now add the following method to LibraryAPI.swift: 現(xiàn)在添加下面的方法來(lái)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ù)雜性,從其他類(lèi)下載圖像的外觀模式。通知發(fā)件人不關(guān)心圖像來(lái)自網(wǎng)絡(luò)或文件系統(tǒng)。

建立并運(yùn)行你的應(yīng)用程序看看美麗的覆蓋在你的horizontalscroller:

停止應(yīng)用程序和運(yùn)行它。注意,有沒(méi)有延遲加載的封面,因?yàn)樗麄円呀?jīng)保存在本地。你甚至可以斷開(kāi)互聯(lián)網(wǎng)和您的應(yīng)用程序?qū)⒄9ぷ鳌H欢幸粋€(gè)奇怪的點(diǎn)這里:微調(diào)從未停止旋轉(zhuǎn)!發(fā)生了什么事?

你開(kāi)始旋轉(zhuǎn)時(shí)下載的圖像,但是你還沒(méi)有實(shí)現(xiàn)的邏輯映像下載完成后停止旋轉(zhuǎn)。你可以發(fā)送一個(gè)通知,每一次的圖像已被下載,但相反的,你會(huì)使用其他觀察者模式,KVO。

Key-Value Observing (KVO) 鍵值觀察(KVO)

在KVO,對(duì)象可以被通知到一個(gè)特定的財(cái)產(chǎn)的任何變化;要么自己或另一個(gè)對(duì)象。如果你有興趣,你可以更多地了解這個(gè)Apple’s KVO Programming Guide

How to Use the KVO Pattern 如何使用KVO模式

如上所述,該KVO機(jī)制允許一個(gè)對(duì)象觀察變化的屬性。在你的情況,你可以使用KVO觀察到保存圖像的UIImageView圖像屬性。

打開(kāi)albumview.swift并添加以下代碼以init(框架:albumcover:),只在你添加載體圖像作為子視圖:

coverImage.addObserver(self, forKeyPath: "image", options: nil, context: nil)

這增加了self,這是當(dāng)前類(lèi),對(duì)載體圖像圖像特性觀察。

你還需要注銷(xiāo)作為觀察者,仍在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()

}

}

你必須在每一個(gè)類(lèi)中實(shí)現(xiàn)這種方法作為一個(gè)觀察者。系統(tǒng)每一次都會(huì)有一次觀測(cè)到的性能變化來(lái)執(zhí)行這個(gè)方法。在上面的代碼中,你停止旋轉(zhuǎn)時(shí)的“形象”性質(zhì)的變化。這樣,當(dāng)一個(gè)圖像被加載,微調(diào)將停止轉(zhuǎn)動(dòng)。

建設(shè)和運(yùn)行您的項(xiàng)目。微調(diào)應(yīng)該消失:

如果你適當(dāng)使用和終止它,你會(huì)注意到,你的應(yīng)用程序的狀態(tài)沒(méi)有保存。你看的最后一張專(zhuān)輯當(dāng)應(yīng)用程序啟動(dòng)時(shí)不會(huì)是默認(rèn)的相冊(cè)。

The Memento Pattern 備忘錄模式

備忘錄模式捕捉和表現(xiàn)對(duì)象的內(nèi)部狀態(tài)。換句話說(shuō),它可以節(jié)省你的東西。后來(lái),這種外在的狀態(tài)可以在不破壞封裝恢復(fù);即,私有數(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保存當(dāng)前專(zhuān)輯指數(shù)NSUserDefaults – NSUserDefaults是一種標(biāo)準(zhǔn)的數(shù)據(jù)存儲(chǔ)提供的iOS應(yīng)用程序特定的設(shè)置和數(shù)據(jù)保存。

loadpreviousstate加載以前保存的指標(biāo)。這不是該備忘錄模式實(shí)現(xiàn)比較充分的,但是你要有。

現(xiàn)在,添加下面一行在viewcontroller.swift viewDidLoad前scroller.delegate =self:

loadPreviousState()

當(dāng)應(yīng)用程序啟動(dòng)時(shí)加載先前保存的狀態(tài)。但是,你從哪里來(lái)拯救這個(gè)應(yīng)用程序的當(dāng)前狀態(tài)?你會(huì)使用通知來(lái)做這個(gè)。iOS發(fā)送uiapplicationdidenterbackgroundnotification通知當(dāng)應(yīng)用程序進(jìn)入后臺(tái)。你可以使用該通知稱(chēng)savecurrentstate。那不方便嗎?

Add the following line to the end of viewDidLoad: 添加下面一行到viewDidLoad:

NSNotificationCenter.defaultCenter().addObserver(self, selector:"saveCurrentState", name: UIApplicationDidEnterBackgroundNotification, object: nil)

現(xiàn)在,當(dāng)應(yīng)用程序即將進(jìn)入后臺(tái),視圖會(huì)自動(dòng)調(diào)用的savecurrentstate保存當(dāng)前狀態(tài)。

向類(lèi)添加下面的代碼:

deinit {

NSNotificationCenter.defaultCenter().removeObserver(self)

}

這將確保你將類(lèi)作為一個(gè)觀察者的時(shí)候釋放視圖。

構(gòu)建和運(yùn)行您的應(yīng)用程序。導(dǎo)航到一個(gè)相冊(cè),把應(yīng)用程序的主頁(yè)按鈕的背景(命令+ Shift +如果你在模擬器),然后關(guān)閉你的應(yīng)用程序從Xcode。重新啟動(dòng),并檢查先前選定的專(zhuān)輯的中心:

它看起來(lái)像這張專(zhuān)輯的數(shù)據(jù)是正確的,但不是以正確版本的專(zhuān)輯。給什么?

這是可選的方法initialviewindexforhorizontalscroller的意思是!因?yàn)檫@方法不在委托執(zhí)行,在這種情況下,視圖,初始視圖總是設(shè)置為第一視角。

為了解決這個(gè)問(wèn)題,將下面的代碼添加到viewcontroller.swift:

func initialViewIndex(scroller: HorizontalScroller) -> Int {

return currentAlbumIndex

}

現(xiàn)在horizontalscroller第一視角設(shè)置為任何專(zhuān)輯由currentalbumindex。這是為了確保應(yīng)用程序的經(jīng)驗(yàn)仍然是個(gè)人和恢復(fù)一個(gè)的方式。

再次運(yùn)行您的應(yīng)用程序。滾動(dòng)到一張專(zhuān)輯之前,把應(yīng)用程序的背景下,停止應(yīng)用程序,然后重新啟動(dòng)以保證問(wèn)題是固定的:

如果你看persistencymanager的初始化,你會(huì)注意到這張專(zhuān)輯是硬編碼的數(shù)據(jù)并重新創(chuàng)建每一次persistencymanager創(chuàng)建。但最好在一個(gè)文件中創(chuàng)建一個(gè)相冊(cè)列表。如何將相冊(cè)數(shù)據(jù)保存到文件中?

然后他們需要重現(xiàn)重新創(chuàng)建專(zhuān)輯的情況時(shí),一種選擇是遍歷專(zhuān)輯的性質(zhì),將它們保存到一個(gè)plist文件。這不是最好的選擇,因?yàn)樗枰銓?xiě)特定的代碼,根據(jù)什么數(shù)據(jù)/屬性是在每個(gè)類(lèi)。例如,如果你創(chuàng)建了一個(gè)具有不同性質(zhì)的電影類(lèi),保存和加載該數(shù)據(jù)將需要新的代碼。

此外,您將無(wú)法為每個(gè)類(lèi)實(shí)例保存私有變量,因?yàn)樗鼈儾荒茉L問(wèn)外部類(lèi)。這正是蘋(píng)果創(chuàng)造的歸檔機(jī)制的原因。

Archiving

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

如何使用Archiving

打開(kāi)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會(huì)給你打電話的時(shí)候,問(wèn)一個(gè)專(zhuān)輯實(shí)例進(jìn)行歸檔。相反,init(編碼器)初始化將用來(lái)重建或解壓縮從保存的實(shí)例。它雖然簡(jiǎn)單,但強(qiáng)大。

現(xiàn)在,該相冊(cè)類(lèi)可以被歸檔,添加的代碼,實(shí)際上保存和加載的專(zhuān)輯列表。

添加下面的方法到persistencymanager.swift:

func saveAlbums() {

var filename = NSHomeDirectory().stringByAppendingString("/Documents/albums.bin")

let data = NSKeyedArchiver.archivedDataWithRootObject(albums)

data.writeToFile(filename, atomically: true)

}

這將是一種被稱(chēng)為保存專(zhuān)輯的方法。nskeyedarchiver檔案專(zhuān)輯陣列為一個(gè)名為albums.bin。

當(dāng)你archive的對(duì)象包含其他對(duì)象,該文檔會(huì)自動(dòng)嘗試遞歸archive的子對(duì)象和孩子任何子對(duì)象等。在這種情況下,archive開(kāi)始與專(zhuān)輯,這是一個(gè)數(shù)組的相冊(cè)實(shí)例。由于數(shù)組和專(zhuān)輯都支持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()

}

你有移動(dòng)占位符創(chuàng)作專(zhuān)輯代碼可讀性的一個(gè)單獨(dú)的方法createplaceholderalbum()。在新的代碼,如果它存在的話,nskeyedunarchiver加載相冊(cè)數(shù)據(jù)從文件。如果它不存在,它創(chuàng)建的相冊(cè)數(shù)據(jù),并立即保存它為下一次推出的應(yīng)用程序。

你還想保存相冊(cè)數(shù)據(jù),每次應(yīng)用程序進(jìn)入背景。這似乎不是必要的,但如果你后來(lái)添加的選項(xiàng)來(lái)改變專(zhuān)輯的數(shù)據(jù)?然后,你會(huì)希望這個(gè),以確保所有的變化被保存。

由于主要的應(yīng)用程序訪問(wèn)所有服務(wù)通過(guò)libraryapi,這就是應(yīng)用程序?qū)⒆宲ersistencymanager知道它需要保存相冊(cè)數(shù)據(jù)。

現(xiàn)在添加以下實(shí)現(xiàn)方法到 LibraryAPI.swift中

func saveAlbums() {

persistencyManager.saveAlbums()

}

此代碼只會(huì)在調(diào)用libraryapi保存相冊(cè)上persistencymangaer。

將下面的代碼添加到saveCurrentState在ViewController.swift結(jié)束:

LibraryAPI.sharedInstance.saveAlbums()

和上面的代碼使用libraryapi觸發(fā)數(shù)據(jù)視圖專(zhuān)輯時(shí)保存其狀態(tài)的保存。

Final Touches

您將通過(guò)允許用戶(hù)執(zhí)行刪除操作來(lái)刪除一個(gè)相冊(cè),或撤消操作以使其改變自己的想法,從而為您的音樂(lè)應(yīng)用程序添加最后的觸摸!

添加以下屬性視圖:

// We will use this array as a stack to push and pop operation for the undo option

var undoStack: [(Album, Int)] = []

這將創(chuàng)建一個(gè)空的撤銷(xiāo)堆棧。的undoStack將舉行一個(gè)元組的兩參數(shù)。第一張是一張專(zhuān)輯,二是這張專(zhuān)輯的索引。

在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)建了一個(gè)工具欄,其中有2個(gè)按鈕和一個(gè)靈活的空間。撤消按鈕在這里被禁用,因?yàn)槌废褩i_(kāi)始空。注意工具欄已經(jīng)在故事情節(jié)中,所以你需要做的是設(shè)置工具欄的項(xiàng)目。

你會(huì)加入三個(gè)方法在viewcontroller.swift,專(zhuān)輯管理動(dòng)作處理:添加,刪除,和撤銷(xiāo)。

第一個(gè)是增加新專(zhuān)輯的方法:

func addAlbumAtIndex(album: Album,index: Int) {

LibraryAPI.sharedInstance.addAlbum(album, index: index)

currentAlbumIndex = index

reloadScroller()

}

在這里你添加相冊(cè),將其設(shè)置為當(dāng)前專(zhuān)輯索引,并重新加載滾動(dòng)。

下一步是刪除方法:

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

}

}

考慮下面的每一個(gè)部分:

獲得這張專(zhuān)輯刪除.

創(chuàng)建一個(gè)變量稱(chēng)為undoaction存儲(chǔ)一個(gè)元組的專(zhuān)輯,這張專(zhuān)輯的指標(biāo)。然后將元組添加到堆棧中

使用libraryapi從數(shù)據(jù)結(jié)構(gòu)中刪除專(zhuān)輯和重載滾動(dòng)。.

因?yàn)樵诔废褩V杏幸粋€(gè)動(dòng)作,您需要啟用撤消按鈕.

最后,添加撤消操作的方法:

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

}

最后考慮上述方法的意見(jiàn):

該方法將對(duì)象從堆棧中彈出,給你一個(gè)包含已刪除的相冊(cè)及其索引的元組。然后你繼續(xù)增加專(zhuān)輯的背面。

自從你在堆棧中的最后一個(gè)對(duì)象被刪除時(shí),你就需要檢查堆棧是否為空。如果是,那就意味著沒(méi)有更多的動(dòng)作來(lái)撤消。所以你禁用了撤消按鈕

你也知道,既然你毀掉了一個(gè)行動(dòng),至少應(yīng)該有一張專(zhuān)輯封面。因此你啟用了垃圾桶。

建立和運(yùn)行你的應(yīng)用程序來(lái)測(cè)試你的撤銷(xiāo)機(jī)制,刪除一張專(zhuān)輯(或兩者),并點(diǎn)擊撤消按鈕看到它的動(dòng)作:

這也是一個(gè)很好的地方,以測(cè)試是否更改您的相冊(cè)數(shù)據(jù)保留在會(huì)話之間。現(xiàn)在,如果你刪除了一張專(zhuān)輯,把應(yīng)用程序發(fā)送到后臺(tái),然后終止應(yīng)用程序,下一次你啟動(dòng)應(yīng)用程序的顯示相冊(cè)列表應(yīng)該反映刪除。

如果你想得到所有的專(zhuān)輯回來(lái),只是刪除應(yīng)用程序并運(yùn)行它再?gòu)腦code安裝一個(gè)新的副本的入門(mén)資料。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容