Introducing iOS Design Patterns in Swift Part 2(完整版)
2016-04-09
3月31日 刚刚开始写。
4月5日写着写着就凌晨一点多了,又添加了一些内容。本来想着写到项目目前的状态来,不过也不早了,明天还要好好想一想论文的实验的几个关键点。睡了,明天接着写。
4月6日网络不好真的很耽误事,还影响心情,今天整了一天的网,不开心。本来能多做好多事的,明天继续。
4月7日早上早起写的,写着写着饿了,看看中午加把劲,把这个弄完,再把项目自己写一遍。
4月9日终于写完了,这只是个开始。博文继续,代码不止!
2015年4月22日: Xcode6.3 和Swift1.2 更新
更新日志:这次IOS8和Swift的指导课是有Vincent Ngo更新的。原版本事由指导课成员Eli Ganem推出的。
欢迎回到设计模式介绍指导课的第二部分!在第一部分,你已经学习关于Cocoa的一些基本的设计模式,例如MVC,单例模式,和装饰模式等。
在最后这一部分,你将会学习一些其他的由大量IOS和OS X开发者提出的一些基本设计模式。例如:适配器模式,观察者模式和纪念品模式。让我们马上开始学习吧!
Getting Started
你可以下载第一部分的实例代码开始这一部分。
这是我们在第一部分结束的时候完成的音乐库APP:

这个APP原来的设计是在屏幕的顶部设计一个水平滚动窗口来选择相册。既然不是为了单一的目的而设计使用的,为什么我们不使他为每一个视图重复使用呢?
为了使这个视图重复使用,因此所有关于它内容的决定都要交给另一个对象:一个委托。这个水平滑动视图要声明委托工具(delegate implements)的方法来使他为滚动窗口工作。这种方法一UITableView委托方法相似。我们将在接下来的要讨论的设计模式中用到这种方法。
The Adapter Pattern
适配器可以使接口不匹配的类一起工作。他用一个类将自己包裹起来,并且暴露出标准接口给要交流的类。
如果你熟悉适配器模式,你会发现,苹果的用一种完全不同的方法使用它-苹果用协议来完成这份工作。你可能已经了解了UITableViewDelegate,UIScrollViewDelegate,NSCoding和NSCopying. 例如:在NSCopying协议里面,每一种协议可以提供一种标准的复制方法。
How to Use the Adapter Pattern
水平滑动的方法看起来会是这样的:

打开HorizontalSCroller.swift 然后插入如下代码:
@objc protocol HorizontalScrollerDelegate {
}
定义一个名为HorizontalScrollerDelegate的协议。你需要在协议声明前添加一个@objc。然后你就可以像在Object-C里面一样好好使用@optional代理的方法了。
你定义了需要的和可选的方法,然后代理就会使用协议中的方法。因此在协议中加入如下方法:
// 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 <index>
func horizontalScrollerViewAtIndex(scroller: HorizontalScroller, index:Int) -> UIView
// inform the delegate what the view at <index> 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
现在已经有了必需的方法和可选的方法。必需的方法是代理必需使用的,并且通常包含者一些类必需用刀的数据。在这个例子里面,这些必需的细节是view的数量,view的具体编号和当view被触摸时的行为。可选方法是view的初始化;如果它没有被使用HorizontalScroller将会默认设置为第一个。
在HorizontalScroller.swift里加上如下代码定义HorizontalScroller类:
weak var delegate: HorizontalScrollerDelegate?
将你定义的变量属性设置为weak,这是避免死循环而必需做的。如果类与它的代理有着强引用关系,并且代理对它遵从的类也保持着强引用,你的APP将会内存不足,因为所有的类都不会释放内存再分配给其他类。swift里面所有的变量都默认是strong。
代理是可选的,因此,无论是谁都可能不比提供代理而使用这个类。但是,如果它们这么做了,它就会遵从HorizontalScrollerDelegate 并且可以保证协议里的方法是可以在这里被使用的。
为类添加几个的变量:
// 1
private let VIEW_PADDING = 10
private let VIEW_DIMENSIONS = 100
private let VIEWS_OFFSET = 100
// 2
private var scroller : UIScrollView!
// 3
var viewArray = [UIView]()
下面给出每一块代码的注释:
1.定义常量使他在设计时更容易模块化。scroller里面view的大小是100*100,到闭合长方形边界有10像素点。
2.创建一个scroller包含这个view。
3.创建一个包含所有相册封面的数组。
接下来就要执行初始化,添加如下代码:
// 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]()
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)
}
重装的方法是在重新加载UITableView数据数据之后建模的;它重新加载所有的数据用于构建horizontal scroller。
下面是每一步的步骤:
1.创建一个新的UIScrollerView实例并且把它添加到父视图。
2.关闭自动排版。这样你就可以使用你自己的布局约束条件。
3.对scrollview使用约束条件,使scroller view完全填充满horizontalscroller
4.创建一个tap手势识别。tap收拾识别探测在scrollerview上的触摸并且检查是否有相册封面被选中。如果被选中将会通知horizontalscroller delegate。
现在再添加上这个方法:
func scrollerTapped(gesture: UITapGestureRecognizer) {
let location = gesture.locationInView(gesture.view)
if let delegate = delegate {
for index in 0..<delegate.numberOfViewsForHorizontalScroller(self) {
let view = scroller.subviews[index] as! UIView
if CGRectContainsPoint(view.frame, location) {
delegate.horizontalScrollerClickedViewAtIndex(self, index: index)
scroller.setContentOffset(CGPoint(x: view.frame.origin.x - self.frame.size.width/2 + view.frame.size.width/2, y: 0), animated:true)
break
}
}
}
}
这个手势以一个元素传入,使你用locationInView()来提取位置信息。
接下来,调用delegate里面的numberOfViewsForHorizontalScroller()。HorizontalScroller实例没有关于delegate的信息,而是知道它可以安全地传递消息,因为delegate必须遵从HorizontalScrollerDelegate协议。
对于在scroll view里的每一个view用一个点击测试CGRectContainsPoint找到被触摸的view。当找到了这个view,调用horizontalScrollerClickedViewAtIndex方法。在打破for循环之前,触摸scroll view里的view。
现在添加reload到scroller里面:
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..<delegate.numberOfViewsForHorizontalScroller(self) {
// 5 - add a view at the right position
xValue += VIEW_PADDING
let view = delegate.horizontalScrollerViewAtIndex(self, index: index)
view.frame = CGRectMake(CGFloat(xValue), CGFloat(VIEW_PADDING), CGFloat(VIEW_DIMENSIONS), CGFloat(VIEW_DIMENSIONS))
scroller.addSubview(view)
xValue += VIEW_DIMENSIONS + VIEW_PADDING
// 6 - Store the view so we can reference it later
viewArray.append(view)
}
// 7
scroller.contentSize = CGSizeMake(CGFloat(xValue + VIEWS_OFFSET), frame.size.height)
// 8 - If an initial view is defined, center the scroller on it
if let initialView = delegate.initialViewIndex?(self) {
scroller.setContentOffset(CGPoint(x: CGFloat(initialView)*CGFloat((VIEW_DIMENSIONS + (2 * VIEW_PADDING))), y: 0), animated: true)
}
}
}
reload方法是在UITableView重新加载数据以后设计的模型;它重新加载了用于构建horizontal scroller的所有数据。
具体步骤如下:
1.在我们做任何一个reload之前,检查是否存在一个delegate。
2.即使你清除了相册封面,你也需要重置viewArray。否则,将会有大量的数据从之前的封面里留存下来。
3.在添加到scroller view之前,移除所有的subview。
4.所有view的初始位置都是由给定的offset定位的,当前是100,但是它可以通过修改文件开始的VIEW_OFFET的内容很容易的进行拉伸。
5.HorizontalView要求它的delegate 按照设定好的padding一次一个view水平的依次摆放。
6.将view存到viewArray里面以追踪scrollview里所有view的踪迹。
7.一旦所有的view就位以后,为scroll view设置content offset来使用户可以在所有的相册封面之间滑动。
8.horizontalscroller 检查它的delegate是否执行了initialviewindex方法。这一步检查是必不可少的,因为特殊协议方法是可选的。如果delegate没有使用这个方法,默认值就是0。最终,这段代码设置scroll view 到由delegate定义的初始视图的中心。
当数据改变时,执行reload方法。当你添加horizontalscroller到其他的view时也需要这个方法。将如下代码添加到HorizontalView.swift:
override func didMoveToSuperview() {
reload()
}
didMoveToSuperview是一个view在被添加到另一个view作为子视图时被调用的。这正是重新加载scroller内容的好时机。
关于HorizontalScroller最后的一个难点就是要确定你正在浏览的相册总是在scrollerview的内部。要做到这一点,当用户用手指拖拽scroll的时候你需要做一些计算。
添加如下代码:
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))
}
}
上面的代码时为了计算当前view到中心的距离,所以要考虑到当前scroll view的offset,尺寸和view的padding。最后一行是很重要的,一旦view居中了,然后你就通知delegate选中的view改变了。
为了探测到用户在scroll view里面结束拖动,你需要使用一些UIScrollViewDelegate方法。添加如下类拓展,记住要添加在类的大括号外面!
extension HorizontalScroller: UIScrollViewDelegate {
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
centerCurrentView()
}
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
centerCurrentView()
}
}
scrollViewDidEndDragging(_:willDecelerate:)当用户结束拖动时通知delegate。如果scroll view 没有完全停止则减速参数为真。当scroll动作停止了,系统将会调用scrollViewDidEndDecelerating。这两种情况你都需要调用新方法使当前的view居中,即使当前的试图可能在用户的拖拽scroll view时改变了。
左后不要忘了设置delegate。在initializeScrollView()里面的scroller = UIScrollView():后面添加如下代码:
scroller.delegate = self;
HorizontalScroller已经等着你使用了!浏览你刚才写的代码你会发现;你一点也没有提到Album类或AlbumView类。这太棒了,因为这意味着新的scroller真正的实现了独立和重用。
build你的工程确保每一件事都完美完成。
现在HorizontalScroller 已经完成了,是时候在你的app里面使用了。首先,打开main.storyboard.点击顶部的灰色矩形视图,并且点击 identity inspector。用如下操作修改类名。


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)
}
}
让我们再一行一行的走一遍delegate方法:
1.首先,抓取之前选定的相册,不选定相册封面。
2.存储当前你点击的相册封面的序号。
3.抓取当前选定的相册封面并且使它高亮选中。
4.在tableview中显示新的相册的数据。
接下来,添加如下扩展方法:
func numberOfViewsForHorizontalScroller(scroller: HorizontalScroller) -> (Int) {
return allAlbums.count
}
这个方法就像你想的一样,是一个返回scroll view里view数量的一个协议方法。因为scroll view将会显示所有相册数据的封面,这个值是相册记录的数量。
现在添加如下代码:
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
}
在这里创建一个新的AlbumView,接下来点击试一下用户是否选中了这个相册。然后就可以根据相册是否被选中设置为高亮显示或不高亮。最后传递给HorizontalScroller。
这就好了!仅仅三个简单的方法就能实现一个好看的水平滑动窗口。
是的,你还需要创建一个scroller并把它添加到你的主view中,但是在你做这个之前,添加如下方法到主类:
func reloadScroller() {
allAlbums = LibraryAPI.sharedInstance.getAlbums()
if currentAlbumIndex < 0 {
currentAlbumIndex = 0
} else if currentAlbumIndex >= allAlbums.count {
currentAlbumIndex = allAlbums.count - 1
}
scroller.reload()
showDataForAlbum(currentAlbumIndex)
}
这个方法通过LibraryAPI重新加载相册数据,并且基于当前view的序号值显示当前的view。如果当前的view序号小于0,意味着现在没有view被选中,然后显示list中的第一个相册。否则,显示最后一个相册。
现在,通过在viewDidLoad的最后添加如下代码初始化scroller:
scroller.delegate = self
reloadScroller()
既然HorizontalScroller在storyboard中创建,你所要做的事情有:设置delegate,调用reloadScroller()来为scroller加载subview以显示相册数据。
注意:如果一个协议变得很大并且有很多可选的方法,你就要考虑把它拆成几个小的协议。UITableViewDelegate and UITableViewDataSource就是很好的例子。试着设计你的协议,这样每一个协议处理一个具体的功能区域。
编译运行你的项目,然后就能看到你美好的水平滑动窗口。

是的,你还没有写用来加载封面的代码。你需要添加一个下载图片的方法。因为,你所有的存取服务都是通过LibraryAPI。这就是新的方法要做的事情。然而,在这之前还有一些事情需要考虑:
1.AlbumView不应该直接使用LibraryAPI。你不想将视图逻辑和交互逻辑混合。
2.出于相同的理由,LibraryAPI不应该知道AlbumView。
3.一旦封面下载完成,LibraryAPI需要告知AlbumView,因为AlbumView需要显示封面。
听起来是不是很复杂?不要担心,接下来你将会学习观察者模式!😄
The Observer Pattern
在观察者模式中,一个对象通知其他的对象所有关于状态的改变。参与对象不需要知道彼此-尽管支持松耦合。这些模式大多数用于感兴趣的对象,当属性发生变化的时候。
通常的做法是需要一个观察登记员对另一个对象的状态感兴趣。当状态发生改变时,所有的观察对象都会被通知。
如果你想依照MVC概念,你需要Model对象与View对象交互,但不是直接在两者之间交互。这就产生了观察者模式。
Cocoa用两种相似的方法使用了观察者模式:Notifications 和 Key-Value Observing。
Notifications
不要将它与推送和本地通知混淆,Notification是基于subscribe-and-publish模型,这个模型允许一个对象发送消息给其他的对象。出版商永远不必知道关于订购者的任何消息。
Notification在Apple中广泛使用。例如,键盘显示或隐藏时,系统发送一个UIKeyboardWillShowNotification/UIKeyboardWillHideNotification,当你的程序进入后台时,系统发送一个UIApplicationDidEnterBackgroundNotification。
注意:打开UIApplication.swift,在文档的末尾,你会看见至少20个由系统发出的notifications。
How to Use Notifations
打开AlbumView.swift在init()后面插入如下代码:
NSNotificationCenter.defaultCenter().postNotificationName("BLDownloadImageNotification", object: self, userInfo: ["imageView":coverImage, "coverUrl" : albumCover])
这一行代码通过NSNotifacationCenter单例来发送一个notification。通知信息包括UIImageview和将要下载的封面信息的URL。这是所有你需要的完成封面下载任务的信息。
直接在super.init()后添加如下一行初始化LibraryAPI.swift:
NSNotificationCenter.defaultCenter().addObserver(self, selector:"downloadImage:", name: "BLDownloadImageNotification", object: nil)
这就是方程的另一面:观察者。每次AlbumView类发出一个BLDownloadImageNotification 通知时,因为LibraryAPI已经登记为为了相同通知的观察者,系统通知LibraryAPI。然后LibraryAPI调用downloadImage()作为回应。
然而,在你使用downloadImage()之前,你必须记住当类被释放的时候你要从通知中取消订阅。如果你不从你注册的通知中取消订阅,通知可能会送到一个释放的实例。这可能导致应用冲突。
在LibraryAPI.swift中添加如下代码:
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
当对象被释放,它将会从所有登记过的通知中移除观察者自己。
这儿还有一件事情要做。将下载的封面保存到本地是一个好方法,这样app就不用一遍一遍地下载相同的封面了。
打开PersistencyManager.swift并且添加如下方法:
func getImage(filename: String) -> UIImage? {
var error: NSError?
let path = NSHomeDirectory().stringByAppendingString("/Documents/\(filename)")
let data : NSData?
do {
data = try NSData(contentsOfFile: path, options: .UncachedRead)
} catch let error1 as NSError {
error = error1
data = nil
}
if let unwrappedError = error {
return nil
} else {
return UIImage(data: data!)
}
}
这段代码十分的直接,下载的图片将会存储直接存储到document,如果没有在document中找到匹配的文件,getImage()会返回nil。
现在在LibraryAPI.swift中添加如下方法:
func downloadImage(notification: NSNotification) {
//1
let userInfo = notification.userInfo as! [String: AnyObject]
var imageView = userInfo["imageView"] as! UIImageView?
let coverUrl = userInfo["coverUrl"] as! 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)
})
})
}
}
}
下面就是代码中的断点:
1.downloadImage是通过通知执行的,所以方法接收通知对象作为参数。UIImageView和图像的URL从通知中取回。
2.如果之前已经下载了图像,直接从PersistencyManager里取回图像。
3.如果图像还没有被下载,用HTTPClient取回。
4.当下载完成,在图像窗口显示图像,并且用PersistencyManager保存到本地。
在次说明,你正在使用Facade模式隐藏从其他类下载图片的复杂细节。通知发送者不关心图像是来自网络还是本地。
编译运行你的程序,你就会在水平滑动窗口里面看到美丽的封面。

当你下载图片的时候开启的旋转,但你没有做当图片加载完成时停止旋转的逻辑。每次你可以在一个图片下载完成时发出一个通知,但是作为替代,你将会你将会用到其他的观察者模式,KOV。
Key-Value Observing(KVO)
在kvo中一个对象能够具体性质的任意变化的通知,不管是他自己的还是其他对象的。如果你感兴趣,你可以参考Apple’s KVO Programming Guide.
如何使用KVO模式
上边我们提到过,KVO机制允许对象观察一个属性的变化。在你的示例中,你可以用KVO观察者模式去观察UIImageView中image属性的变化。
打开AlbumView.swift添加如下代码到init(frame:albumCover),就像添加coverImage作为自窗口一样:
coverImage.addObserver(self, forKeyPath: "image", options: [], context: nil)
添加self是指当前作为coverImage属性image的观察者。
当你用完以后,你还需要注销观察者。还是在AlbumView.swift,添加如下代码:
deinit {
coverImage.removeObserver(self, forKeyPath: "image")
}
最后添加如下方法:
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if keyPath == "image" {
indicator.stopAnimating()
}
}
作为一个观察者,你必须在每一个类里面都适用这个方法。每当被观察的属性发生改变时系统都会执行这个方法。在上面的代码中。当“image”属性发生变化时停止转动。这样,当一个图像加载完成时,就会停止转动。
编译运行,这时转动加载将会消失:

注意:一定要记住当观察者结束时,移除它们。否则你的app会在对象视图发送消息给那些不存在的观察者时崩溃。
如果你玩弄一下你的app并且终止他,你将会发现你app的状态没有保存。当app启动的时候,你最后观察的相册不会时默认的相册。
为了修正这个问题,你可以使用下一个设计模式-纪念品模式(Memento)。
The Memento Pattern
纪念品模式捕获并展示项目的初始状态。用另外一句话说就是,他保存你某一时刻的事情。然后,这种外部状态在不违背封装的情况下恢复,也就是说,私有数据还是私有的。
How to Use Memnto pattern
添加如下代码到viewcontroler.swift:
//MARK: Memento Pattern
func saveCurrentState() {
// When the user leaves the app and then comes back again, he wants it to be in the exact same state
// he left it. In order to do this we need to save the currently displayed album.
// Since it's only one piece of information we can use NSUserDefaults.
NSUserDefaults.standardUserDefaults().setInteger(currentAlbumIndex, forKey: "currentAlbumIndex")
}
func loadPreviousState() {
currentAlbumIndex = NSUserDefaults.standardUserDefaults().integerForKey("currentAlbumIndex")
showDataForAlbum(currentAlbumIndex)
}
saveCurrentState()保存当前相册的序号到NSUserDefaults – NSUserDefaults是由IOS为了保存应用具体的设置和数据提供的标准数据存储。
loadPreviousState()加载之前保存的序号。这并不是纪念品模式全部的应用,你学到这儿即可。
现在,在scroller.delegate = self:添加下边这一行到ViewControler.swift的viewDidLoad
loadPreviousState()
这样当app启动的时候就会加载之前保存的状态。但是你从那个地方保存当前app的状态呢?你可以用通知解决这个问题。当app进入后台时ios发售一个UIApplicationDidEnterBackgroundNotification的通知。你可以用这个通知去调用saveCurrentState,这不是很方便吗?
在viewDidLoad方法末尾添加下边这一行
NSNotificationCenter.defaultCenter().addObserver(self, selector:"saveCurrentState", name: UIApplicationDidEnterBackgroundNotification, object: nil)
现在当app进入后台运行的时候,ViewControler将会通过调用saveCurrentState自动的保存当前状态。
想往常一样,你还需要注销观察者通知。添加如下代码:
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
这可以保证当ViewController被释放时,移除类的观察者。
编译运行你的程序,指到其中的一个相册,点击 home键(command+shift+H如果你用的是虚拟机)然后从Xcode里面关闭你的应用程序。重新启动发现之前选中的相册已经居中了。

这就是方法initialViewIndexForHorizontalScroller 要做的事情!然而这个方法没有在delegate中使用,在这个例子中ViewControler的初始view总是设置为第一个view。
为了修复这个问题,添加如下代码到ViewControler.swift中:
func initialViewIndex(scroller: HorizontalScroller) -> Int {
return currentAlbumIndex
}
现在HorizontalScroller的第一个view的相册已经设置为由currentAlbumIndex决定的。这是一个确保app保持私人化并且可以重用的好方法。
再次运行你的app,按照上一步的操作执行,发现问题已经解决了。

其中的一个办法是通过相册是属性迭代,把他们保存到一个plist文件中,当需要的时候重建Album的实例。但这并不是最好的选择,因为你需要写的代码决定于每一个类中的数据或属性。例如,你后来创建了一个有不同属性的Movie类,那么报讯和加载数据需要写一段新的代码。
另外,你不能保存每一个类实例中的私有变量,因为他们不能被外部类读写,这就是为什么Apple创建了archiving机制的原因了。
Archiving(档案)
Apple在纪念品模式中最特别的应用之一就是Archiving.他把对象转化成一个流,这样就能在不暴漏私有属性给外部类的情况下存储和重新加载。你也可以在iOS6指导书的第16章阅读更多与这个方程相关的工作。或者在Apple’s Archives and Serializations Programming Guide.
How to Use Archving
首先,你需要声明相册可以通过遵守NSCoding协议被存档。打开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协议的一部分,encodingWithCoder函数将会在你请求一个Album实例的时候被存档。相反,init(coder:)初始化将会被用于从保存的实例中重建或者回档。它很简单,但是很强大!
现在,Album类能够被存档了,添加如下代码,使它能够真正的保存和加载相册列表。
添加如下方法到PersistencyManager.swift.
func saveAlbums() {
var filename = NSHomeDirectory().stringByAppendingString("/Documents/albums.bin")
let data = NSKeyedArchiver.archivedDataWithRootObject(albums)
data.writeToFile(filename, atomically: true)
}
这个是将要被用来调用保存相册的方法。NSKeyedArchiver将相册数组归档到一个叫做album.bin的文件中。
当你归档一个包含其他对象的对象时,归档器将会自动地递归存档子对象和子对象的子对象。在这个例子中,从album开始归档,这是一个相册数组的实例。既然数组和相册都支持NSCopying接口,数组中的每一个都会被归档。
现在用下面的代码重写PersistencyManager.swift的init方法:
override init() {
super.init()
if let data = NSData(contentsOfFile: NSHomeDirectory().stringByAppendingString("/Documents/albums.bin")) {
let unarchiveAlbums = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! [Album]?
if let unwrappedAlbum = unarchiveAlbums {
albums = unwrappedAlbum
}
} else {
createPlaceholderAlbum()
}
}
func createPlaceholderAlbum(){
//Dummy list of albums
let album1 = Album(title: "Best of Bowie",
artist: "David Bowie",
genre: "Pop",
coverUrl: "https://upload.wikimedia.org/wikipedia/en/2/29/Best_of_bowie.jpg",
year: "1992")
let album2 = Album(title: "It's My Life",
artist: "No Doubt",
genre: "Pop",
coverUrl: "https://upload.wikimedia.org/wikipedia/en/c/c1/BonJoviItsMyLifeCDSingleCover.jpg",
year: "2003")
let album3 = Album(title: "Nothing Like The Sun",
artist: "Sting",
genre: "Pop",
coverUrl: "https://upload.wikimedia.org/wikipedia/en/3/30/%E2%80%A6Nothing_Like_the_Sun_(Sting_album_-_cover_art).jpg",
year: "1999")
let album4 = Album(title: "Staring at the Sun",
artist: "U2",
genre: "Pop",
coverUrl: "https://upload.wikimedia.org/wikipedia/en/2/22/Rooster_-_Staring_At_The_Sun_-Single-.jpg",
year: "2000")
let album5 = Album(title: "American Pie",
artist: "Madonna",
genre: "Pop",
coverUrl: "http://ecx.images-amazon.com/images/I/71WZbVhqbkL._SL1300_.jpg",
year: "2000")
albums = [album1, album2, album3, album4, album5]
saveAlbums()
}
为了提高可读性,你已经将placeholder album creation代码放到一个独立的方法createPlaceholderAlbum中。在新的代码里面,如果存在文件,NSKeyedUnarchiver从文件里加载相册数据。如果不存在的话他就创建相册数据并且为了保证下一次的启动立即保存。
每次app进入后台的时候,你还想要保存相册数据。这个现在看起来没有必要,但是如果你以后添加了修改相册信息的操作怎么办?然后,你就要确保你所有的更改都会被保存。
因为所有主要的存取操作都是通过LibraryAPI完成的,这就是应用如何使PersistencyManager知道什么时候需要保存相册数据。
现在添加执行方法到LibraryAPI.swift
func saveAlbums() {
persistencyManager.saveAlbums()
}
为了保存相册到PersistencyManger,这段代码简单地传递了一个请求到LibraryAPI。
添加如下代码到ViewControler.swift的saveCurrentState的末尾:
LibraryAPI.sharedInstance.saveAlbums()
上面这段代码是无论什么时候ViewController保存他的状态都是用LibraryAPI去触发保存相册信息。
编译运行你的程序,检查所有的编译。
不幸的是,没有一种简单的方法可以检查数据是否持续正确的写入。你可以查看你的App的监视文档目录看看相册信息的文件是否被创建,但是为了看的其他的变化,你必须添加修改相册数据的技能。
但是如果不是修改数据,你怎样添加一个方法可以删除Library中你不再想要的相册?另外,如果你误删了一个相册这时有撤消操作不是很好嘛。
Final Touch
你要通过允许用户使用删除动作来移除一个相册或者是万一他改变了想法要撤消操作最后完善你的音乐应用。
添加如下属性到ViewControler:
// We will use this array as a stack to push and pop operation for the undo option
var undoStack: [(Album, Int)] = []
这是创建了一个空的撤销栈。这个撤销栈有一个两个变量的数组。第一个是相册,第二个是相册的编号。
在viewDidload中的reloadScroller()后面添加如下代码:
let undoButton = UIBarButtonItem(barButtonSystemItem: .Undo, target: self, action:"undoAction")
undoButton.enabled = false;
let space = UIBarButtonItem(barButtonSystemItem: .FlexibleSpace, target:nil, action:nil)
let trashButton = UIBarButtonItem(barButtonSystemItem: .Trash, target:self, action:"deleteAlbum")
let toolbarButtonItems = [undoButton, space, trashButton]
toolbar.setItems(toolbarButtonItems, animated: true)
上面的代码是为了创建一个有两个按钮并且他们之间空隙可变的工具栏。撤销按钮现在还不能用,因为撤销栈还是空的。需要注意toolbar已经在storyboard里面了,所以你只需要设置一下。
你为了处理相册管理的三个动作:添加,删除和撤销,添加了三种方法到ViewControler.swift
第一个方法是添加一个新的相册。
func addAlbumAtIndex(album: Album,index: Int) {
LibraryAPI.sharedInstance.addAlbum(album, index: index)
currentAlbumIndex = index
reloadScroller()
}
添加一个新的相册,设置当前的相册序号,重新加载scroller。
接下来添加删除操作:
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
}
}
思考下面每一小节的内容。
1.获取要删除的相册。
2.创建叫做undoAction的变量来存储一个关于相册和相册序号的数组。然后让数组进栈。
3.用LibraryAPI从数据结构中删除相册并且重新加载。
4.既然在撤销栈里面有动作,你就需要激活这个撤销按钮。
5.最后检查是否有相册留下,如果没有你可以disable这个垃圾桶按钮。
最后,添加撤消操作:
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
}
最后解释一下上面的几个方法:
1.第一步是出栈操作,给你一个包含删除的相册和序号的数组。然后你处理并找回相册。
2.既然当你“poped”它的时候你也删除了栈里面最后一个对象,现在你就需要检查栈是否为空。如果为空,这就意味着没有需要撤销操作的。因此可以disable撤销按钮。
3.你也明白既然你已经做了一个撤销动作,所以这儿至少有一个相册封面。因此你就要激活这个垃圾桶按钮。
编译运行你的程序测试一下我们的撤销机制,删除一个或两个相册再点击撤销按钮看看他的反应:

如果你想找回所有的相册,你可以删除app再重新运行一遍即可。