你可以下載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)條,看看:
在觀察者模式,一個(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)。
不要被混淆與推送本地通知,通知是基于訂閱和發(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通知。
去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è)。
備忘錄模式捕捉和表現(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ī)制的原因。
蘋(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.
打開(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)的保存。
您將通過(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)資料。