├── .gitignore ├── Chapter01 └── iOS-Design-Patterns.md ├── Chapter02 └── GettingStarted.md ├── Chapter03 ├── MVC.md └── Use-MVC.md ├── Chapter04 ├── Singleton.md └── Use-Singleton.md ├── Chapter05 ├── Facade.md └── Use-Facade.md ├── Chapter06 ├── Decorator-Delegation.md ├── Decorator-Extension.md ├── Decorator.md ├── Use-Decorator-Delegation.md └── Use-Decorator-Extension.md ├── Chapter07 ├── Adapter.md └── Use-Adapter.md ├── Chapter08 ├── KVO.md ├── Notification.md └── Observer.md ├── Chapter09 ├── Archiving.md ├── Memento.md └── Use-Memento.md ├── Chapter10 └── FinalTouches.md ├── Chapter11 └── Final.md ├── LICENSE ├── README.md ├── SUMMARY.md └── images ├── adapter1.png ├── adapter2.png ├── adapter3.png ├── adapter4.png ├── decorator1.png ├── decorator2.png ├── decorator3.png ├── decorator4.png ├── facade1.jpg ├── facade2.png ├── facade3.png ├── facade4.png ├── kvo1.png ├── memento1.png ├── memento2.png ├── mvc1.png ├── mvc2.png ├── mvc3.png ├── notifications1.png ├── singleton1.png ├── swiftDesignPattern1.png ├── swiftDesignPattern2.png └── swiftDesignPattern3.png /.gitignore: -------------------------------------------------------------------------------- 1 | _book 2 | -------------------------------------------------------------------------------- /Chapter01/iOS-Design-Patterns.md: -------------------------------------------------------------------------------- 1 | # iOS 设计模式 2 | 3 | 说到设计模式,相信大家都不陌生,但是又有多少人知道它背后的真正含义?绝大多数程序员都知道设计模式十分重要,不过关于这个话题的文章却不是很多,开发者们在开发的时候有时也不太在意设计模式方面的内容。 4 | 5 | 设计模式针对软件设计中的常见问题,提供了一些可复用的解决方案,开发者可以通过这些模板写出易于理解且能够复用的代码。正确的使用设计模式可以降低代码之间的耦合度,从而很轻松的修改或者替换以前的代码。 6 | 7 | 如果你对设计模式还很陌生,那么告诉你一个好消息!在 iOS 的开发过程中,其实你不知不觉已经用了很多设计模式。这得益于 Cocoa 提供的框架和一些良好的编程习惯。接下来的这篇教程将会带你一起飞,去领略设计模式的魅力。 8 | 9 | ## 常见模式 10 | 11 | 第一部分我们将会完成一个完整的应用,展示音乐专辑和专辑的相关信息。 12 | 13 | 通过这个应用,我们会接触一些 Cocoa 中常见的设计模式: 14 | 15 | - 创建型 (Creational):单例模式 (Singleton) 16 | - 结构型 (Structural):MVC、装饰者模式 (Decorator)、适配器模式 (Adapter)、外观模式 (Facade) 17 | - 行为型 (Behavioral):观察者模式 (Observer)、备忘录模式 (Memento) 18 | 19 | 嘿嘿嘿别愁眉苦脸的嘛,这篇文章不是什么长篇大论的理论知识,你会在开发应用的过程中慢慢学会这些设计模式。 20 | 21 | 先来预览一下最终的结果: 22 | 23 | ![](../images/swiftDesignPattern1.png) 24 | -------------------------------------------------------------------------------- /Chapter02/GettingStarted.md: -------------------------------------------------------------------------------- 1 | # 开始 2 | 3 | 下载[初始项目](https://github.com/yourtion/SwiftDesignPatterns-Demo1/archive/starter_project.zip)并解压,在 Xcode 中打开 `BlueLibrarySwift.xcodeproj` 项目文件。 4 | 5 | 项目中有三个地方需要注意一下: 6 | 7 | - `ViewController` 有两个 `IBOutlet` ,分别连接到了 `UITableView` 和 `UIToolBar` 上。 8 | 9 | - 在 StoryBoard 上有三个组件设置了约束。最上面的是专辑的封面,封面下面是列举了相关专辑的列表,最下面是有两个按钮的工具栏,一个用来撤销操作,另一个用来删除你选中的专辑。 StoryBoard 看起来是这个样子的: 10 | 11 | ![](../images/swiftDesignPattern2.png) 12 | 13 | - 一个简单的 HTTP 客户端类 (HTTPClient) ,里面还没有什么内容,需要你去完善。 14 | 15 | > 注意:其实当你创建一个新的 Xcode 的项目的时候,你的代码里就已经有很多设计模式的影子了: MVC、委托、代理、单例 - 真是众里寻他千百度,得来全不费功夫。 16 | 17 | 在学习第一个设计模式之前,你需要创建两个类,用来存储和展示专辑数据。 18 | 19 | 创建一个新的类,继承 `NSObject` 名为 `Album` ,记得选择 Swift 作为编程语言然后点击下一步。 20 | 21 | 打开 `Album.swift` 然后添加如下定义: 22 | 23 | ```swift 24 | var title : String! 25 | var artist : String! 26 | var genre : String! 27 | var coverUrl : String! 28 | var year : String! 29 | ``` 30 | 31 | 这里创建了五个属性,分别对应专辑的标题、作者、流派、封面地址和出版年份。 32 | 33 | 接下来我们添加一个初始化方法: 34 | 35 | ```swift 36 | init(title: String, artist: String, genre: String, coverUrl: String, year: String) { 37 | super.init() 38 | self.title = title 39 | self.artist = artist 40 | self.genre = genre 41 | self.coverUrl = coverUrl 42 | self.year = year 43 | } 44 | ``` 45 | 46 | 这样我们就可以愉快的初始化了。 47 | 48 | 然后再加上下面这个方法: 49 | 50 | ```swift 51 | override var description: String { 52 | return "title: \(title)" + 53 | "artist: \(artist)" + 54 | "genre: \(genre)" + 55 | "coverUrl: \(coverUrl)" + 56 | "year: \(year)" 57 | } 58 | ``` 59 | 60 | 这是专辑对象的描述方法,详细的打印了 `Album` 的所有属性值,方便我们查看变量各个属性的值。 61 | 62 | 接下来,再创建一个继承自 `UIView` 的视图类 `AlbumView.swift`。 63 | 64 | 在新建的类中添加两个属性: 65 | 66 | ```swift 67 | private var coverImage: UIImageView! 68 | private var indicator: UIActivityIndicatorView! 69 | ``` 70 | 71 | `coverImage` 代表了封面的图片,`indicator` 则是在加载过程中显示的等待指示器。 72 | 73 | 74 | 这两个属性都是私有属性,因为除了 `AlbumView` 之外,其他类没有必要知道他俩的存在。在写一些框架或者类库的时候,这种规范十分重要,可以避免一些误操作。 75 | 76 | 接下来给这个类添加初始化化方法: 77 | 78 | ```swift 79 | required init?(coder aDecoder: NSCoder) { 80 | super.init(coder: aDecoder)! 81 | } 82 | 83 | init(frame: CGRect, albumCover: String) { 84 | super.init(frame: frame) 85 | backgroundColor = UIColor.blackColor() 86 | coverImage = UIImageView(frame: CGRectMake(5, 5, frame.size.width - 10, frame.size.height - 10)) 87 | addSubview(coverImage) 88 | indicator = UIActivityIndicatorView() 89 | indicator.center = center 90 | indicator.activityIndicatorViewStyle = .WhiteLarge 91 | indicator.startAnimating() 92 | addSubview(indicator) 93 | } 94 | ``` 95 | 96 | 因为 `UIView` 遵从 `NSCoding` 协议,所以我们需要 `NSCoder` 的初始化方法。不过目前我们没有 `encode` 和 `decode` 的必要,所以就把它放在那里就行,调用父类方法初始化即可。 97 | 98 | 在真正的初始化方法里,我们设置了一些初始化的默认值。比如设置背景颜色默认为黑色,创建 `ImageView` 并设置了 `margin` 值,添加了一个加载指示器。 99 | 100 | 最终我们再加上如下方法: 101 | 102 | ```swift 103 | func highlightAlbum(didHighlightView didHighlightView: Bool) { 104 | if didHighlightView == true { 105 | backgroundColor = UIColor.whiteColor() 106 | } else { 107 | backgroundColor = UIColor.blackColor() 108 | } 109 | } 110 | ``` 111 | 112 | 这会切换专辑的背景颜色,如果高亮就是白色,否则就是黑色。 113 | 114 | 在继续下面的内容之前, `Command + B` 试一下确保没有什么问题,一切正常?那就开始第一个设计模式的学习啦! 115 | 116 | 完成到这一步的Demo: 117 | 118 | - [查看源码](https://github.com/yourtion/SwiftDesignPatterns-Demo1/tree/GettingStarted) 119 | - [下载Zip](https://github.com/yourtion/SwiftDesignPatterns-Demo1/archive/GettingStarted.zip) 120 | 121 | -------------------------------------------------------------------------------- /Chapter03/MVC.md: -------------------------------------------------------------------------------- 1 | # 设计模式之王 - MVC 2 | 3 | ![](../images/mvc1.png) 4 | 5 | `Model-View-Controller` (缩写 MVC ) 是 Cocoa 框架的一部分,并且毋庸置疑是最常用的设计模式之一。它可以帮你把对象根据职责进行划分和归类。 6 | 7 | 作为划分依据的三个基本职责是: 8 | 9 | - 模型层 (Model) :存储数据并且定义如何操作这些数据。在我们的例子中,就是 `Album` 类。 10 | - 视图层 (View) :负责模型层的可视化展示,并且负责用户的交互,一般来说都是继承自 `UIView` 这个基类。在我们的项目中就是 `AlbumView` 这个类。 11 | - 控制器 (Controller) :控制器是整个系统的掌控者,它连接了模型层和数据层,并且把数据在视图层展示出来,监听各种事件,负责数据的各种操作。不妨猜猜在我们的项目中哪个是控制器?啊哈猜对了:`ViewController` 这个类就是。 12 | 13 | 如果你的项目遵循 MVC 的设计模式,那么各种对象要不是 Model ,要不是 View ,要不就是 Controller。当然在实际的开发中也可以灵活变化,比如结合具体业务使用 MVVM 结构给 `ViewController` 瘦瘦身,也是可以的。 14 | 15 | 三者之间的关系如下: 16 | 17 | ![](../images/mvc2.png) 18 | 19 | 模型层通知控制器层任何数据的变化,然后控制器层会刷新视图层中的数据。视图层可以通知控制器层用户的交互事件,然后控制器会处理各种事件以及刷新数据。 20 | 21 | 你可能会感觉奇怪:为什么要把这三个东西分开来,而不能揉在一个类里呢?那样似乎更简单一点嘛。 22 | 23 | 之所以这样做,是为了将代码更好的分离和重用。理想状态下,视图层应当和模型层完全分离。如果视图层不依赖任何模型层的具体实现,那么就可以很容易的被其他模型复用,用来展示不同的数据。 24 | 25 | 举个例子,比如在未来我们需要添加电影或者什么书籍,我们依旧可以使用 `AlbumView` 这个类作为展示。更久远点来说,在以后如果你创建了一个新的项目并且需要用到和专辑相关的内容,你可以直接复用 `Album` 类因为它并不依赖于任何视图模块。这就是 MVC 的强大之处,三大元素,各司其职,减少依赖。 26 | 27 | -------------------------------------------------------------------------------- /Chapter03/Use-MVC.md: -------------------------------------------------------------------------------- 1 | ## 如何使用 MVC 模式 2 | 3 | 首先,你需要确定你的项目中的每个类都是三大基本类型中的一种:控制器、模型、视图。不要在一个类里糅合多个角色。目前我们创建了 `Album` 类和 `AlbumView` 类是符合要求的,做得很好。 4 | 5 | 然后,为了确保你遵循这种模式,你最好创建三个项目分组来存放代码,分别是 Model、View、Controller,保持每个类型的文件分别独立。 6 | 7 | 接下来把 `Album.swift` 拖到 `Model` 分组,把 `AlbumView.swift` 拖到 `View` 分组,然后把 `ViewController.swift` 拖到 `Controller` 分组中。 8 | 9 | 现在你的项目应该是这个样子: 10 | 11 | ![](../images/mvc3.png) 12 | 13 | 现在你的项目已经有点样子了,不再是各个文件颠沛流离居无定所了。显然你还会有其他分组和类,但是应用的核心就在这三个类里。 14 | 15 | 现在你的内容已经组织好了,接下来要做的就是获取专辑的数据。你将会创建一个 API 类来管理数据 - 这里我们会用到下一个设计模式:单例模式。 16 | -------------------------------------------------------------------------------- /Chapter04/Singleton.md: -------------------------------------------------------------------------------- 1 | # 单例模式 - Singleton 2 | 3 | 单例模式确保每个指定的类只存在一个实例对象,并且可以全局访问那个实例。一般情况下会使用延时加载的策略,只在第一次需要使用的时候初始化。 4 | 5 | 注意:在 iOS 中单例模式很常见,`NSUserDefaults.standardUserDefaults()` 、 `UIApplication.sharedApplication()` 、 `UIScreen.mainScreen()` 、 `NSFileManager.defaultManager()` 这些都是单例模式。 6 | 7 | 你可能会疑惑了:如果多于一个实例又会怎么样呢?代码和内存还没精贵到这个地步吧? 8 | 9 | 某些场景下,保持实例对象仅有一份是很有意义的。举个例子,你的应用实例 (`UIApplication`),应该只有一个吧,显然是指你的当前应用。还有一个例子:设备的屏幕 (`UIScreen`) 实例也是这样,所以对于这些类的情况,你只想要一个实例对象。 10 | 11 | 单例模式的应用还有另一种情况:你需要一个全局类来处理配置文件。我们很容易通过单例模式实现线程安全的实例访问,而如果有多个类可以同时访问配置文件,那可就复杂多了。 12 | 13 | -------------------------------------------------------------------------------- /Chapter04/Use-Singleton.md: -------------------------------------------------------------------------------- 1 | ## 如何使用单例模式 2 | 3 | 可以看下这个图: 4 | 5 | ![](../images/singleton1.png) 6 | 7 | 这是一个日志类,有一个属性 (是一个单例对象) 和两个方法 (`sharedInstance()` 和 `init()`)。 8 | 9 | 第一次调用 `sharedInstance()` 的时候,`instance` 属性还没有初始化。所以我们要创建一个新实例并且返回。 10 | 11 | 下一次你再调用 `sharedInstance()` 的时候,`instance` 已经初始化完成,直接返回即可。这个逻辑确保了这个类只存在一个实例对象。 12 | 13 | 接下来我们继续完善单例模式,通过这个类来管理专辑数据。 14 | 15 | 注意到在我们前面的截图里,分组中有个 `API` 分组,这里可以放那些提供后台服务的类。在这个分组中创建一个新的文件 `LibraryAPI.swift` ,继承自 `NSObject` 类。 16 | 17 | 在 `LibraryAPI` 里添加下面这段代码: 18 | 19 | ```swift 20 | //1 21 | class var sharedInstance: LibraryAPI { 22 | //2 23 | struct Singleton { 24 | //3 25 | static let instance = LibraryAPI() 26 | } 27 | //4 28 | return Singleton.instance 29 | } 30 | ``` 31 | 32 | 在这几行代码里,做了如下工作: 33 | 34 | 创建一个计算类型的类变量,这个类变量,就像是 objc 中的静态方法一样,可以直接通过类访问而不用实例对象。具体可参见苹果官方文档的 属性 这一章。 35 | 36 | 在类变量里嵌套一个 `Singleton` 结构体。 37 | 38 | `Singleton` 封装了一个静态的常量,通过 `static` 定义意味着这个属性只存在一个,注意 Swift 中 `static` 的变量是延时加载的,意味着 `Instance` 直到需要的时候才会被创建。 39 | 40 | 同时再注意一下,因为它是一个常量,所以一旦创建之后不会再创建第二次。这些就是单例模式的核心所在:一旦初始化完成,当前类存在一个实例对象,初始化方法就不会再被调用。 41 | 42 | 返回计算后的属性值。 43 | 44 | 注意:更多的单例模式实例可以看看 Github 上的[这个示例](https://github.com/hpique/SwiftSingleton),列举了单例模式的若干种实现方式。 45 | 46 | 你现在可以将这个单例作为专辑管理类的入口,接下来我们继续创建一个处理专辑数据持久化的类。 47 | 48 | 新建 `PersistencyManager.swift` 并添加如下代码: 49 | 50 | ```swift 51 | private var albums = [Album]() 52 | ``` 53 | 54 | 在这里我们定义了一个私有属性,用来存储专辑数据。这是一个可变数组,所以你可以很容易的增加或者删除数据。 55 | 56 | 然后加上一些初始化的数据: 57 | 58 | ```swift 59 | override init() { 60 | //Dummy list of albums 61 | let album1 = Album(title: "Best of Bowie", 62 | artist: "David Bowie", 63 | genre: "Pop", 64 | coverUrl: "http://img3.douban.com/mpic/s1497881.jpg", 65 | year: "1992") 66 | 67 | let album2 = Album(title: "It's My Life", 68 | artist: "No Doubt", 69 | genre: "Pop", 70 | coverUrl: "http://img3.doubanio.com/mpic/s3880529.jpg", 71 | year: "2003") 72 | 73 | let album3 = Album(title: "Nothing Like The Sun", 74 | artist: "Sting", 75 | genre: "Pop", 76 | coverUrl: "http://img3.doubanio.com/mpic/s3708339.jpg", 77 | year: "1999") 78 | 79 | let album4 = Album(title: "Staring at the Sun", 80 | artist: "U2", 81 | genre: "Pop", 82 | coverUrl: "http://img3.douban.com/mpic/s1882422.jpg", 83 | year: "2000") 84 | 85 | let album5 = Album(title: "American Pie", 86 | artist: "Madonna", 87 | genre: "Pop", 88 | coverUrl: "http://img3.douban.com/mpic/s3105351.jpg", 89 | year: "2000") 90 | 91 | albums = [album1, album2, album3, album4, album5] 92 | } 93 | ``` 94 | 95 | 在这个初始化方法里,我们初始化了五张专辑。如果上面的专辑没有你喜欢的,你可以随意替换成你的菜:] 96 | 97 | 然后添加如下方法: 98 | 99 | ```swift 100 | func getAlbums() -> [Album] { 101 | return albums 102 | } 103 | 104 | func addAlbum(album: Album, index: Int) { 105 | if (albums.count >= index) { 106 | albums.insert(album, atIndex: index) 107 | } else { 108 | albums.append(album) 109 | } 110 | } 111 | 112 | func deleteAlbumAtIndex(index: Int) { 113 | albums.removeAtIndex(index) 114 | } 115 | ``` 116 | 117 | 这些方法可以让你自由的访问、添加、删除专辑数据。 118 | 119 | 这时你可以运行一下你的项目,确保编译通过以便进行下一步操作。 120 | 121 | 此时你或许会感到好奇: `PersistencyManager` 好像不是单例啊?是的,它确实不是单例。不过没关系,在接下来的外观模式章节,你会看到 `LibraryAPI` 和 `PersistencyManagerx` 之间的联系。 122 | 123 | 完成到这一步的Demo: 124 | 125 | - [查看源码](https://github.com/yourtion/SwiftDesignPatterns-Demo1/tree/Use-Singleton) 126 | - [下载Zip](https://github.com/yourtion/SwiftDesignPatterns-Demo1/archive/Use-Singleton.zip) 127 | 128 | -------------------------------------------------------------------------------- /Chapter05/Facade.md: -------------------------------------------------------------------------------- 1 | # 外观模式 - Facade 2 | 3 | ![](../images/facade1.jpg) 4 | 5 | 外观模式在复杂的业务系统上提供了简单的接口。如果直接把业务的所有接口直接暴露给使用者,使用者需要单独面对这一大堆复杂的接口,学习成本很高,而且存在误用的隐患。如果使用外观模式,我们只要暴露必要的 API 就可以了。 6 | 7 | 下图演示了外观模式的基本概念: 8 | 9 | ![](../images/facade2.png) 10 | 11 | API 的使用者完全不知道这内部的业务逻辑有多么复杂。当我们有大量的类并且它们使用起来很复杂而且也很难理解的时候,外观模式是一个十分理想的选择。 12 | 13 | 外观模式把使用和背后的实现逻辑成功解耦,同时也降低了外部代码对内部工作的依赖程度。如果底层的类发生了改变,外观的接口并不需要做修改。 14 | 15 | 举个例子,如果有一天你想换掉所有的后台服务,你只需要修改 API 内部的代码,外部调用 API 的代码并不会有改动。 16 | -------------------------------------------------------------------------------- /Chapter05/Use-Facade.md: -------------------------------------------------------------------------------- 1 | ## 如何使用外观模式 2 | 3 | 现在我们用 `PersistencyManager` 来管理专辑数据,用 `HTTPClient` 来处理网络请求,项目中的其他类不应该知道这个逻辑。他们只需要知道 `LibraryAPI` 这个“外观”就可以了。 4 | 5 | 为了实现外观模式,应该只让 `LibraryAPI` 持有 `PersistencyManager` 和 `HTTPClient` 的实例,然后 `LibraryAPI` 暴露一个简单的接口给其他类来访问,这样外部的访问类不需要知道内部的业务具体是怎样的,也不用知道你是通过 `PersistencyManager` 还是 `HTTPClient` 获取到数据的。 6 | 7 | 大致的设计是这样的: 8 | 9 | ![](../images/facade3.png) 10 | 11 | `LibraryAPI` 会暴露给其他代码访问,但是 `PersistencyManager` 和 `HTTPClient` 则是不对外开放的。 12 | 13 | 打开 `LibraryAPI.swift` 然后添加如下代码: 14 | 15 | ```swift 16 | private let persistencyManager: PersistencyManager 17 | private let httpClient: HTTPClient 18 | private let isOnline: Bool 19 | ``` 20 | 21 | 除了两个实例变量之外,还有个 `Bool` 值: `isOnline` ,这个是用来标识当前是否为联网状态的,如果是联网状态就会去网络获取数据。 22 | 23 | 我们需要在 `init` 里面初始化这些变量: 24 | 25 | ```swift 26 | override init() { 27 | persistencyManager = PersistencyManager() 28 | httpClient = HTTPClient() 29 | isOnline = false 30 | 31 | super.init() 32 | } 33 | ``` 34 | 35 | `HTTPClient` 并不会直接和真实的服务器交互,只是用来演示外观模式的使用。所以 `inOnline` 这个值我们一直设置为 `false`。 36 | 37 | 接下来在 `LibraryAPI.swift` 里添加如下代码: 38 | 39 | ```swift 40 | func getAlbums() -> [Album] { 41 | return persistencyManager.getAlbums() 42 | } 43 | 44 | func addAlbum(album: Album, index: Int) { 45 | persistencyManager.addAlbum(album, index: index) 46 | if isOnline { 47 | httpClient.postRequest("/api/addAlbum", body: album.description()) 48 | } 49 | } 50 | 51 | func deleteAlbum(index: Int) { 52 | persistencyManager.deleteAlbumAtIndex(index) 53 | if isOnline { 54 | httpClient.postRequest("/api/deleteAlbum", body: "\(index)") 55 | } 56 | } 57 | ``` 58 | 59 | 看一下 `addAlbum(_:index:)` 这个方法,先更新本地缓存,然后如果是联网状态还需要向服务器发送网络请求。这便是外观模式的强大之处:如果外部文件想要添加一个新的专辑,它不会也不用去了解内部的实现逻辑是怎么样的。 60 | 61 | 注意:当你设计外观的时候,请务必牢记:使用者随时可能直接访问你的隐藏类。永远不要假设使用者会遵循你当初的设计做事。 62 | 63 | 运行一下你的应用,你可以看到两个空的页面和一个工具栏:最上面的视图用来展示专辑封面,下面的视图展示数据列表。 64 | 65 | ![](../images/facade4.png) 66 | 67 | 你需要在屏幕上展示专辑数据,这是就该用下一种设计模式了:装饰者模式。 68 | 69 | 完成到这一步的Demo: 70 | 71 | - [查看源码](https://github.com/yourtion/SwiftDesignPatterns-Demo1/tree/Use-Facade) 72 | - [下载Zip](https://github.com/yourtion/SwiftDesignPatterns-Demo1/archive/Use-Facade.zip) 73 | 74 | -------------------------------------------------------------------------------- /Chapter06/Decorator-Delegation.md: -------------------------------------------------------------------------------- 1 | ## 委托 2 | 3 | 装饰者模式的另一种实现方案是委托。在这种机制下,一个对象可以和另一个对象相关联。比如你在用 `UITableView` ,你必须实现 `tableView(_:numberOfRowsInSection:)` 这个委托方法。 4 | 5 | 你不应该指望 `UITableView` 知道你有多少数据,这是个应用层该解决的问题。所以,数据相关的计算应该通过 `UITableView` 的委托来解决。这样可以让 `UITableView` 和数据层分别独立。视图层就负责显示数据,你递过来什么我就显示什么。 6 | 7 | 下面这张图很好的解释了 `UITableView` 的工作过程: 8 | 9 | ![](../images/decorator3.png) 10 | 11 | `UITableView` 的工作仅仅是展示数据,但是最终它需要知道自己要展示那些数据,这时就可以向它的委托询问。在 objc 的委托模式里,一个类可以通过协议来声明可选或者必须的方法。 12 | 13 | 看起来似乎继承然后重写必须的方法来的更简单一点。但是考虑一下这个问题:继承的结果必定是一个独立的类,如果你想让某个对象成为多个对象的委托,那么子类这招就行不通了。 14 | 15 | 注意:委托模式十分重要,苹果在 UIKit 中大量使用了该模式,基本上随处可见。 -------------------------------------------------------------------------------- /Chapter06/Decorator-Extension.md: -------------------------------------------------------------------------------- 1 | ## 扩展 2 | 3 | 扩展是一种十分强大的机制,可以让你在不用继承的情况下,给已存在的类、结构体或者枚举类添加一些新的功能。最重要的一点是,你可以在你没有访问权限的情况下扩展已有类。这意味着你甚至可以扩展 Cocoa 的类,比如 `UIView` 或者 `UIImage` 。 4 | 5 | 举个例子,在编译时新加的方法可以像扩展类的正常方法一样执行。这和装饰器模式有点不同,因为扩展不会持有扩展类的对象。 -------------------------------------------------------------------------------- /Chapter06/Decorator.md: -------------------------------------------------------------------------------- 1 | # 装饰者模式 - Decorator 2 | 3 | 装饰者模式可以动态的给指定的类添加一些行为和职责,而不用对原代码进行任何修改。当你需要使用子类的时候,不妨考虑一下装饰者模式,可以在原始类上面封装一层。 4 | 5 | 在 Swift 里,有两种方式实现装饰者模式:扩展 (Extension) 和委托 (Delegation)。 6 | -------------------------------------------------------------------------------- /Chapter06/Use-Decorator-Delegation.md: -------------------------------------------------------------------------------- 1 | ## 如何使用委托模式 2 | 3 | 打开 `ViewController.swift` 文件,添加如下私有变量: 4 | 5 | ```swift 6 | private var allAlbums = [Album]() 7 | private var currentAlbumData : (titles:[String], values:[String])? 8 | private var currentAlbumIndex = 0 9 | ``` 10 | 11 | 在 `viewDidLoad` 里面加入如下内容: 12 | 13 | ```swift 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | //1 17 | self.navigationController?.navigationBar.translucent = false 18 | currentAlbumIndex = 0 19 | 20 | //2 21 | allAlbums = LibraryAPI.sharedInstance.getAlbums() 22 | 23 | // 3 24 | // the uitableview that presents the album data 25 | dataTable.delegate = self 26 | dataTable.dataSource = self 27 | dataTable.backgroundView = nil 28 | view.addSubview(dataTable!) 29 | } 30 | ``` 31 | 32 | 对上面三个部分进行拆解: 33 | 34 | 1. 关闭导航栏的透明效果 35 | 2. 通过 API 获取所有的专辑数据,记住,我们使用外观模式之后,应该从 `LibraryAPI` 获取数据,而不是 `PersistencyManager` 。 36 | 3. 你可以在这里设置你的 `UITablweView` ,在这里声明了 `UITableView` 的 `delegate` 是当前的 `ViewController` 。事实上你用了 XIB 或者 StoryBoard ,可以直接在可视化的页面里拖拽完成。 37 | 38 | 接下来添加一个新的方法用来更方便的获取数据: 39 | 40 | ```swift 41 | func showDataForAlbum(albumIndex: Int) { 42 | // defensive code: make sure the requested index is lower than the amount of albums 43 | if (albumIndex < allAlbums.count && albumIndex > -1) { 44 | //fetch the album 45 | let album = allAlbums[albumIndex] 46 | // save the albums data to present it later in the tableview 47 | currentAlbumData = album.ae_tableRepresentation() 48 | } else { 49 | currentAlbumData = nil 50 | } 51 | // we have the data we need, let's refresh our tableview 52 | dataTable!.reloadData() 53 | } 54 | ``` 55 | 56 | `showDataForAlbum()` 这个方法获取最新的专辑数据,当你想要展示新数据的时候,你需要调用 `reloadData()` 这个方法,这样 `UITableView` 就会向委托请求数据,比如有多少个 `section` 有多少个 `row` 之类的。 57 | 58 | 在 `viewDidLoad` 里面调用上面的方法: 59 | 60 | ```swift 61 | self.showDataForAlbum(currentAlbumIndex) 62 | ``` 63 | 64 | 这样应用一启动就会去加载当前的专辑数据。因为 `currentAlbumIndex` 的默认值是 0 ,所以一开始会默认显示第一章专辑的信息。 65 | 66 | 接下来我们该去完善 `DataSource` 的协议方法了。你可以直接把委托方法写在类里面,当然如果你想让你的代码看起来更整洁一点,则可以放在扩展里。 67 | 68 | 在文件底部添加如下方法,注意一定要放在类定义的大括号外面,因为这两个家伙不是类定义的一部分,它们是扩展: 69 | 70 | ```swift 71 | extension ViewController: UITableViewDataSource { 72 | } 73 | 74 | extension ViewController: UITableViewDelegate { 75 | } 76 | ``` 77 | 78 | 上面就是实现委托的方法 - 你可以把协议想象成是与委托之间的约定,只要你实现了约定的方法,就算是实现了委托。在我们的代码中, `ViewController` 需要遵守 `UITableViewDataSource` 和 `UITableViewDelegate` 的协议。这样 `UITableView` 才能确保必要的委托方法都已经实现了。 79 | 80 | 在 `UITableViewDataSource` 对应的那个扩展里加上如下方法: 81 | 82 | ```swift 83 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 84 | if let albumData = currentAlbumData { 85 | return albumData.titles.count 86 | } else { 87 | return 0 88 | } 89 | } 90 | 91 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 92 | let cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell 93 | if let albumData = currentAlbumData { 94 | cell.textLabel?.text = albumData.titles[indexPath.row] 95 | if let detailTextLabel = cell.detailTextLabel { 96 | detailTextLabel.text = albumData.values[indexPath.row] 97 | } 98 | } 99 | return cell 100 | } 101 | ``` 102 | 103 | `tableView(_:numberOfRowsInSection:)` 返回需要展示的行数,和存储的数据中的 title 的数目相同。 104 | 105 | `tableView(_:cellForRowAtIndexPath:)` 创建并且返回了一个单元格,上面有标题和对应的值。 106 | 107 | 注意:你可以把这些方法直接加在类声明里面,也可以放在扩展里,编译器不会去管数据源到底在哪里,只要能找到对应的方法就可以了。而我们之所以这样做,是为了方便其他人阅读。 108 | 109 | 此时再构建项目,你可以看到如下内容: 110 | 111 | ![](../images/decorator4.png) 112 | 113 | 是的,显示成功啦! 114 | 115 | 我们的原计划是在上面的空白处放一个可以横滑浏览专辑的视图。其实仔细想想,这个控件是可以应用在其他地方的,我们不妨把它做成一个可复用的视图。 116 | 117 | 为了让这个视图可以复用,显示内容的工作都只能交给另一个对象来完成:它的委托。这个横滑页面应该声明一些方法让它的委托去实现,就像是 `UITableView` 的 `UITableViewDelegate` 一样。我们将会在下一个设计模式中实现这个功能。 118 | 119 | 完成到这一步的Demo: 120 | 121 | - [查看源码](https://github.com/yourtion/SwiftDesignPatterns-Demo1/tree/Decorator) 122 | - [下载Zip](https://github.com/yourtion/SwiftDesignPatterns-Demo1/archive/Decorator.zip) 123 | 124 | -------------------------------------------------------------------------------- /Chapter06/Use-Decorator-Extension.md: -------------------------------------------------------------------------------- 1 | ## 如何使用扩展 2 | 3 | 想象一下这个场景,我们需要在下面这个列表里展示数据: 4 | 5 | ![](../images/decorator1.png) 6 | 7 | 专辑标题从哪里来? `Album` 本身是个 Model 对象,所以它不应该负责如何展示数据。你需要一些额外的代码添加展示数据的逻辑,但是为了保持 `Model` 的干净,我们不应该直接修改代码,因为这样不符合单一职责原则。 `Model` 层最好就是负责纯粹的数据结构,如果有数据的操作可以放到扩展中完成。 8 | 9 | 接下来我们会创建一个扩展,扩展现有的 `Album` 类,在扩展里定义了新的方法,返回更适合 `UITableView` 展示用的数据结构。 10 | 11 | 数据的结构大概是这样: 12 | 13 | ![](../images/decorator2.png) 14 | 15 | 新建一个 Swift 文件:`AlbumExtensions` ,在里面添加如下扩展: 16 | 17 | ```swift 18 | extension Album { 19 | func ae_tableRepresentation() -> (titles:[String], values:[String]) { 20 | return (["Artist", "Album", "Genre", "Year"], [artist, title, genre, year]) 21 | } 22 | } 23 | ``` 24 | 25 | 在方法的前面有个 `ae_` 前缀,是 `AlbumExtension` 的缩写,这样有利于和类的原有方法进行区分,避免使用的时候产生冲突。现在很多还在维护中的第三方库都已经改成了这个风格。 26 | 27 | 注意:类是可以重写父类方法的,但是在扩展里不可以。扩展里的方法和属性不能和原始类里的方法和属性冲突。 28 | 29 | 思考一下这个设计模式的强大之处: 30 | 31 | - 我们可以直接在扩展里使用 `Album` 里的属性。 32 | - 我们给 `Album` 类添加了内容但是并没有继承它,事实上,使用继承来扩展业务也可以实现一样的功能。 33 | - 这个简单的扩展让我们可以更好地把 `Album` 的数据展示在 `UITableView` 里,而且不用修改源码。 34 | -------------------------------------------------------------------------------- /Chapter07/Adapter.md: -------------------------------------------------------------------------------- 1 | # 适配器模式 - Adapter 2 | 3 | 适配器把自己封装起来然后暴露统一的接口给其他类,这样即使其他类的接口各不相同,也能相安无事,一起工作。 4 | 5 | 如果你熟悉适配器模式,那么你会发现苹果在实现适配器模式的方式稍有不同:苹果通过委托实现了适配器模式。委托相信大家都不陌生。举个例子,如果一个类遵循了 `NSCoying` 的协议,那么它一定要实现 `copy` 方法。 6 | -------------------------------------------------------------------------------- /Chapter07/Use-Adapter.md: -------------------------------------------------------------------------------- 1 | ## 如何使用适配器模式 2 | 3 | 横滑的滚动栏理论上应该是这个样子的: 4 | 5 | ![](../images/adapter1.png) 6 | 7 | 新建一个 Swift 文件:`HorizontalScroller.swift` ,作为我们的横滑滚动控件, `HorizontalScroller` 继承自 `UIView` 。 8 | 9 | 打开 `HorizontalScroller.swift` 文件并添加如下代码: 10 | 11 | ```swift 12 | @objc protocol HorizontalScrollerDelegate { 13 | } 14 | ``` 15 | 16 | 这行代码定义了一个新的协议: `HorizontalScrollerDelegate` 。我们在前面加上了 `@objc` 的标记,这样我们就可以像在 objc 里一样使用 `@optional` 的委托方法了。 17 | 18 | 接下来我们在大括号里定义所有的委托方法,包括必须的和可选的: 19 | 20 | ```swift 21 | // 在横滑视图中有多少页面需要展示 22 | func numberOfViewsForHorizontalScroller(scroller: HorizontalScroller) -> Int 23 | // 展示在第 index 位置显示的 UIView 24 | func horizontalScrollerViewAtIndex(scroller: HorizontalScroller, index:Int) -> UIView 25 | // 通知委托第 index 个视图被点击了 26 | func horizontalScrollerClickedViewAtIndex(scroller: HorizontalScroller, index:Int) 27 | // 可选方法,返回初始化时显示的图片下标,默认是0 28 | optional func initialViewIndex(scroller: HorizontalScroller) -> Int 29 | ``` 30 | 31 | 其中,没有 `option` 标记的方法是必须实现的,一般来说包括那些用来显示的必须数据,比如如何展示数据,有多少数据需要展示,点击事件如何处理等等,不可或缺;有 `option` 标记的方法为可选实现的,相当于是一些辅助设置和功能,就算没有实现也有默认值进行处理。 32 | 33 | 在 `HorizontalScroller` 类里添加一个新的委托对象: 34 | 35 | ```swift 36 | weak var delegate: HorizontalScrollerDelegate? 37 | ``` 38 | 39 | 为了避免循环引用的问题,委托是 `weak` 类型。如果委托是 `strong` 类型的,当前对象持有了委托的强引用,委托又持有了当前对象的强引用,这样谁都无法释放就会导致内存泄露。 40 | 41 | 委托是可选类型,所以很有可能当前类的使用者并没有指定委托。但是如果指定了委托,那么它一定会遵循 `HorizontalScrollerDelegate` 里约定的内容。 42 | 43 | 再添加一些新的属性: 44 | 45 | ```swift 46 | // 1 47 | private let VIEW_PADDING = 10 48 | private let VIEW_DIMENSIONS = 100 49 | private let VIEWS_OFFSET = 100 50 | 51 | // 2 52 | private var scroller : UIScrollView! 53 | // 3 54 | var viewArray = [UIView]() 55 | ``` 56 | 57 | 上面标注的三点分别做了这些事情: 58 | 59 | - 定义一个常量,用来方便的改变布局。现在默认的是显示的内容长宽为100,间隔为10。 60 | - 创建一个 `UIScrollView` 作为容器。 61 | - 创建一个数组用来存放需要展示的数据 62 | 63 | 接下来实现初始化方法: 64 | 65 | ```swift 66 | override init(frame: CGRect) { 67 | super.init(frame: frame) 68 | initializeScrollView() 69 | } 70 | 71 | required init(coder aDecoder: NSCoder) { 72 | super.init(coder: aDecoder) 73 | initializeScrollView() 74 | } 75 | 76 | func initializeScrollView() { 77 | //1 78 | scroller = UIScrollView() 79 | scroller.delegate = self 80 | addSubview(scroller) 81 | 82 | //2 83 | scroller.translatesAutoresizingMaskIntoConstraints = false 84 | 85 | //3 86 | self.addConstraint(NSLayoutConstraint(item: scroller, attribute: .Leading, relatedBy: .Equal, toItem: self, attribute: .Leading, multiplier: 1.0, constant: 0.0)) 87 | self.addConstraint(NSLayoutConstraint(item: scroller, attribute: .Trailing, relatedBy: .Equal, toItem: self, attribute: .Trailing, multiplier: 1.0, constant: 0.0)) 88 | self.addConstraint(NSLayoutConstraint(item: scroller, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1.0, constant: 0.0)) 89 | self.addConstraint(NSLayoutConstraint(item: scroller, attribute: .Bottom, relatedBy: .Equal, toItem: self, attribute: .Bottom, multiplier: 1.0, constant: 0.0)) 90 | 91 | //4 92 | let tapRecognizer = UITapGestureRecognizer(target: self, action:Selector("scrollerTapped:")) 93 | scroller.addGestureRecognizer(tapRecognizer) 94 | } 95 | ``` 96 | 97 | 上面的代码做了如下工作: 98 | 99 | - 创建一个 `UIScrollView` 对象并且把它加到父视图中。 100 | - 关闭 `autoresizing masks` ,从而可以使用 `AutoLayout` 进行布局。 101 | - 给 `scrollview` 添加约束。我们希望 `scrollview` 能填满 `HorizontalScroller` 。 102 | - 创建一个点击事件,检测是否点击到了专辑封面,如果确实点击到了专辑封面,我们需要通知 `HorizontalScroller` 的委托。 103 | 104 | 添加委托方法: 105 | 106 | ```swift 107 | func scrollerTapped(gesture: UITapGestureRecognizer) { 108 | let location = gesture.locationInView(gesture.view) 109 | if let delegate = self.delegate { 110 | for index in 0.. UIView { 132 | return viewArray[index] 133 | } 134 | ``` 135 | 136 | 这个方法很简单,只是用来更方便获取数组里的 `view` 而已。在后面实现高亮选中专辑的时候会用到这个方法。 137 | 138 | 添加如下代码用来重新加载 `scroller` : 139 | 140 | ```swift 141 | func reload() { 142 | // 1 - Check if there is a delegate, if not there is nothing to load. 143 | if let delegate = self.delegate { 144 | //2 - Will keep adding new album views on reload, need to reset. 145 | viewArray = [] 146 | let views: NSArray = scroller.subviews 147 | 148 | // 3 - remove all subviews 149 | views.enumerateObjectsUsingBlock { 150 | (object: AnyObject!, idx: Int, stop: UnsafeMutablePointer) -> Void in 151 | object.removeFromSuperview() 152 | } 153 | // 4 - xValue is the starting point of the views inside the scroller 154 | var xValue = VIEWS_OFFSET 155 | for index in 0.. (Int) { 280 | return allAlbums.count 281 | } 282 | ``` 283 | 284 | 这个委托方法返回 `scroll vew` 里面的视图数量,因为是用来展示所有的专辑的封面,所以数目也就是专辑数目。 285 | 286 | 然后添加如下代码: 287 | 288 | ```swift 289 | func horizontalScrollerViewAtIndex(scroller: HorizontalScroller, index: Int) -> (UIView) { 290 | let album = allAlbums[index] 291 | let albumView = AlbumView(frame: CGRectMake(0, 0, 100, 100), albumCover: album.coverUrl) 292 | if currentAlbumIndex == index { 293 | albumView.highlightAlbum(didHighlightView: true) 294 | } else { 295 | albumView.highlightAlbum(didHighlightView: false) 296 | } 297 | return albumView 298 | } 299 | ``` 300 | 301 | 我们创建了一个新的 `AlbumView` ,然后检查一下是不是当前选中的专辑,如果是则设为高亮,最后返回结果。 302 | 303 | 是的就是这么简单!三个方法,完成了一个横向滚动的浏览视图。 304 | 305 | 我们还需要创建这个滚动视图并把它加到主视图里,但是在这之前,先添加如下方法: 306 | 307 | ```swift 308 | func reloadScroller() { 309 | allAlbums = LibraryAPI.sharedInstance.getAlbums() 310 | if currentAlbumIndex < 0 { 311 | currentAlbumIndex = 0 312 | } else if currentAlbumIndex >= allAlbums.count { 313 | currentAlbumIndex = allAlbums.count - 1 314 | } 315 | scroller.reload() 316 | showDataForAlbum(currentAlbumIndex) 317 | } 318 | ``` 319 | 320 | 这个方法通过 `LibraryAPI` 加载专辑数据,然后根据 `currentAlbumIndex` 的值设置当前视图。在设置之前先进行了校正,如果小于0则设置第一个专辑为展示的视图,如果超出了范围则设置最后一个专辑为展示的视图。 321 | 322 | 接下来只需要指定委托就可以了,在 `viewDidLoad` 最后加入一下代码: 323 | 324 | ```swift 325 | scroller.delegate = self 326 | reloadScroller() 327 | ``` 328 | 329 | 因为 `HorizontalScroller` 是在 `StoryBoard` 里初始化的,所以我们需要做的只是指定委托,然后调用 `reloadScroller()` 方法,从而加载所有的子视图并且展示专辑数据。 330 | 331 | 标注:如果协议里的方法过多,可以考虑把它分解成几个更小的协议。`UITableViewDelegate` 和 `UITableViewDataSource` 就是很好的例子,它们都是 `UITableView` 的协议。尝试去设计你自己的协议,让每个协议都单独负责一部分功能。 332 | 333 | 运行一下当前项目,看一下我们的新页面: 334 | 335 | ![](../images/adapter4.png) 336 | 337 | 等下,滚动视图显示出来了,但是专辑的封面怎么不见了? 338 | 339 | 啊哈,是的。我们还没完成下载部分的代码,我们需要添加下载图片的方法。因为我们所有的访问都是通过 `LibraryAPI` 实现的,所以很显然我们下一步应该去完善这个类了。不过在这之前,我们还需要考虑一些问题: 340 | 341 | - `AlbumView` 不应该直接和 `LibraryAPI` 交互,我们不应该把视图的逻辑和业务逻辑混在一起。 342 | - 同样, `LibraryAPI` 也不应该知道 `AlbumView` 这个类。 343 | - 如果 `AlbumView` 要展示封面,`LibraryAPI` 需要告诉 `AlbumView` 图片下载完成。 344 | 345 | 看起来好像很难的样子?别绝望,接下来我们会用观察者模式 (`Observer Pattern`) 解决这个问题!:] 346 | 347 | 完成到这一步的Demo: 348 | 349 | - [查看源码](https://github.com/yourtion/SwiftDesignPatterns-Demo1/tree/Adapter) 350 | - [下载Zip](https://github.com/yourtion/SwiftDesignPatterns-Demo1/archive/Adapter.zip) 351 | -------------------------------------------------------------------------------- /Chapter08/KVO.md: -------------------------------------------------------------------------------- 1 | ## 键值观察 - KVO 2 | 3 | 在 KVO 里,对象可以注册监听任何属性的变化,不管它是否持有。如果感兴趣的话,可以读一读[苹果 KVO 编程指南](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html)。 4 | 5 | ### 如何使用 KVO 6 | 7 | 正如前面所提及的, 对象可以关注任何属性的变化。在我们的例子里,我们可以用 KVO 关注 `UIImageView` 的 `image` 属性变化。 8 | 9 | 打开 `AlbumView.swift` 文件,找到 `init(frame:albumCover:)` 方法,在把 `coverImage` 添加到 `subView` 的代码后面添加如下代码: 10 | 11 | ```swift 12 | coverImage.addObserver(self, forKeyPath: "image", options: NSKeyValueObservingOptions([.New, .Old]), context: nil) 13 | ``` 14 | 15 | 这行代码把 `self` (也就是当前类) 添加到了 `coverImage` 的 `image` 属性的观察者里。 16 | 17 | 在销毁的时候,我们也需要取消观察。还是在 `AlbumView.swift` 文件里,添加如下代码: 18 | 19 | ```swift 20 | deinit { 21 | coverImage.removeObserver(self, forKeyPath: "image") 22 | } 23 | ``` 24 | 25 | 最终添加如下方法: 26 | 27 | ```swift 28 | override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer) { 29 | if keyPath == "image" { 30 | indicator.stopAnimating() 31 | } 32 | } 33 | ``` 34 | 35 | 必须在所有的观察者里实现上面的代码。在检测到属性变化的时候,系统会自动调用这个方法。在上面的代码里,我们在图片加载完成的时候把那个提示加载的小菊花去掉了。 36 | 37 | 再次运行项目,你会发现一切正常了: 38 | 39 | ![](../images/kvo1.png) 40 | 41 | 注意:一定要记得移除观察者,否则如果对象已经销毁了还给它发送消息会导致应用崩溃。 42 | 43 | 此时你可以把玩一下当前的应用然后再关掉它,你会发现你的应用的状态并没有存储下来。最后看见的专辑并不会再下次打开应用的时候出现。 44 | 45 | 为了解决这个问题,我们可以使用下一种模式:备忘录模式。 46 | 47 | 48 | 完成到这一步的Demo: 49 | 50 | - [查看源码](https://github.com/yourtion/SwiftDesignPatterns-Demo1/tree/KVO) 51 | - [下载Zip](https://github.com/yourtion/SwiftDesignPatterns-Demo1/archive/KVO.zip) 52 | -------------------------------------------------------------------------------- /Chapter08/Notification.md: -------------------------------------------------------------------------------- 1 | ## 通知 - Notification 2 | 3 | 不要把这里的通知和推送通知或者本地通知搞混了,这里的通知是基于订阅-发布模型的,即一个对象 (发布者) 向其他对象 (订阅者) 发送消息。发布者永远不需要知道订阅者的任何数据。 4 | 5 | Apple 对于通知的使用很频繁,比如当键盘弹出或者收起的时候,系统会分别发送 `UIKeyboardWillShowNotification/UIKeyboardWillHideNotification` 的通知。当你的应用切到后台的时候,又会发送 `UIApplicationDidEnterBackgroundNotification` 的通知。 6 | 7 | 注意:打开 `UIApplication.swift` 文件,在文件结尾你会看到二十多种系统发送的通知。 8 | 9 | ### 如何使用通知 10 | 11 | 打开 `AlbumView.swift` 然后在 `init` 的最后插入如下代码: 12 | 13 | ```swift 14 | NSNotificationCenter.defaultCenter().postNotificationName("BLDownloadImageNotification", object: self, userInfo: ["imageView":coverImage, "coverUrl" : albumCover]) 15 | ``` 16 | 17 | 这行代码通过 `NSNotificationCenter` 发送了一个通知,通知信息包含了 `UIImageView` 和图片的下载地址。这是下载图像需要的所有数据。 18 | 19 | 然后在 `LibraryAPI.swift` 的 `init` 方法的 `super.init()` 后面加上如下代码: 20 | 21 | ```swift 22 | NSNotificationCenter.defaultCenter().addObserver(self, selector:"downloadImage:", name: "BLDownloadImageNotification", object: nil) 23 | ``` 24 | 25 | 这是等号的另一边:观察者。每当 `AlbumView` 发出一个 `BLDownloadImageNotification` 通知的时候,由于 `LibraryAPI` 已经注册了成为观察者,所以系统会调用 `downloadImage()` 方法。 26 | 27 | 但是,在实现 `downloadImage()` 之前,我们必须先在 `dealloc` 里取消监听。如果没有取消监听消息,消息会发送给一个已经销毁的对象,导致程序崩溃。 28 | 29 | 在 `LibaratyAPI.swift` 里加上取消订阅的代码: 30 | 31 | ```swift 32 | deinit { 33 | NSNotificationCenter.defaultCenter().removeObserver(self) 34 | } 35 | ``` 36 | 37 | 当对象销毁的时候,把它从所有消息的订阅列表里去除。 38 | 39 | 这里还要做一件事情:我们最好把图片存储到本地,这样可以避免一次又一次下载相同的封面。 40 | 41 | 打开 `PersistencyManager.swift` 添加如下代码: 42 | 43 | ```swift 44 | func saveImage(image: UIImage, filename: String) { 45 | let path = NSHomeDirectory().stringByAppendingString("/Documents/\(filename)") 46 | let data = UIImagePNGRepresentation(image) 47 | data.writeToFile(path, atomically: true) 48 | } 49 | 50 | func getImage(filename: String) -> UIImage? { 51 | let path = NSHomeDirectory().stringByAppendingString("/Documents/\(filename)") 52 | do { 53 | let data = try NSData(contentsOfFile: path, options: .UncachedRead) 54 | return UIImage(data: data) 55 | } catch _ { 56 | return nil 57 | } 58 | } 59 | ``` 60 | 61 | 代码很简单直接,下载的图片会存储在 `Documents` 目录下,如果没有检查到缓存文件, `getImage()` 方法则会返回 `nil` 。 62 | 63 | 然后在 `LibraryAPI.swift` 添加如下代码: 64 | 65 | ```swift 66 | func downloadImage(notification: NSNotification) { 67 | //1 68 | let userInfo = notification.userInfo as! [String: AnyObject] 69 | let imageView = userInfo["imageView"] as! UIImageView? 70 | let coverUrl = userInfo["coverUrl"] as! NSString 71 | 72 | //2 73 | if let imageViewUnWrapped = imageView { 74 | imageViewUnWrapped.image = persistencyManager.getImage(coverUrl.lastPathComponent) 75 | if imageViewUnWrapped.image == nil { 76 | //3 77 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in 78 | let downloadedImage = self.httpClient.downloadImage(coverUrl as String) 79 | //4 80 | dispatch_sync(dispatch_get_main_queue(), { () -> Void in 81 | imageViewUnWrapped.image = downloadedImage 82 | self.persistencyManager.saveImage(downloadedImage, filename: coverUrl.lastPathComponent) 83 | }) 84 | }) 85 | } 86 | } 87 | } 88 | ``` 89 | 90 | 拆解一下上面的代码: 91 | 92 | - `downloadImage` 通过通知调用,所以这个方法的参数就是 `NSNotification` 本身。 `UIImageView` 和 `URL` 都可以从其中获取到。 93 | - 如果以前下载过,从 `PersistencyManager` 里获取缓存。 94 | - 如果图片没有缓存,则通过 `HTTPClient` 获取。 95 | - 如果下载完成,展示图片并用 `PersistencyManager` 存储到本地。 96 | 97 | 再回顾一下,我们使用外观模式隐藏了下载图片的复杂程度。通知的发送者并不在乎图片是如何从网上下载到本地的。 98 | 99 | 如果你是 `Xcode 7` 和 iOS9 那么运行项目,程序会崩溃同时看到控制台有如下输出: 100 | 101 | App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file. 102 | 103 | 解决方法是如下(参考:[解决iOS9下blocked cleartext HTTP](http://blog.yourtion.com/ios9-http-blocked.html)) 104 | 105 | 修改项目的 Info.plist 文件,增加以下内容: 106 | 107 | ```xml 108 | NSAppTransportSecurity 109 | 110 | NSAllowsArbitraryLoads 111 | 112 | 113 | ``` 114 | 115 | 现在运行一下项目,可以看到专辑封面已经显示出来了: 116 | 117 | ![](../images/notifications1.png) 118 | 119 | 关了应用再重新运行,注意这次没有任何延时就显示了所有的图片,因为我们已经有了本地缓存。我们甚至可以在没有网络的情况下正常使用我们的应用。不过出了问题:这个用来提示加载网络请求的小菊花怎么一直在显示! 120 | 121 | 我们在下载图片的时候开启了这个白色小菊花,但是在图片下载完毕的时候我们并没有停掉它。我们可以在每次下载成功的时候发送一个通知,但是我们不这样做,这次我们来用用另一个观察者模式: `KVO` 。 122 | 123 | 完成到这一步的Demo: 124 | 125 | - [查看源码](https://github.com/yourtion/SwiftDesignPatterns-Demo1/tree/Notification) 126 | - [下载Zip](https://github.com/yourtion/SwiftDesignPatterns-Demo1/archive/Notification.zip) 127 | 128 | -------------------------------------------------------------------------------- /Chapter08/Observer.md: -------------------------------------------------------------------------------- 1 | # 观察者模式 - Observer 2 | 3 | 在观察者模式里,一个对象在状态变化的时候会通知另一个对象。参与者并不需要知道其他对象的具体是干什么的 - 这是一种降低耦合度的设计。这个设计模式常用于在某个属性改变的时候通知关注该属性的对象。 4 | 5 | 常见的使用方法是观察者注册监听,然后再状态改变的时候,所有观察者们都会收到通知。 6 | 7 | 在 MVC 里,观察者模式意味着需要允许 `Model` 对象和 `View` 对象进行交流,而不能有直接的关联。 8 | 9 | `Cocoa` 使用两种方式实现了观察者模式: `Notification` 和 `Key-Value Observing (KVO)`。 10 | 11 | -------------------------------------------------------------------------------- /Chapter09/Archiving.md: -------------------------------------------------------------------------------- 1 | ## 归档 - Archiving 2 | 3 | 苹果通过归档的方法来实现备忘录模式。它把对象转化成了流然后在不暴露内部属性的情况下存储数据。你可以读一读 《iOS 6 by Tutorials》 这本书的第 16 章,或者看下苹果的[归档和序列化文档](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Archiving/Archiving.html)。 4 | 5 | ### 如何使用归档 6 | 7 | 首先,我们需要让 `Album` 实现 `NSCoding` 协议,声明这个类是可被归档的。打开 `Album.swift` 在 `class` 那行后面加上 `NSCoding` : 8 | 9 | ```swift 10 | class Album: NSObject, NSCoding { 11 | ``` 12 | 13 | 然后添加如下的两个方法: 14 | 15 | ```swift 16 | required init(coder decoder: NSCoder) { 17 | super.init() 18 | self.title = decoder.decodeObjectForKey("title") as! String 19 | self.artist = decoder.decodeObjectForKey("artist")as! String 20 | self.genre = decoder.decodeObjectForKey("genre") as! String? 21 | self.coverUrl = decoder.decodeObjectForKey("cover_url")as! String 22 | self.year = decoder.decodeObjectForKey("year") as! String 23 | } 24 | 25 | func encodeWithCoder(aCoder: NSCoder) { 26 | aCoder.encodeObject(title, forKey: "title") 27 | aCoder.encodeObject(artist, forKey: "artist") 28 | aCoder.encodeObject(genre, forKey: "genre") 29 | aCoder.encodeObject(coverUrl, forKey: "cover_url") 30 | aCoder.encodeObject(year, forKey: "year") 31 | } 32 | ``` 33 | 34 | `encodeWithCoder` 方法是 `NSCoding` 的一部分,在被归档的时候调用。相对的, `init(coder:)` 方法则是用来解档的。很简单,很强大。 35 | 36 | 现在 `Album` 对象可以被归档了,添加一些代码来存储和加载 `Album` 数据。 37 | 38 | 在 `PersistencyManager.swift` 里添加如下代码: 39 | 40 | 41 | ```swift 42 | func saveAlbums() { 43 | let filename = NSHomeDirectory().stringByAppendingString("/Documents/albums.bin") 44 | let data = NSKeyedArchiver.archivedDataWithRootObject(albums) 45 | data.writeToFile(filename, atomically: true) 46 | } 47 | ``` 48 | 49 | 这个方法可以用来存储专辑。 `NSKeyedArchiver` 把专辑数组归档到了 `albums.bin` 这个文件里。 50 | 51 | 当我们归档一个包含子对象的对象时,系统会自动递归的归档子对象,然后是子对象的子对象,这样一层层递归下去。在我们的例子里,我们归档的是 `albums` 因为 `Array` 和 `Album` 都是实现 `NSCopying` 接口的,所以数组里的对象都可以自动归档。 52 | 53 | 用下面的代码取代 `PersistencyManager` 中的 `init` 方法: 54 | 55 | ```swift 56 | override init() { 57 | super.init() 58 | if let data = NSData(contentsOfFile: NSHomeDirectory().stringByAppendingString("/Documents/albums.bin")) { 59 | let unarchiveAlbums = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! [Album] 60 | if let unwrappedAlbum : [Album] = unarchiveAlbums { 61 | albums = unwrappedAlbum 62 | } 63 | } else { 64 | createPlaceholderAlbum() 65 | } 66 | } 67 | 68 | func createPlaceholderAlbum() { 69 | //Dummy list of albums 70 | let album1 = Album(title: "Best of Bowie", 71 | artist: "David Bowie", 72 | genre: "Pop", 73 | coverUrl: "http://www.coversproject.com/static/thumbs/album/album_david%20bowie_best%20of%20bowie.png", 74 | year: "1992") 75 | 76 | let album2 = Album(title: "It's My Life", 77 | artist: "No Doubt", 78 | genre: "Pop", 79 | coverUrl: "http://www.coversproject.com/static/thumbs/album/album_no%20doubt_its%20my%20life%20%20bathwater.png", 80 | year: "2003") 81 | 82 | let album3 = Album(title: "Nothing Like The Sun", 83 | artist: "Sting", 84 | genre: "Pop", 85 | coverUrl: "http://www.coversproject.com/static/thumbs/album/album_sting_nothing%20like%20the%20sun.png", 86 | year: "1999") 87 | 88 | let album4 = Album(title: "Staring at the Sun", 89 | artist: "U2", 90 | genre: "Pop", 91 | coverUrl: "http://www.coversproject.com/static/thumbs/album/album_u2_staring%20at%20the%20sun.png", 92 | year: "2000") 93 | 94 | let album5 = Album(title: "American Pie", 95 | artist: "Madonna", 96 | genre: "Pop", 97 | coverUrl: "http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png", 98 | year: "2000") 99 | albums = [album1, album2, album3, album4, album5] 100 | saveAlbums() 101 | } 102 | ``` 103 | 104 | 我们把创建专辑数据的方法放到了 `createPlaceholderAlbum` 里,这样代码可读性更高。在新的代码里,如果存在归档文件, `NSKeyedUnarchiver` 从归档文件加载数据;否则就创建归档文件,这样下次程序启动的时候可以读取本地文件加载数据。 105 | 106 | 我们还想在每次程序进入后台的时候存储专辑数据。看起来现在这个功能并不是必须的,但是如果以后我们加了编辑功能,这样做还是很有必要的,那时我们肯定希望确保新的数据会同步到本地的归档文件。 107 | 108 | 因为我们的程序通过 `LibraryAPI` 来访问所有服务,所以我们需要通过 `LibraryAPI` 来通知 `PersistencyManager` 存储专辑数据。 109 | 110 | 在 `LibraryAPI` 里添加存储专辑数据的方法: 111 | 112 | ```swift 113 | func saveAlbums() { 114 | persistencyManager.saveAlbums() 115 | } 116 | ``` 117 | 118 | 这个方法很简单,就是把 `LibraryAPI` 的 `saveAlbums` 方法传递给了 `persistencyManager` 的 `saveAlbums` 方法。 119 | 120 | 然后在 `ViewController.swift` 的 `saveCurrentState` 方法的最后加上: 121 | 122 | ```swift 123 | LibraryAPI.sharedInstance.saveAlbums() 124 | ``` 125 | 126 | 在 `ViewController` 需要存储状态的时候,上面的代码通过 `LibraryAPI` 归档当前的专辑数据。 127 | 128 | 运行一下程序,检查一下没有编译错误。 129 | 130 | 不幸的是似乎没什么简单的方法来检查归档是否正确完成。你可以检查一下 `Documents` 目录,看下是否存在归档文件。如果要查看其他数据变化的话,还需要添加编辑专辑数据的功能。 131 | 132 | 不过和编辑数据相比,似乎加个删除专辑的功能更好一点,如果不想要这张专辑直接删除即可。再进一步,万一误删了话,是不是还可以再加个撤销按钮? 133 | 134 | 135 | 完成到这一步的Demo: 136 | 137 | - [查看源码](https://github.com/yourtion/SwiftDesignPatterns-Demo1/tree/Archiving) 138 | - [下载Zip](https://github.com/yourtion/SwiftDesignPatterns-Demo1/archive/Archiving.zip) 139 | -------------------------------------------------------------------------------- /Chapter09/Memento.md: -------------------------------------------------------------------------------- 1 | # 备忘录模式 - Memento 2 | 3 | 备忘录模式捕捉并且具象化一个对象的内在状态。换句话说,它把你的对象存在了某个地方,然后在以后的某个时间再把它恢复出来,而不会打破它本身的封装性,私有数据依旧是私有数据。 4 | -------------------------------------------------------------------------------- /Chapter09/Use-Memento.md: -------------------------------------------------------------------------------- 1 | ## 如何使用备忘录模式 2 | 3 | 在 `ViewController.swift` 里加上下面两个方法: 4 | 5 | ```swift 6 | //MARK: Memento Pattern 7 | func saveCurrentState() { 8 | // When the user leaves the app and then comes back again, he wants it to be in the exact same state 9 | // he left it. In order to do this we need to save the currently displayed album. 10 | // Since it's only one piece of information we can use NSUserDefaults. 11 | NSUserDefaults.standardUserDefaults().setInteger(currentAlbumIndex, forKey: "currentAlbumIndex") 12 | } 13 | 14 | func loadPreviousState() { 15 | currentAlbumIndex = NSUserDefaults.standardUserDefaults().integerForKey("currentAlbumIndex") 16 | showDataForAlbum(currentAlbumIndex) 17 | } 18 | ``` 19 | 20 | `saveCurrentState` 把当前相册的索引值存到 `NSUserDefaults` 里。`NSUserDefaults` 是 iOS 提供的一个标准存储方案,用于保存应用的配置信息和数据。 21 | 22 | `loadPreviousState` 方法加载上次存储的索引值。这并不是备忘录模式的完整实现,但是已经离目标不远了。 23 | 24 | 接下来在 `viewDidLoad` 的 `scroller.delegate = self` 前面调用: 25 | 26 | ```swift 27 | loadPreviousState() 28 | ``` 29 | 30 | 这样在刚初始化的时候就加载了上次存储的状态。但是什么时候存储当前状态呢?这个时候我们可以用通知来做。在应用进入到后台的时候, iOS 会发送一个 `UIApplicationDidEnterBackgroundNotification` 的通知,我们可以在这个通知里调用 `saveCurrentState` 这个方法。是不是很方便? 31 | 32 | 在 `viewDidLoad` 的最后加上如下代码: 33 | 34 | ```swift 35 | NSNotificationCenter.defaultCenter().addObserver(self, selector:"saveCurrentState", name: UIApplicationDidEnterBackgroundNotification, object: nil) 36 | ``` 37 | 38 | 现在,当应用即将进入后台的时候,`ViewController` 会调用 `saveCurrentState` 方法自动存储当前状态。 39 | 40 | 当然也别忘了取消监听通知,添加如下代码: 41 | 42 | ```swift 43 | deinit { 44 | NSNotificationCenter.defaultCenter().removeObserver(self) 45 | } 46 | ``` 47 | 48 | 这样就确保在 `ViewController` 销毁的时候取消监听通知。 49 | 50 | 这时再运行程序,随意移到某个专辑上,然后按下 Home 键把应用切换到后台,再在 Xcode 上把 App 关闭。重新启动,会看见上次记录的专辑已经存了下来并成功还原了: 51 | 52 | ![](../images/memento1.png) 53 | 54 | 看起来专辑数据好像是对了,但是上面的滚动条似乎出了问题,没有居中啊! 55 | 56 | 这时 `initialViewIndex` 方法就派上用场了。由于在委托里 (也就是 `ViewController` ) 还没实现这个方法,所以初始化的结果总是第一张专辑。 57 | 58 | 为了修复这个问题,我们可以在 `ViewController.swift` 里添加如下代码: 59 | 60 | ```swift 61 | func initialViewIndex(scroller: HorizontalScroller) -> Int { 62 | return currentAlbumIndex 63 | } 64 | ``` 65 | 66 | 现在 `HorizontalScroller` 可以根据 `currentAlbumIndex` 自动滑到相应的索引位置了。 67 | 68 | 再次重复上次的步骤,切到后台,关闭应用,重启,一切顺利: 69 | 70 | ![](../images/memento2.png) 71 | 72 | 回头看看 `PersistencyManager` 的 `init` 方法,你会发现专辑数据是我们硬编码写进去的,而且每次创建 `PersistencyManager` 的时候都会再创建一次专辑数据。而实际上一个比较好的方案是只创建一次,然后把专辑数据存到本地文件里。我们如何把专辑数据存到文件里呢? 73 | 74 | 一种方案是遍历 Album 的属性然后把它们写到一个 `plist` 文件里,然后如果需要的时候再重新创建 `Album` 对象。这并不是最好的选择,因为数据和属性不同,你的代码也就要相应的产生变化。举个例子,如果我们以后想添加 `Movie` 对象,它有着完全不同的属性,那么存储和读取数据又需要重写新的代码。 75 | 76 | 况且你也无法存储这些对象的私有属性,因为其他类是没有访问权限的。这也就是为什么 Apple 提供了 归档 的机制。 77 | 78 | 完成到这一步的Demo: 79 | 80 | - [查看源码](https://github.com/yourtion/SwiftDesignPatterns-Demo1/tree/Memento) 81 | - [下载Zip](https://github.com/yourtion/SwiftDesignPatterns-Demo1/archive/Memento.zip) 82 | 83 | -------------------------------------------------------------------------------- /Chapter10/FinalTouches.md: -------------------------------------------------------------------------------- 1 | # 最后的润色 2 | 3 | 现在我们将添加最后一个功能:允许用户删除专辑,以及撤销上次的删除操作。 4 | 5 | 在 `ViewController` 里添加如下属性: 6 | 7 | ```swift 8 | // 为了实现撤销功能,我们用数组作为一个栈来 push 和 pop 用户的操作 9 | var undoStack: [(Album, Int)] = [] 10 | ``` 11 | 12 | 然后在 `viewDidLoad` 的 `reloadScroller()` 后面添加如下代码: 13 | 14 | ```swift 15 | let undoButton = UIBarButtonItem(barButtonSystemItem: .Undo, target: self, action:"undoAction") 16 | undoButton.enabled = false; 17 | let space = UIBarButtonItem(barButtonSystemItem: .FlexibleSpace, target:nil, action:nil) 18 | let trashButton = UIBarButtonItem(barButtonSystemItem: .Trash, target:self, action:"deleteAlbum") 19 | let toolbarButtonItems = [undoButton, space, trashButton] 20 | toolbar.setItems(toolbarButtonItems, animated: true) 21 | ``` 22 | 23 | 上面的代码创建了一个 `toolbar` ,上面有两个按钮,在 `undoStack` 为空的情况下, `undo` 的按钮是不可用的。注意 `toolbar` 已经在 `storyboard` 里了,我们需要做的只是配置上面的按钮。 24 | 25 | 我们需要在 `ViewController.swift` 里添加三个方法,用来处理专辑的编辑事件:增加,删除,撤销。 26 | 27 | 先写添加的方法: 28 | 29 | ```swift 30 | func addAlbumAtIndex(album: Album,index: Int) { 31 | LibraryAPI.sharedInstance.addAlbum(album, index: index) 32 | currentAlbumIndex = index 33 | reloadScroller() 34 | } 35 | ``` 36 | 37 | 做了三件事:添加专辑,设为当前的索引,重新加载滚动条。 38 | 39 | 接下来是删除方法: 40 | 41 | ```swift 42 | func deleteAlbum() { 43 | //1 44 | let deletedAlbum : Album = allAlbums[currentAlbumIndex] 45 | //2 46 | let undoAction = (deletedAlbum, currentAlbumIndex) 47 | undoStack.insert(undoAction, atIndex: 0) 48 | //3 49 | LibraryAPI.sharedInstance.deleteAlbum(currentAlbumIndex) 50 | reloadScroller() 51 | //4 52 | let barButtonItems = toolbar.items! as [UIBarButtonItem] 53 | let undoButton : UIBarButtonItem = barButtonItems[0] 54 | undoButton.enabled = true 55 | //5 56 | if (allAlbums.count == 0) { 57 | let trashButton : UIBarButtonItem = barButtonItems[2] 58 | trashButton.enabled = false 59 | } 60 | } 61 | ``` 62 | 63 | 挨个看一下各个部分: 64 | 65 | - 获取要删除的专辑。 66 | - 创建一个 `undoAction` 对象,用元组存储 `Album` 对象和它的索引值。然后把这个元组加到了栈里。 67 | - 使用 `LibraryAPI` 删除专辑数据,然后重新加载滚动条。 68 | - 既然撤销栈里已经有了数据,那么我们需要设置撤销按钮为可用。 69 | - 检查一下是不是还剩专辑,如果没有专辑了那就设置删除按钮为不可用。 70 | 71 | 最后添加撤销按钮: 72 | 73 | ```swift 74 | func undoAction() { 75 | let barButtonItems = toolbar.items! as [UIBarButtonItem] 76 | //1 77 | if undoStack.count > 0 { 78 | let (deletedAlbum, index) = undoStack.removeAtIndex(0) 79 | addAlbumAtIndex(deletedAlbum, index: index) 80 | } 81 | //2 82 | if undoStack.count == 0 { 83 | let undoButton : UIBarButtonItem = barButtonItems[0] 84 | undoButton.enabled = false 85 | } 86 | //3 87 | let trashButton : UIBarButtonItem = barButtonItems[2] 88 | trashButton.enabled = true 89 | } 90 | ``` 91 | 92 | 照着备注的三个步骤再看一下撤销方法里的代码: 93 | 94 | - 首先从栈里 `pop` 出一个对象,这个对象就是我们当初塞进去的元祖,存有删除的 `Album` 对象和它的索引位置。然后我们把取出来的对象放回了数据源里。 95 | - 因为我们从栈里删除了一个对象,所以需要检查一下看看栈是不是空了。如果空了那就设置撤销按钮不可用。 96 | - 既然我们已经撤消了一个专辑,那删除按钮肯定是可用的。所以把它设置为 `enabled` 。 97 | 98 | 这时再运行应用,试试删除和插销功能,似乎一切正常了: 99 | 100 | ![](../images/swiftDesignPattern3.png) 101 | 102 | 我们也可以趁机测试一下,看看是否及时存储了专辑数据的变化。比如删除一个专辑,然后切到后台,强关应用,再重新开启,看看是不是删除操作成功保存了。 103 | 104 | 如果想要恢复所有数据,删除应用然后重新安装即可。 105 | 106 | 完成到这一步的Demo: 107 | 108 | - [查看源码](https://github.com/yourtion/SwiftDesignPatterns-Demo1/tree/FinalTouches) 109 | - [下载Zip](https://github.com/yourtion/SwiftDesignPatterns-Demo1/archive/FinalTouches.zip) 110 | -------------------------------------------------------------------------------- /Chapter11/Final.md: -------------------------------------------------------------------------------- 1 | # 小结 2 | 3 | 最终项目的源代码可以在 [BlueLibrarySwift-Final](https://github.com/yourtion/SwiftDesignPatterns-Demo1/archive/Final.zip) 下载。 4 | 5 | 通过这两篇设计模式的学习,我们接触到了一些基础的设计模式和概念:`Singleton` 、`MVC` 、`Delegation` 、`Protocols` 、`Facade` 、`Observer` 、`Memento` 。 6 | 7 | 这篇文章的目的,并不是推崇每行代码都要用设计模式,而是希望大家在考虑一些问题的时候,可以参考设计模式提出一些合理的解决方案,尤其是应用开发的起始阶段,思考和设计尤为重要。 8 | 9 | 如果想继续深入学习设计模式,推荐设计模式的经典书籍:[Design Patterns: Elements of Reusable Object-Oriented Software](http://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612/)。 10 | 11 | 如果想看更多的设计模式相关的代码,推荐这个神奇的项目: [Swift 实现的种种设计模式](https://github.com/ochococo/Design-Patterns-In-Swift)。 12 | 13 | 接下来你可以看看这篇:[Swift 设计模式中级指南](http://www.raywenderlich.com/86053/intermediate-design-patterns-in-swift),学习更多的设计模式。 14 | 15 | 玩的开心。 :] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 郭宇翔 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift设计模式 (iOS) 2 | 3 | 我们将会通过完成一个完整的应用,展示音乐专辑和专辑的相关信息来学习设计模式在 Swift 中的实现。 4 | 5 | 通过这个应用,我们会接触一些 Cocoa 中常见的设计模式: 6 | 7 | - 创建型 (Creational):单例模式 (Singleton) 8 | - 结构型 (Structural):MVC、装饰者模式 (Decorator)、适配器模式 (Adapter)、外观模式 (Facade) 9 | - 行为型 (Behavioral):观察者模式 (Observer)、备忘录模式 (Memento) 10 | 11 | ## 整理排版说明 12 | 13 | 在 `Xcode 7` 中进行编码测试,升级为 `Swift 2.0` 解决原文中出现的问题,保证了语句与Demo的可用性 14 | 15 | 在线阅读: http://swift-design-patterns.books.yourtion.com/ 16 | 17 | 下载电子书: https://www.gitbook.com/book/yourtion/swiftdesignpatterns/details 18 | 19 | 直接下载:[PDF](https://www.gitbook.com/download/pdf/book/yourtion/swiftdesignpatterns)、[EPub](https://www.gitbook.com/download/epub/book/yourtion/swiftdesignpatterns)、[Mobi](https://www.gitbook.com/download/mobi/book/yourtion/swiftdesignpatterns) 20 | 21 | **有修改建议优化请[提交Issus](https://github.com/yourtion/SwiftDesignPatterns/issues/new),或请直接Fork: 进行修改并申请 Pull Request。** 22 | 23 | 项目Demo:https://github.com/yourtion/SwiftDesignPatterns-Demo1 24 | 25 | ## 更新声明 26 | 27 | 本书整理排版自: 28 | 29 | - [iOS 中的设计模式 (Swift版本) Part 1](http://blog.callmewhy.com/2014/12/29/introducing-ios-design-patterns-in-swift-part-1/) 30 | - [iOS 中的设计模式 (Swift版本) Part 2](http://blog.callmewhy.com/2015/03/01/introducing-ios-design-patterns-in-swift-part-2/) 31 | 32 | 原文翻译自 [Introducing iOS Design Patterns in Swift – Part 1/2](http://www.raywenderlich.com/86477/introducing-ios-design-patterns-in-swift-part-1) 和 [Introducing iOS Design Patterns in Swift – Part 2/2](http://www.raywenderlich.com/90773/introducing-ios-design-patterns-in-swift-part-2) ,本教程 [objc](http://www.raywenderlich.com/46988/ios-design-patterns) 版本的作者是 Eli Ganem ,由 Vincent Ngo 更新为 Swift 版本。 33 | 34 | Update 04/22/2015: Updated for Xcode 6.3 and Swift 1.2. 35 | 36 | Update note: This tutorial was updated for iOS 8 and Swift by Vincent Ngo. Original post by Tutorial team member Eli Ganem. 37 | 38 | ## GitBook 排版 39 | 40 | **Yourtion** 41 | - yourtion@gmail.com 42 | - https://github.com/yourtion 43 | 44 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | * [iOS 设计模式](./Chapter01/iOS-Design-Patterns.md) 2 | * [入门 - 开始](./Chapter02/GettingStarted.md) 3 | * [设计模式之王 - MVC](./Chapter03/MVC.md) 4 | * [如何使用 MVC 模式](./Chapter03/Use-MVC.md) 5 | * [单例模式 - Singleton](./Chapter04/Singleton.md) 6 | * [如何使用单例模式](./Chapter04/Use-Singleton.md) 7 | * [外观模式 - Facade](./Chapter05/Facade.md) 8 | * [如何使用外观模式](./Chapter05/Use-Facade.md) 9 | * [装饰者模式 - Decorator](./Chapter06/Decorator.md) 10 | * [扩展](./Chapter06/Decorator-Extension.md) 11 | * [如何使用扩展](./Chapter06/Use-Decorator-Extension.md) 12 | * [委托](./Chapter06/Decorator-Delegation.md) 13 | * [如何使用委托模式](./Chapter06/Use-Decorator-Delegation.md) 14 | * [适配器模式 - Adapter](./Chapter07/Adapter.md) 15 | * [如何使用适配器模式](./Chapter07/Use-Adapter.md) 16 | * [观察者模式 - Observer](./Chapter08/Observer.md) 17 | * [通知 - Notification](./Chapter08/Notification.md) 18 | * [键值观察 - KVO](./Chapter08/KVO.md) 19 | * [备忘录模式 - Memento](./Chapter09/Memento.md) 20 | * [如何使用备忘录模式](./Chapter09/Use-Memento.md) 21 | * [归档 - Archiving](./Chapter09/Archiving.md) 22 | * [最后的润色](./Chapter10/FinalTouches.md) 23 | * [入门 - 小结](./Chapter11/Final.md) -------------------------------------------------------------------------------- /images/adapter1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/adapter1.png -------------------------------------------------------------------------------- /images/adapter2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/adapter2.png -------------------------------------------------------------------------------- /images/adapter3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/adapter3.png -------------------------------------------------------------------------------- /images/adapter4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/adapter4.png -------------------------------------------------------------------------------- /images/decorator1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/decorator1.png -------------------------------------------------------------------------------- /images/decorator2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/decorator2.png -------------------------------------------------------------------------------- /images/decorator3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/decorator3.png -------------------------------------------------------------------------------- /images/decorator4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/decorator4.png -------------------------------------------------------------------------------- /images/facade1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/facade1.jpg -------------------------------------------------------------------------------- /images/facade2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/facade2.png -------------------------------------------------------------------------------- /images/facade3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/facade3.png -------------------------------------------------------------------------------- /images/facade4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/facade4.png -------------------------------------------------------------------------------- /images/kvo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/kvo1.png -------------------------------------------------------------------------------- /images/memento1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/memento1.png -------------------------------------------------------------------------------- /images/memento2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/memento2.png -------------------------------------------------------------------------------- /images/mvc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/mvc1.png -------------------------------------------------------------------------------- /images/mvc2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/mvc2.png -------------------------------------------------------------------------------- /images/mvc3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/mvc3.png -------------------------------------------------------------------------------- /images/notifications1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/notifications1.png -------------------------------------------------------------------------------- /images/singleton1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/singleton1.png -------------------------------------------------------------------------------- /images/swiftDesignPattern1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/swiftDesignPattern1.png -------------------------------------------------------------------------------- /images/swiftDesignPattern2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/swiftDesignPattern2.png -------------------------------------------------------------------------------- /images/swiftDesignPattern3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourtion/SwiftDesignPatterns/b1cf961fff9936325a9171b63b81b7c28e0b76f4/images/swiftDesignPattern3.png --------------------------------------------------------------------------------