ios设计模式

IOS设计模式之一(MVC模式,单例模式)
博客分类:
ios
本文原文请见:http://www.raywenderlich.com/46988/ios-design-patterns.
由 @krq_tiger(http://weibo.com/xmuzyq)翻译,如果你发现有什么翻译错误,请与我联系谢谢。

iOS 设计模式-你可能已经听说过这个词,但是你真正理解它意味着什么吗?虽然大多数的开发者可能都会认为设计模式是非常重要的,然而关于设计模式这一主题的文章却不多,并且有时候我们开发者在写代码的时候也不会太关注它。
在软件设计领域,设计模式是对通用问题的可复用的解决方案。设计模式是一系列帮你写出更可理解和复用代码的模板,设计模式帮你创建松耦合的代码以便你不需要费多大力就可以改变或者替换代码中的组件。
如果你刚接触设计模式,我们有好消息告诉你!首先,多亏了Cocoa的构建方式,你已经使用了许多的设计模式以及被鼓励的最佳实践。
其次本指南将带你使用绝大多数(并不是所有)Cocoa中频繁使用的IOS 设计模式。
本指南被分为了许多部分,每个部分涉及一个设计模式。在每个部分中,你将会了解到如下内容:
• 设计模式是什么?
• 你为什么要用设计模式?
• 如何使用设计模式,以及在使用的时候,哪里是合适的,哪里是需要注意的坑。
在本指南中,你将创建一个音乐库应用,这个应用将显示你的专辑以及它们相关联的信息。
在开发本应用的过程中,你将熟悉被大量使用的Cocoa 设计模式:
• 创建型:单利(单态)和 抽象工厂
• 结构型:模型-视图-控制器,装饰器,适配器,外观(门面)和组合模式
• 行为型:观察者,备忘录,责任链和命令模式
不要被误导认为这是一篇关于设计模式理论的文章,在本音乐应用中,你将使用这些设计模式中的大多数,最终你的音乐应用将长的像下图所示的那样:
ios设计模式_第1张图片
我们开始吧!
下载 starter project,导出zip文件的内容,然后用xcode打开BlueLibrary.xcodeproj.
工程里面没有太多的文件,仅仅包含缺省的ViewController以及空实现的HTTP Client.
注意:当你创建一个新的Xcode工程的时候,你的代码其实已经涉及到了设计模式,你知道吗?模型-视图-控制器,委托,协议,单例-你不费吹灰之力就可以免费使用它们啦。
在你深入到第一个设计模式之前,你首先必须创建两个类,用这两个类去保存和显示音乐库专辑的信息。
在Xcode中,导航到”File\New\File…”(或者按Command+N快捷键),选择IOS>Cocoa Touch,然后Objective-C class,点击下一步。设置类名称为Album,父类选择NSObject,点击下一步,然后创建。
打开Album.h文件,在@interface和@end之间,增加如下的属性和方法原型:
Objective -c代码

1.@property (nonatomic, copy, readonly) NSString *title, *artist, *genre, *coverUrl, *year;    
2.- (id)initWithTitle:(NSString*)title artist:(NSString*)artist coverUrl:(NSString*)coverUrl year:(NSString*)year;  

注意到新增代码中所有的属性都是只读的,因为在Album对象创建以后,不需要修改它们的值。
新增的方法是对象初始化器(object initializer),当你创建一个新的专辑(album)对象的时候,你需要传递专辑(album)名,艺术家,专辑封面URL,以及年份。
现在打开Album.m文件,在@implementation 和 @end 之间 增加如下代码:
Objective-c代码

1.- (id)initWithTitle:(NSString*)title artist:(NSString*)artist coverUrl:(NSString*)coverUrl  
2.  year:(NSString*)year {  
3.    self = [super init];  
4.    if (self)  
5.    {  
6.        _title = title;  
7.        _artist = artist;  
8.        _coverUrl = coverUrl;  
9.        _year = year;  
10.        _genre = @"Pop";  
11.    }  
12.    return self;  
13.}  

这里没什么复杂花哨的东西,仅仅是一个创建Album实例的初始化方法而已。
在Xcode中,再一次导航到”File\New\File…”选择Cocoa Touch,然后Objective-C class,点击下一步。设置类名为AlbumView,但是这一次设置父类为UIView。点击下一步然后点击创建。
注意:如果你发现键盘快捷键更容易使用,Command+N将创建一个新文件,Command+Option+N将创建一个新组,Command+B将构建你的工程,Command + R 将运行它。
现在打开AlbumView.h,在@interface 和 @end之间 增加如下的方法原型:
Objective-c代码

1.- (id)initWithFrame:(CGRect)frame   albumCover:(NSString*)albumCover;  

现在打开AlbumView.m,用如下代码替换@implementation 之后所有的代码:
Objective-c代码

1.@implementationAlbumView  
2.{  
3.    UIImageView *coverImage;  
4.    UIActivityIndicatorView *indicator;  
5.}  
6.   
7.- (id)initWithFrame:(CGRect)frame albumCover:(NSString*)albumCover  
8.{  
9.    self = [super initWithFrame:frame];  
10.    if (self)  
11.    {  
12.   
13.        self.backgroundColor = [UIColor blackColor];  
14.        // the coverImage has a 5 pixels margin from its frame  
15.        coverImage = [[UIImageView alloc] initWithFrame:CGRectMake(5, 5, frame.size.width-10,  
16.   
17.        frame.size.height-10)];  
18.        [self addSubview:coverImage];  
19.   
20.        indicator = [[UIActivityIndicatorView alloc] init];  
21.        indicator.center = self.center;  
22.        indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;  
23.        [indicator startAnimating];  
24.        [self addSubview:indicator];  
25.    }  
26.    return self;  
27.}  
28.   
29.@end  

上面的代码里,你首先需要注意的是coverImage实例变量。它表示这个专辑的封面图。第二个变量是一个通过旋转来指示封面图正在下载的指示器。
在初始化器的实现中你设置背景颜色为黑色,创建了有5像素边框的图片视图,同时还创建了指示器。
注意:你可能想知道为什么私有变量在实现文件中定义,而不是在接口文件中?这是因为AlbumView以外的类不需要知道这些变量的存在,这些变量仅仅只在类内部函数使用。如果你在开发给其它开发者使用的框架,这个约定就显得十分重要了。
构建(Command + B)你的工程确保每件事情都井井有条,都ok吗?然后准备迎接我们的第一个设计模式!
模型-视图-控制器(MVC)模式 - 设计模式之王

模型-视图-控制器(MVC) 是Cocoa的构建块之一,毫无疑问它是使用最频繁的设计模式。它根据通用的角色去划分类,这样就使得类的
职责可以根据角色清晰的划分开来。
涉及到的三个角色如下:
Model:
模型保存应用程序的数据,定义了怎么去操作它。例如在本应用中模型就是Album类。
View:
视图是模型的可视化表示以及用户交互的控件;基本上来说,所有的UIView对象以及它的子类都属于视图。在本应用中AlbumView代表了视图。
Controller:
控制器是一个协调所有工作的中介者(Mediator)。它访问模型中的数据并在视图中展示它们,同时它们还监听事件和根据需要操作数据。你可以猜猜哪个类是控制器吗?它正是:ViewController。
一个MVC模式的好的实现也就意味着每一个对象都会被划分到上面所说的组中。
我们可以很好的用下图来描述通过控制器实现的视图到模型的交互过程:
ios设计模式_第2张图片
模型会把任何数据的变更通知控制器,然后控制器更新视图数据。视图对象通知控制器用户的操作,控制器要么根据需要来更新模型,要么检索任何被请求的数据。
你可能在想为什么不能仅仅使用控制器,在一个类中实现视图和模型,这样貌似更加容易?
所有的这些都归结于代码关注点分离以及复用。在理想的状态下,视图应该和模型完全的分离。如果视图不依赖某个实际的模型,那么视图就可以被复用来展示不同模型的数据。
举个例子来说,如果将来你打算加入电影或者书籍到你的资料库中,你仍然可以使用同样的AlbumView去显示电影和书籍数据。更进一步来说,如果你想创建一个新的与专辑有关联的工程,你可以很简单的复用Album类,因为它不依赖任何视图。这就是MVC的强大之处。
如何使用MVC模式
首先,你需要确保在你工程中的每个类是控制器,模型和视图中的一种,不要在一个类中组合两种角色的功能。到目前为止,你创建了一个Album类和AlbumView类,这样做挺好的。
其次,为了确保你能符合这种工作方法,你应该创建三个工程组(Project Group)来保存你的代码,每个工程组只存放一种类型的代码。
导航到”文件\新建\组(File\New\Group)”(或者按下Command + Option + N),命名组为Model,重复同样的过程来创建View和Controller组。
现在拖动Album.h和Album.m去模型组,拖动AlbumView.h和AlbumView.m去视图组,最后拖动ViewController.h和ViewController.m到控制器组。
此时工程结构应该看起来和下图类似:
ios设计模式_第3张图片
没有了之前所有文件都散落在各处,现在你的工程已经开起来好多了。显然你也可以有其它的组和类,但是本应用的核心包含在这三个类别中(Model,View,Controller)。
现在所有的组件都已经安排好了,你需要从某处获取专辑数据。你将创建一个贯穿于代码的管理数据的API-这也就代表将有机会去讨论下一个设计模式 - 单例(单态)模式。
单例(单态)模式
单例设计模式确保对于一个给定的类只有一个实例存在,这个实例有一个全局唯一的访问点。它通常采用懒加载的方式在第一次用到实例的时候再去创建它。
注意:苹果大量使用了此模式。例如:[NSUserDefaults standardUserDefaults],[UIApplication sharedApplication], [UIScreen mainScreen], [NSFileManager defaultManager],所有的这些方法都返回一个单例对象。
你很可能会想为什么这么关心是否一个类有多个实例?毕竟代码和内存都是廉价的,对吗?
有一些情况下,只有一个实例显得非常合理。举例来说,你不需要有多个Logger的实例,除非你想去写多个日志文件。或者一个全局的配置处理类:实现线程安全的方式访问共享实例是容易的,比如一个配置文件,有好多个类同时修改这个文件。
如何使用单例模式
首先来看看下面的图:
ios设计模式_第4张图片
上面的图描述了一个有单一属性(它就是单一实例)和sharedInstance,init两个方法的类。
客户端第一次发送sharedInstance消息的时候,instance属性尚未被初始化,所以此时你需要创建一个新的实例,然后返回它的引用。
当你下一次调用sharedInstance的时候,instance不需要任何初始化可以立即返回。这个逻辑保证总是只有一个实例。
你接下来将用这个模式来创建一个管理所有专辑数据的类。
你将注意到工程中有一个API的组,在这个组里你可以放入给你应用提供服务的所有类。在此组中,用IOS\Cocoa Touch\Objective-C class 模板创建一个新类,命名它为LibraryAPI,设置父类为NSObject.
打开LibraryAPI.h,用如下代码替换它的内容:
Objective-c代码

1.@interfaceLibraryAPI : NSObject   
2.+ (LibraryAPI*)sharedInstance;    
3.@end  

现在打开LibraryAPI.m,在@implementation 那一行后面插入下面的方法:
Objective-c代码

1.+ (LibraryAPI*)sharedInstance  
2.{  
3.    // 1  
4.    static LibraryAPI *_sharedInstance = nil;  
5.   
6.    // 2  
7.    static dispatch_once_t oncePredicate;  
8.   
9.    // 3  
10.    dispatch_once(&oncePredicate, ^{  
11.        _sharedInstance = [[LibraryAPI alloc] init];  
12.    });  
13.    return _sharedInstance;  
14.}  

在这个简短的方法中,有一些需要需要注意的点:
1.声明一个静态变量去保存类的实例,确保它在类中的全局可用性。
2.声明一个静态变量dispatch_once_t ,它确保初始化器代码只执行一次
3.使用Grand Central Dispatch(GCD)执行初始化LibraryAPI变量的block.这 正是单例模式的关键:一旦类已经被初始化,初始化器永远不会再被调用。
下一次你调用sharedInstance的时候,dispatch_once块中的代码将不会执行(因为它已经被执行了一次),你将得到原先已经初始化好的实例。
注意: 为了学习更多关于GCD方面的信息以及如何使用,请查看本站指南Multithreading and Grand Central Dispatch 和 How to Use Blocks。
你现在有一个单例的对象作为管理专辑数据的入口。咋们更进一步来创建一个处理资料库数据持久化的类。
在API组中,使用iOS\Cocoa Touch\Objective-C class 模板 创建一个新类,命名它为PersistencyManager,设置父类为NSObject.
打开PersistencyManager.h 在文件头部增加下面的导入语句:

import “Album.h”

接下来,在PersistenceManager.h文件的@interface之后,增加下面的代码:
Objective-c代码

1.- (NSArray*)getAlbums;  
2.- (void)addAlbum:(Album*)album atIndex:(int)index;  
3.- (void)deleteAlbumAtIndex:(int)index;  

上面是你需要处理专辑数据的方法的原型。
打开PersistencyManager.m文件,在@implementation行之前,增加下面的代码:
Objective-c代码

1.@interfacePersistencyManager () {  
2.    // an array of all albums  
3.    NSMutableArray *albums;  
}  

上面增加了一个类扩张(class extension),这是另外一个增加私有方法和变量以至于外部类不会看到它们的方式。这里,你申明了一个数组NSMutableArry 来保存专辑数据。这个数组是可变的方便你增加和删除专辑。
现在在PersistencyManager.m文件中@implementation行之后增加如下代码:
Objective-c代码

1.- (id)init  
2.{  
3.    self = [super init];  
4.    if (self) {  
5.        // a dummy list of albums  
6.        albums = [NSMutableArrayarrayWithArray:  
7.                 @[[[Album alloc] initWithTitle:@"Best of Bowie" artist:@"David Bowie" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_david%20bowie_best%20of%20bowie.png" year:@"1992"],  
8.                 [[Album alloc] initWithTitle:@"It's My Life" artist:@"No Doubt" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_no%20doubt_its%20my%20life%20%20bathwater.png" year:@"2003"],  
9.                 [[Album alloc] initWithTitle:@"Nothing Like The Sun" artist:@"Sting" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_sting_nothing%20like%20the%20sun.png" year:@"1999"],  
10.                 [[Album alloc] initWithTitle:@"Staring at the Sun" artist:@"U2" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_u2_staring%20at%20the%20sun.png" year:@"2000"],  
11.                 [[Album alloc] initWithTitle:@"American Pie" artist:@"Madonna" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png" year:@"2000"]]];  
12.    }  
13.    return self;  
14.}  

在init中,你用五条样例专辑填充数组。如果你不喜欢上面的专辑,你可以自由用你喜欢的专辑替换它们。
现在在PersistencyManager.m文件中增加下面的三个方法:
Objective-c代码

1.- (NSArray*)getAlbums  
2.{  
3.    return albums;  
4.}  
5.   
6.- (void)addAlbum:(Album*)album atIndex:(int)index  
7.{  
8.    if (albums.count >= index)  
9.        [albums insertObject:album atIndex:index];  
10.    else  
11.        [albums addObject:album];  
12.}  
13.   
14.- (void)deleteAlbumAtIndex:(int)index  
15.{  
16.    [albums removeObjectAtIndex:index];  
17.}  

这些方法让你可以增加和删除专辑。
构建你的工程确保每个资源都可以被正确的编译。
这时候,你可能想知道PersistencyManager类来自哪里?因为它不是一个单例类。下一部分,我们将探究LibraryAPI 和PersistencyManager之间的关系,那时候你将看到门面或者外观(Facade)模式。

IOS设计模式之二(门面模式,装饰器模式)
博客分类:
ios
本文原文请见:http://www.raywenderlich.com/46988/ios-design-patterns.
由 @krq_tiger(http://weibo.com/xmuzyq)翻译,如果你发现有什么错误,请与我联系谢谢。
门面(Facade)模式(译者注:facade有些书籍译为门面,有些书籍译为外观,此处译为门面)
ios设计模式_第5张图片
门面模式针对复杂的子系统提供了单一的接口,不需要暴漏一些列的类和API给用户,你仅仅暴漏一个简单统一的API。
下面的图解释了这个概念:
这个API的使用者完全不需要关心背后的复杂性。这个模式非常适合有一大堆很难使用或者理解的类的情况。
门面模式解耦了使用系统的代码和需要隐藏的接口和实现类。它也降低了外部代码对内部子系统的依赖性。当隐藏在门面之后的类很容易发生变化的时候,此模式就很有用了,因为当背后的类发生变化的时候,门面类始终保持了同样的API。
举个例子来说,如果有一天你想取代后端服务,你不需要改变API的使用者,因为API没有发生变化。

如何使用门面模式

当前你已经用PersistencyManager本地保存专辑数据,使用HTTPClient处理远程连接,工程中的其它类暂时与本次实现的逻辑无关。
为了实现这个模式,只有LibraryAPI应该保存PersistencyManager和HTTPClient的实例,然后LibraryAPI将暴漏一个简单的API去访问这些服务。

注意: 通常来说,单例类的生命周期贯穿于整个应用的生命周期中,你不应对保存太多其它对象的强引用,因为他们只有到应用关闭的时候才能被释放。

本次设计看起来像下图:
这里写图片描述
LibraryAPI将暴漏给其它代码,但是它隐藏了HTTPClient和PersistencyManager的复杂性。

打开LibraryAPI.h,在文件头部增加下面的导入语句:

Objective-c代码
1.#import “Album.h”

接下来,在LibraryAPI.h中增加如下的方法定义:
Objective-c代码

1.- (NSArray*)getAlbums;  
2.- (void)addAlbum:(Album*)album atIndex:(int)index;  
3.- (void)deleteAlbumAtIndex:(int)index;  

目前有一些你需要暴漏给其它类的方法。
打开LibraryAPI.m,增加如下的两个导入语句:

Objective-c代码

1.#import "PersistencyManager.h"  
2.#import "HTTPClient.h"  

这里将是唯一的导入这两个类的地方。记住:你的API是对于复杂系统唯一的访问点。
现在,增加通过类扩展(class extension)增加一些私有的变量(在@implementation 行之上):

Objective-c代码

1.@interfaceLibraryAPI () {  
2.    PersistencyManager *persistencyManager;  
3.    HTTPClient *httpClient;  
4.    BOOL isOnline;  
5.  
6.}  
7.@end  

isOnline决定了是否服务器中任何专辑数据的改变应该被更新,例如增加或者删除专辑。
你现在需要通过init初始化这些变量。在LibraryAPI.m中增加如下的代码:

Objective-c代码

1.- (id)init{  
2.    self = [super init];  
3.  
4.    if (self) {  
5.  
6.        persistencyManager = [[PersistencyManager alloc] init];  
7.  
8.        httpClient = [[HTTPClient alloc] init];  
9.  
10.        isOnline = NO;  
11.  
12.    }  
13.  
14.    return self;  
15.  
16.}  

HTTP 客户端实际上不会真正的和一个服务器交互,它在这里仅仅是用来演示门面模式的使用,所以isOnline将总是NO。
接下来,增加如下的三个方法到LibraryAPI.m:

Objective-c代码

1. -(NSArray*)getAlbums  
2.{  
3.    return [persistencyManager getAlbums];  
4.}  
5.  
6.   
7.  
8.- (void)addAlbum:(Album*)album atIndex:(int)index  
9.{  
10.    [persistencyManager addAlbum:album atIndex:index];  
11.  
12.    if (isOnline)  
13.  
14.    {  
15.  
16.        [httpClient postRequest:@"/api/addAlbum" body:[album description]];  
17.  
18.    }  
19.  
20.}  
21.  
22.   
23.  
24.- (void)deleteAlbumAtIndex:(int)index  
25.{  
26.  
27.    [persistencyManager deleteAlbumAtIndex:index];  
28.  
29.    if (isOnline)  
30.    {  
31.  
32.        [httpClient postRequest:@"/api/deleteAlbum" body:[@(index) description]];  
33.  
34.    }  
35.  
36.}  

我们来看一看addAlbum:atIndex:.这个类首先更新本地的数据,然后如果有网络连接,它更新远程服务器。这就是门面模式的强大之处。当某些外部的类增加一个新的专辑的时候,它不知道也不需要知道背后的复杂性。

注意:当为子系统的类设计门面的时候,要记住:任何东西都不能阻止客户端直接访问这些隐藏的类。不要对这些防御性的代码太过于吝啬,并且也不要假设所有的客户端都会和门面一样使用你的类。

构建并运行你的应用。你将看到一个激动人心的空白的黑屏:
接下来,你将需要在屏幕上显示专辑数据,使用你的下个设计模式-装饰器设计模式将是非常好的选择。
装饰器(Decorator)模式

装饰器模式在不修改原来代码的情况下动态的给对象增加新的行为和职责,它通过一个对象包装被装饰对象的方法来修改类的行为,这种方法可以做为子类化的一种替代方法。
在Objective-C中,存在两种非常常见的实现:Category(类别)和Delegation(委托)。

Category(类别)
Category(类别)是一种不需要子类化就可以让你能动态的给已经存在的类增加方法的强有力的机制。新增的方法是在编译期增加的,这些方法执行的时候和被扩展的类的其它方法是一样的。它可能与装饰器设计模式的定义稍微有点不同,因为Category(类别)不会保存被扩展类的引用。
注意: 你除了可以扩展你自己的类以外,你还可以给Cocoa自己的类增加方法。
如何使用类别
设想一种情况,你需要让Album(专辑)对象显示在一个表格视图(TableView)中:
ios设计模式_第6张图片
专辑的标题从何而来?因为专辑是模型对象,它本身不需要关心你如何显示它的数据。你需要增加一些代码去扩展专辑类的行为,但是不需要直接修改专辑类。
你将创建一个专辑类扩展的类别,它将定义一个新的方法,这个方法会返回能很容易和UITableViews使用的数据结构。这个数据结构如下图所示:
这里写图片描述
为了给Album增加一个类别,导航到“File\New\File…\”,选择Objective-C category模板,不要习惯性的选择Objective-C class模板。在Category域输入TableRepresentation,Category on域输入Album.
注意:你已经注意到了新建文件的名字了吗?Album+TableRepresentation意味着你正在扩展Album类。这种约定是非常重要,因为它方便阅读以及阻止和你或者其他人创建的类别冲突。
打开Album+TableRepresentation.h类,新增如下的方法原型:
Objective-c代码
1.- (NSDictionary*)tr_tableRepresentation;

注意在方法开头有一个tr_前缀,它是类别TableRepresentation的缩写。再一次,这种约定也可以阻止和其它的方法冲突。
注意:如果方法与原来类的方法重名了,或者与同样的类(甚至它的父类)的其它的扩展重名,那么运行期到底应该调用哪个方法是未定义的。当你仅仅是在扩展你自己写的类时,这没什么问题, 但是当你在扩展标准的Cocoa 或者Cocoa Touch类的时候,它可能会导致严重的问题。
打开Album+TableRepresentation.m,增加下面的方法:

Objective-c代码

1.- (NSDictionary*)tr_tableRepresentation  
2.{  
3.    return @{@"titles":@[@"Artist", @"Album", @"Genre", @"Year"],  
4.             @"values":@[self.artist, self.title, self.genre, self.year]};  
5.}  

咋们稍停片刻来看看这个模式的强大之处:
1. 你可以直接使用Album的属性
2. 你不需要子类化就可以增加方法。当然如果你想子类化Album,你任然可以那样做。
3. 简简单单的几句代码就让你在不修改Album的情况下,返回了一个UITableView风格的Album。
苹果在Foundation类中大量使用了Category。想知道他们是怎么做的,你可以代开NSString.h文件,找到@interface NSString,你将看到类和其它三个类别的定义:NSStringExtensionMethods,NSExtendedStringPropertyListParsing,NSStringDeprecated.类别让方法组织成相互独立的部分。
Delegation(委托)
委托作为另外一个装饰器模式,它是一种和其它对象交互的机制。举例来说,当你使用UITableView的时候,你必须要实现tableView:numberOfRowsInSection:方法。
你不可能让UITableView知道它需要在每个区域显示多少行,因为这些是应用特定的数据。因此计算每个区域需要显示多少行的职责就给了UITableView的委托。这就让UITableView类独立于它要显示的数据。
这里通过一个图来解释当你创建UITableView的时候会发生什么:
ios设计模式_第7张图片
UITableView的职责就是显示一个表格视图。然而最终它需要一些它自身没有的信息。那么它就求助于它的委托,通过发送消息给委托来获取信息。在Objective-C实现委托模式的时候,一个类可以通过协议(Protocol)来声明可选以及必要的方法。本指南稍后会涉及协议方面的内容。
子类化一个对象,复写需要的方法看起来好像更容易一点,但是考虑到你只能子类化一个类,如果你想一个对象作为两个或者更多对象的委托的话,使用子类化将不能实现。
注意:这个是一个重要的模式。苹果在UIKit类中大量使用了它:UITableView, UITextView,UITextField, UIWebView, UIAlert, UIActionSheet, UICollectionView,UIPickerView,UIGestureRecognizer, UIScrollView等等等。

如何使用委托模式
打开ViewController.m文件,在文件开头增加下面的导入语句:
Objective-c代码

1.#import "LibraryAPI.h"  
2.   #import "Album+TableRepresentation.h"  

现在,给类的扩展增加如下的私有变量,最终类的扩展如下所示:
Objective-c代码

1.@interfaceViewController () {  
2.    UITableView *dataTable;  
3.    NSArray *allAlbums;  
4.    NSDictionary *currentAlbumData;  
5.    int currentAlbumIndex;  
6.   }  
7.   
8.   @end  

然后用下面的代码取代类型扩展中@interface一行:
Objective-c代码

1.@interface ViewController ()  {  

这就是如何使得委托符合协议,你可以把它认为是委托履行协议方法契约的约定。在这里,你指定ViewController将实现UITableViewDataSource和UITableViewDelegate协议。这种方式使得UITableView非常确定那些委托必须要实现的方法。
接下来,用如下代码取代viewDidLoad方法:
Objective-c代码

1.- (void)viewDidLoad  
2.{  
3.    [super viewDidLoad];  
4.    // 1  
5.    self.view.backgroundColor = [UIColor colorWithRed:0.76f green:0.81f blue:0.87f alpha:1];  
6.    currentAlbumIndex = 0;  
7.   
8.    //2  
9.    allAlbums = [[LibraryAPI sharedInstance] getAlbums];  
10.   
11.    // 3  
12.    // the uitableview that presents the album data  
13.    dataTable = [[UITableView alloc] initWithFrame:CGRectMake(0, 120, self.view.frame.size.width, self.view.frame.size.height-120) style:UITableViewStyleGrouped];  
14.    dataTable.delegate = self;  
15.    dataTable.dataSource = self;  
16.    dataTable.backgroundView = nil;  
17.    [self.view addSubview:dataTable];  
18.}  

下面我们来解释一下上面代码中标记了数字的地方:
1. 改变背景色为漂亮的藏青色
2. 通过API获取专辑数据。你不需要直接使用PersistencyManager。
3. 创建UITableView,声明viewController为UITableView的委托和数据源;因此viewController将提供所有的被UITableView需要的信息。
现在,在ViewController.m中增加如下的方法:
Objective-c代码

1.- (void)showDataForAlbumAtIndex:(int)albumIndex  
2.{  
3.    // defensive code: make sure the requested index is lower than the amount of albums  
4.    if (albumIndex < allAlbums.count)  
5.    {  
6.        // fetch the album  
7.        Album *album = allAlbums[albumIndex];  
8.        // save the albums data to present it later in the tableview  
9.        currentAlbumData = [album tr_tableRepresentation];  
10.    }  
11.    else  
12.    {  
13.        currentAlbumData = nil;  
14.    }  
15.   
16.    // we have the data we need, let's refresh our tableview     
17.    [dataTable reloadData];  
18.}  

showDataForAlbumAtIndex:从专辑数组中获取需要的专辑数据。当你想去显示新的数据的时候,你仅仅需要调用reloadData.这将使得UITableView去问委托一些如下的信息:表格视图有多少个区域,每个区域应该显示多少行,每个单元格长什么样。
在viewDidLoad最后增加下面一行代码:
Objective-c代码

1.[self showDataForAlbumAtIndex:currentAlbumIndex];  

上面的代码会在app启动的时候加载当前的专辑,因为currentAlbumIndex设置为0,所以它将显示第一个专辑。
构建并运行你的工程;你将得到一个崩溃信息,在调试控制台上面将显示如下的异常:
这里写图片描述
这里出了什么问题?你声明ViewController作为UITableView的委托和数据源,但是这样做了,也就意味着你必须实现所必须的方法-这些方法包括你还没有实现的tableView:numberOfRowsInSection:方法.
在ViewController.m文件中@implementation 和@end 之间的任何位置,增加下面的代码:
Objective-c代码

1.- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section  
2.{  
3.    return [currentAlbumData[@"titles"] count];  
4.}  
5.   
6.- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath  
7.{  
8.    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];  
9.    if (!cell)  
10.    {  
11.        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"cell"];  
12.    }  
13.   
14.    cell.textLabel.text = currentAlbumData[@"titles"][indexPath.row];  
15.    cell.detailTextLabel.text = currentAlbumData[@"values"][indexPath.row];  
16.   
17.    return cell;  
18.}  

tableView:numberOfRowsInSection:方法返回表格视图需要显示的行数。这个和数据结构中的标题数量是匹配的。
tableView:cellForRowAtIndexPath:创建并返回一个包含标题和标题值的的单元格。
构建并运行你的工程,你的app应该会正常启动并显示下图所示的画面:
ios设计模式_第8张图片
到目前为止进展挺顺利。但是你回忆第一张本app最终效果的图,你会发现在屏幕顶部有一个水平滚动视图在不同的专辑之间切换。并不是构建一个只为这次使用的单一目的的水平滚动视图,你为什么不做一个可以让任何视图复用的滚动视图呢?
为了使这个视图可以复用,应该由委托来决定所有的从左边开始依次到下一个对象的内容。水平滚动条应该声明那些能和它一起工作的委托方法,这有点类似UITableView的委托方法的方式。我们将在讨论下一个模式的时候来实现它。

IOS设计模式之三(适配器模式,观察者模式)
博客分类:
ios
本文原文请见:http://www.raywenderlich.com/46988/ios-design-patterns.
由 @krq_tiger(http://weibo.com/xmuzyq)翻译,如果你发现有什么错误,请与我联系谢谢。
适配器(Adapter)模式
适配器可以让一些接口不兼容的类一起工作。它包装一个对象然后暴漏一个标准的交互接口。
如果你熟悉适配器设计模式,苹果通过一个稍微不同的方式来实现它-苹果使用了协议的方式来实现。你可能已经熟悉UITableViewDelegate, UIScrollViewDelegate, NSCoding 和 NSCopying协议。举个例子,使用NSCopying协议,任何类都可以提供一个标准的copy方法。

如何使用适配器模式

前面提到的水平滚动视图如下图所示:
ios设计模式_第9张图片
为了开始实现它,在工程导航视图中右键点击View组,选择New File…使用iOS\Cocoa Touch\Objective-C class 模板创建一个类。命名这个新类为HorizontalScroller,并且设置它是UIView的子类。

打开HorizontalScroller.h文件,在@end 行后面插入如下代码:
Objective-c代码

1.@protocolHorizontalScrollerDelegate   
2.// methods declaration goes in here  
3.@end  

上面的代码定义了一个名为HorizontalScrollerDelegate的协议,它采用Objective-C 类继承父类的方式继承自NSObject协议。去遵循NSObject协议或者遵循一个本身实现了NSObject协议的类 是一条最佳实践,这使得你可以给HorizontalScroller的委托发送NSObject定义的消息。你不久会意识到为什么这样做是重要的。
在@protocol和@end之间,你定义了委托必须实现以及可选的方法。所以增加下面的方法:

Objective-c代码

1.@required  
2.  
3.// ask the delegate how many views he wants to present inside the horizontal scroller  
4.  
5.- (NSInteger)numberOfViewsForHorizontalScroller:(HorizontalScroller*)scroller;  
6.  
7.   
8.  
9.// ask the delegate to return the view that should appear at <index>  
10.  
11.- (UIView*)horizontalScroller:(HorizontalScroller*)scroller viewAtIndex:(int)index;  
12.  
13.   
14.  
15.// inform the delegate what the view at <index> has been clicked  
16.  
17.- (void)horizontalScroller:(HorizontalScroller*)scroller clickedViewAtIndex:(int)index;  
18.  
19.   
20.  
21.@optional  
22.  
23.// ask the delegate for the index of the initial view to display. this method is optional  
24.  
25.// and defaults to 0 if it's not implemented by the delegate  
26.  
27.- (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller*)scroller;  

这里你既有必需的方法也有可选方法。必需的方法要求委托必须实现它,因为它提供一些必需的数据。在这里,必需的是视图的数量,指定索引位置的视图,以及用户点击视图后的行为,可选的方法是初始化视图;如果它没有实现,那么HorizontalScroller将缺省用第一个索引的视图。
下一步,你需要在HorizontalScroller类中引用新建的委托。但是委托的定义是在类的定义之后的,所以在类中它是不可见的,怎么办呢?
解决方案就是前置声明委托协议以便编译器(和Xcode)知道协议的存在。如何做?你只需要在@interface行前面增加下面的代码即可:

@protocolHorizontalScrollerDelegate;

继续在HorizontalScroller.h文件中,在@interface 和@end之间增加如下的语句:
Objective-c代码

1.@property (weak) id delegate;  
2.- (void)reload;  

这里你声明属性为weak.这样做是为了防止循环引用。如果一个类强引用它的委托,它的委托也强引用那个类,那么你的app将会出现内存泄露,因为任何一个类都不能释放调分配给另一个类的内存。
id意味着delegate属性可以用任何遵从HorizontalScrollerDelegate的类赋值,这样可以保障一定的类型安全。
reload方法在UITableView的reloadData方法之后被调用,它重新加载所有的数据去构建水平滚动视图。

用如下的代码取代HorizontalScroller.m的内容:

Objective-c代码

1.#import "HorizontalScroller.h"  
2.  
3.   
4.  
5.// 1  
6.  
7.#define VIEW_PADDING 10  
8.  
9.#define VIEW_DIMENSIONS 100  
10.  
11.#define VIEWS_OFFSET 100  
12.  
13.   
14.  
15.// 2  
16.  
17.@interfaceHorizontalScroller () <UIScrollViewDelegate>  
18.  
19.@end  
20.  
21.  
22.// 3  
23.  
24.@implementationHorizontalScroller  
25.  
26.{  
27.  
28.    UIScrollView *scroller;  
29.  
30.}  
31.  
32.   
33.  
34.@end  

让我们来对上面每个注释块的内容进行一一分析:

  1. 定义了一系列的常量以方便在设计的时候修改视图的布局。水平滚动视图中的每个子视图都将是100*100,10点的边框的矩形.
  2. HorizontalScroller遵循UIScrollViewDelegate协议。因为HorizontalScroller使用UIScrollerView去滚动专辑封面,所以它需要用户停止滚动类似的事件
    3.创建了UIScrollerView的实例。

下一步你需要实现初始化器。增加下面的代码:

Objective-c代码

1.- (id)initWithFrame:(CGRect)frame  
2.  
3.{  
4.  
5.    self = [super initWithFrame:frame];  
6.  
7.    if (self)  
8.  
9.    {  
10.  
11.        scroller = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];  
12.  
13.        scroller.delegate = self;  
14.  
15.        [self addSubview:scroller];  
16.  
17.        UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(scrollerTapped:)];  
18.  
19.        [scroller addGestureRecognizer:tapRecognizer];  
20.  
21.    }  
22.  
23.    return self;  
24.  
25.}  

滚动视图完全充满了HorizontalScroller。UITapGestureRecognizer检测滚动视图的触摸事件,它将检测专辑封面是否被点击了。如果专辑封面被点击了,它会通知HorizontalScroller的委托。
现在,增加下面的代码:

Objective-c代码

1.- (void)scrollerTapped:(UITapGestureRecognizer*)gesture  
2.  
3.{  
4.  
5.    CGPoint location = [gesture locationInView:gesture.view];  
6.  
7.    // we can't use an enumerator here, because we don't want to enumerate over ALL of the UIScrollView subviews.  
8.  
9.    // we want to enumerate only the subviews that we added  
10.  
11.    for (int index=0; index<[self.delegate numberOfViewsForHorizontalScroller:self]; index++)  
12.  
13.    {  
14.  
15.        UIView *view = scroller.subviews[index];  
16.  
17.        if (CGRectContainsPoint(view.frame, location))  
18.  
19.        {  
20.  
21.            [self.delegate horizontalScroller:self clickedViewAtIndex:index];  
22.  
23.            [scroller setContentOffset:CGPointMake(view.frame.origin.x - self.frame.size.width/2 + view.frame.size.width/2, 0) animated:YES];  
24.  
25.            break;  
26.  
27.        }  
28.  
29.    }  
30.  
31.}  

Gesture对象被当做参数传递,让你通过locationInView:导出点击的位置。
接下来,你调用了numberOfViewsForHorizontalScroller:委托方法,HorizontalScroller实例除了知道它可以安全的发送这个消息给委托之外,它不知道其它关于委托的信息,因为委托必须遵循HorizontalScrollerDelegate协议。
对于滚动视图中的每个子视图,通过CGRectContainsPoint方法发现被点击的视图。当你已经找到了被点击的视图,给委托发送horizontalScroller:clickedViewAtIndex:消息。在退出循环之前,将被点击的视图放置到滚动视图的中间。
现在增加下面的代码去重新加载滚动视图:

Objective-c代码

1.- (void)reload  
2.  
3.{  
4.  
5.    // 1 - nothing to load if there's no delegate  
6.  
7.    if (self.delegate == nil) return;  
8.  
9.   
10.  
11.    // 2 - remove all subviews  
12.  
13.    [scroller.subviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {  
14.  
15.        [obj removeFromSuperview];  
16.  
17.    }];  
18.  
19.   
20.  
21.    // 3 - xValue is the starting point of the views inside the scroller  
22.  
23.    CGFloat xValue = VIEWS_OFFSET;  
24.  
25.    for (int i=0; i<[self.delegate numberOfViewsForHorizontalScroller:self]; i++)  
26.  
27.    {  
28.  
29.        // 4 - add a view at the right position  
30.  
31.        xValue += VIEW_PADDING;  
32.  
33.        UIView *view = [self.delegate horizontalScroller:self viewAtIndex:i];  
34.  
35.        view.frame = CGRectMake(xValue, VIEW_PADDING, VIEW_DIMENSIONS, VIEW_DIMENSIONS);  
36.  
37.        [scroller addSubview:view];  
38.  
39.        xValue += VIEW_DIMENSIONS+VIEW_PADDING;  
40.  
41.    }  
42.  
43.   
44.  
45.    // 5  
46.  
47.    [scroller setContentSize:CGSizeMake(xValue+VIEWS_OFFSET, self.frame.size.height)];  
48.  
49.   
50.  
51.    // 6 - if an initial view is defined, center the scroller on it  
52.  
53.    if ([self.delegate respondsToSelector:@selector(initialViewIndexForHorizontalScroller:)])  
54.  
55.    {  
56.  
57.        int initialView = [self.delegate initialViewIndexForHorizontalScroller:self];  
58.  
59.        [scroller setContentOffset:CGPointMake(initialView*(VIEW_DIMENSIONS+(2*VIEW_PADDING)), 0) animated:YES];  
60.  
61.    }  
62.  
63.}  

我们来一步步的分析代码中有注释的地方:
1. 如果没有委托,那么不需要做任何事情,仅仅返回即可。
2. 移除之前添加到滚动视图的子视图
3. 所有的视图的位置从给定的偏移量开始。当前的偏移量是100,它可以通过改变文件头部的#DEFINE来很容易的调整。
4. HorizontalScroller每次从委托请求视图对象,并且根据预先设置的边框来水平的放置这些视图。
5. 一旦所有视图都设置好了以后,设置UIScrollerView的内容偏移(contentOffset)以便用户可以滚动的查看所有的专辑封面。
6. HorizontalScroller检测是否委托实现了initialViewIndexForHorizontalScroller:方法,这个检测是需要的,因为这个方法是可选的。如果委托没有实现这个方法,0就是缺省值。最后设置滚动视图为协议规定的初始化视图的中间。
当数据已经发生改变的时候,你要执行reload方法。当增加HorizontalScroller到另外一个视图的时候,你也需要调用reload方法。增加下面的代码来实现后面一种场景:

Objective-c代码

1.- (void)didMoveToSuperview  
2.{  
3.  
4.    [self reload];  
5.  
6.}  

didMoveToSuperview方法会在视图被增加到另外一个视图作为子视图的时候调用,这正式重新加载滚动视图的最佳时机。

最后我们需要确保所有你正在浏览的专辑数据总是在滚动视图的中间。为了这样做,当用户的手指拖动滚动视图的时候,你将需要做一些计算。

再一次在HorizontalScroller.m中增加如下方法:

Objective-c代码

1.- (void)centerCurrentView  
2.  
3.{  
4.  
5.    int xFinal = scroller.contentOffset.x + (VIEWS_OFFSET/2) + VIEW_PADDING;  
6.  
7.    int viewIndex = xFinal / (VIEW_DIMENSIONS+(2*VIEW_PADDING));  
8.  
9.    xFinal = viewIndex * (VIEW_DIMENSIONS+(2*VIEW_PADDING));  
10.  
11.    [scroller setContentOffset:CGPointMake(xFinal,0) animated:YES];  
12.  
13.    [self.delegate horizontalScroller:self clickedViewAtIndex:viewIndex];  
14.  
15.}  

为了计算当前视图到中间的距离,上面的代码考虑了滚动视图当前的偏移量,视图的尺寸以及边框。最后一行代码是重要的,一当子视图被置中,你将需要将这种变化通知委托。
为了检测用户在滚动视图中的滚动,你必需增加如下的UIScrollerViewDelegate方法:

Objective-c代码

1.- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate  
2.  
3.{  
4.  
5.    if (!decelerate)  
6.  
7.    {  
8.  
9.        [self centerCurrentView];  
10.  
11.    }  
12.  
13.}  
14.  
15.   
16.  
17.- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView  
18.  
19.{  
20.  
21.    [self centerCurrentView];  
22.  
23.}  

scrollViewDidEndDragging:willDecelerate:方法在用户完成拖动的时候通知委托。如果视图还没有完全的停止,那么decelerate参数为true.当滚动完全停止的时候,系统将会调用scrollViewDidEndDecelerating.在两种情况下,我们都需要调用我们新增的方法去置中当前的视图,因为当前的视图在用户拖动以后可能已经发生了变化。

你的HorizontalScroller现在已经可以使用了。浏览你刚刚写的代码,没有涉及到任何与Album或AlbumView类的信息。这个相对的棒,因为这意味着这个新的滚动视图是完全的独立和可复用的。

构建的工程确保每个资源可以正确编译。
现在HorizontalScroller完整了,是时候去在app使用它了。打开ViewController.m 增加下面的导入语句:

Objective-c代码

1.#import "HorizontalScroller.h"  
2.#import "AlbumView.h"  

增加HorizontalScrollerDelegate协议为ViewController遵循的协议:

Objective-c代码

1.@interfaceViewController ()<UITableViewDataSource, UITableViewDelegate, HorizontalScrollerDelegate>  

在类的扩展中增加下面的实例变量:

HorizontalScroller *scroller;

现在你可以实现委托方法;你可能会感到惊讶,因为只需要几行代码就可以实现大量的功能啦。

在ViewController.m中增加下面的代码:

Objective-c代码

1.#pragma mark - HorizontalScrollerDelegate methods  
2.  
3.- (void)horizontalScroller:(HorizontalScroller *)scroller clickedViewAtIndex:(int)index  
4.{  
5.    currentAlbumIndex = index;  
6.  
7.    [self showDataForAlbumAtIndex:index];  
8.  
} 

它设置保存当前专辑数据的变量,然后调用showDataForAlbumAtIndex:方法显示专辑数据。

注意:在#pragma mark 指令后面写方法代码是一种通用的实践。c 编译器会忽略调这些行,但是如果你通过Xcode的弹出框的时候,你将看到这些指令会帮你把代码组织成有独立和粗体标题的组。这可以帮你使得你的代码更方便在Xcode中导航。

接下来,增加下面的代码:

Objective-c代码

1.- (NSInteger)numberOfViewsForHorizontalScroller:(HorizontalScroller*)scroller  
2.{  
3.    return allAlbums.count;  
4.}  

正如你意识到的,这个是返回滚动视图所有子视图数量的协议方法。因为滚动视图要显示所有专辑的封面,这个数量就是专辑记录的数量。

现在,增加下面的代码:
Objective-c代码

1.- (UIView*)horizontalScroller:(HorizontalScroller*)scroller viewAtIndex:(int)index  
2.{  
3.  
4.    Album *album = allAlbums[index];  
5.  
6.    return [[AlbumView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) albumCover:album.coverUrl];  
7.  
8.}  

这里你创建了一个新的AlbumView,并且将它传递给HorizontalScroller。

够了,仅仅三个简短的方法就可以显示一个漂亮的水平滚动视图。

是的,你任然需要创建滚动视图,并且把它增加到你的主视图中,但是在这样做之前,你增加下面的方法先:

Objective-c代码

1.- (void)reloadScroller  
2.  
3.{  
4.  
5.    allAlbums = [[LibraryAPI sharedInstance] getAlbums];  
6.  
7.    if (currentAlbumIndex < 0) currentAlbumIndex = 0;  
8.  
9.    else if (currentAlbumIndex >= allAlbums.count) currentAlbumIndex = allAlbums.count-1;  
10.  
11.    [scroller reload];  
12.  
13.   
14.  
15.    [self showDataForAlbumAtIndex:currentAlbumIndex];  
16.  
17.}  

这个方法通过LibraryAPI加载专辑数据,然后根据当前视图的索引设置当前显示的视图。如果当前的视图索引小于0,意味着当前没有选定任何视图,此时可以选择第一个专辑来显示,否则下面一个专辑将会显示。

现在在viewDidLoad的[self showDataForAlbumAtIndex:0]之前增加下面的代码来初始化滚动视图:
Objective-c代码

1.scroller = [[HorizontalScroller alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 120)];  
2.  
3.scroller.backgroundColor = [UIColor colorWithRed:0.24f green:0.35f blue:0.49f alpha:1];  
4.  
5.scroller.delegate = self;  
6.  
7.[self.view addSubview:scroller];  
8.  
9.   
10.  
11.[self reloadScroller];  

上面的代码简单的创建了一个HorizontalScroller类的实例,设置它的背景色,委托,增加它到主视图,然后加载所有子视图去显示专辑数据。

注意:如果一个协议变得特别冗长,包含太多的方法。你应该考虑将它氛围更家细粒度的协议。UITableViewDelegate 和 UITableViewDataSource是一个好的例子。因为它们都是UITableView的协议。试着设计你的协议以便每个协议都关注特定的功能。
构建并运行你的on过程,查看一下你帅气十足的水平滚动视图吧:
ios设计模式_第10张图片
对了,等等。水平滚动视图没问题,但是为什么没有显示封面呢?

是的,那就对了-你还没有实现下载封面的代码。为了实现这个功能,你需要去新增一个下载图片的方法。因为所有对服务的访问都通过LibraryAPI,那我们就可以在LibraryAPI中实现新的方法。然而我们首先需要虑一些事情:
  1. AlbumView不应该直接和LibraryAPI交互。你不想混淆显示逻辑和网络交互逻辑。
  2. 同样的原因,LibraryAPI也不应该知道AlbumView。
  3. 一旦封面已经下载,LibraryAPI需要通知AlbumView,因为AlbumView显示专辑封面。
    听上去是不是挺糊涂的?不要灰心。你将学习如何使用观察者模式来实现它。

观察者(Observer)模式

在观察者模式中,一个对象任何状态的变更都会通知另外的对改变感兴趣的对象。这些对象之间不需要知道彼此的存在,这其实是一种松耦合的设计。当某个属性变化的时候,我们通常使用这个模式去通知其它对象。
此模式的通用实现中,观察者注册自己感兴趣的其它对象的状态变更事件。当状态发生变化的时候,所有的观察者都会得到通知。苹果的推送通知(Push Notification)就是一个此模式的例子。
如果你要遵从MVC模式的概念,你需要让模型对象和视图对象在不相互直接引用的情况下通信。这正是观察者模式的用武之地。

Cocoa通过通知(Notifications)和Key-Value Observing(KVO)来实现观察者模式。

通知(Notifications)
不要和远程推送以及本地通知所混淆,通知是一种基于订阅-发布模式的模型,它让发布者可以给订阅者发送消息,并且发布者不需要对订阅者有任何的了解。
通知在苹果官方被大量的使用。举例来说,当键盘弹出或者隐藏的时候,系统会独立发送UIKeyboardWillShowNotification/UIKeyboardWillHideNotification通知。当你的应用进入后台运行的时候,系统会发送一个UIApplicationDidEnterBackgroundNotification通知。

注意:打开UIApplication.h,在文件的末尾,你将看到一个由系统发出的超过20个通知组成的列表。

如何使用通知(Notifications)
打开AlbumView.m,在initWithFrame:albumCover::方法的[self addSubview:indicator];语句之后加入如下代码:
Objective-c代码

1.[[NSNotificationCenterdefaultCenter] postNotificationName:@"BLDownloadImageNotification"  
2.                                                    object:self  
3.                                                  userInfo:@{@"imageView":coverImage, @"coverUrl":albumCover}];  

这行代码通过NSNotificationCenter单例发送了一个通知。这个通知包含了UIImageView和需要下载的封面URL,这些是你下载任务所需要的所有信息。
在LibraryAPI.m文件init方法的isOnline=NO之后,增加如下的代码:
Objective-c代码

1.[[NSNotificationCenterdefaultCenter] addObserver:self selector:@selector(downloadImage:) name:@"BLDownloadImageNotification" object:nil];  

这个是观察者模式中两部分的另外一部分:观察者。每次AlbumView发送一个BLDownloadImageNotification通知,因为LibraryAPI已经注册为同样的通知的观察者,那么系统就会通知LibraryAPI,LibraryAPI又会调用downloadImage:来响应。
然而在你实现downloadImage:方法之前,你必须在你的对象销毁的时候,退订所有之前订阅的通知。如果你不能正确的退订的话,一个通知发送给一个已经销毁的对象会导致你的app崩溃。
在Library.m中增加下面的代码:

Objective-c代码

1.- (void)dealloc  
2.{  
3.    [[NSNotificationCenterdefaultCenter] removeObserver:self];  
4.  
5.}  

当对象被销毁的时候,它将移除所有监听通知的观察者。
还有一件事情需要去做,将已经下载的封面图片本地存储起来是个不错的主意,这样可以避免每次都重新下载相同的封面。

打开PersistencyManager.h文件,增加下面两个方法原型:
Objective-c代码

1.- (void)saveImage:(UIImage*)image filename:(NSString*)filename;  
2.  
3.- (UIImage*)getImage:(NSString*)filename;  

在PersistencyManager.m文件中,增加方法的实现:

Objective-c代码

1.- (void)saveImage:(UIImage*)image filename:(NSString*)filename  
2.  
3.{  
4.  
5.    filename = [NSHomeDirectory() stringByAppendingFormat:@"/Documents/%@", filename];  
6.  
7.    NSData *data = UIImagePNGRepresentation(image);  
8.  
9.    [data writeToFile:filename atomically:YES];  
10.  
11.}  
12.  
13.   
14.  
15.- (UIImage*)getImage:(NSString*)filename  
16.  
17.{  
18.  
19.    filename = [NSHomeDirectory() stringByAppendingFormat:@"/Documents/%@", filename];  
20.  
21.    NSData *data = [NSDatadataWithContentsOfFile:filename];  
22.  
23.    return [UIImage imageWithData:data];  
24.  
25.}  

上面的代码相当直接。下载的图片会被保存在文档(Documents)目录,如果在文档目录不存在指定的文件,getImage:方法将返回nil.
现在在LibraryAPI.m中增加下面的方法:
Objective-c代码

1.- (void)downloadImage:(NSNotification*)notification  
2.  
3.{  
4.  
5.    // 1  
6.  
7.    UIImageView *imageView = notification.userInfo[@"imageView"];  
8.  
9.    NSString *coverUrl = notification.userInfo[@"coverUrl"];  
10.  
11.   
12.  
13.    // 2  
14.  
15.    imageView.image = [persistencyManager getImage:[coverUrl lastPathComponent]];  
16.  
17.   
18.  
19.    if (imageView.image == nil)  
20.  
21.    {  
22.  
23.        // 3  
24.  
25.        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
26.  
27.            UIImage *image = [httpClient downloadImage:coverUrl];  
28.  
29.   
30.  
31.            // 4  
32.  
33.            dispatch_sync(dispatch_get_main_queue(), ^{  
34.  
35.                imageView.image = image;  
36.  
37.                [persistencyManager saveImage:image filename:[coverUrl lastPathComponent]];  
38.  
39.            });  
40.  
41.        });  
42.  
43.    }     
44.  
45.}  

下面是以上代码分段描述:

  1. downloadImage方法是通过通知被执行的,所以通知对象会当作参数传递。UIImageView和图片URL都会从通知中获取。
  2. 如果图片已经被下载过了,直接从PersistencyManager方法获取。
  3. 如果图片还没有被下载,通过HTTPClient去获取它。
  4. 当图片下载的时候,将它显示在UIImageView中,同时使用PersistencyManager保存到本地。

再一次,你使用了门面(Facade)模式隐藏了下载图片的复杂性。通知的发送者不需要关心图片是来自网络还是来自本地文件系统。
构建并运行你的应用,看看那些在滚动视图中的漂亮封面吧:
ios设计模式_第11张图片
停止你的应用再一次运行它,你会注意到不会存在加载图片的延迟,因为它们都已经被保存到了本地。甚至你可以断开网络,你的应用也可以完美地运行。然而这里有点奇怪,图片上的提示转盘一直在转动,出了什么问题呢?
当开始下载图片的时候,你启动了提示图片正在加载的旋转提示器,但是你还没有实现图片下载完成后停止它的逻辑。你应该在每次图片下载完成的时候发送一个通知,但是这里你使用KVO这种观察者模式。

Key-Value Observing(KVO)模式
在KVO中,一个对象可以要求在它自身或者其它对象的属性发送变化的时候得到通知。如果你对KVO感兴趣的话,你可以更进一步的阅读这篇文章:Apple’s KVO Programming Guide.
如何使用KVO
正如上面所说的,KVO机制让对象可以感知到属性的变化。在本例中,你可以使用KVO去观察UIImageView的image属性的变化。
打开AlbumView.m文件,在initWithFrame:albumCover:方法[self addSubview:indicator]这一行后,增加下面的代码:
Objective-c代码

[coverImage addObserver:self forKeyPath:@"image" options:0 context:nil]; 

这里它增加了它自己(当前的类)作为image属性的观察者。
当完成的时候,你同样需要注销相应的观察者。仍然在AlbumView.m中增加下面的代码:
Objective-c代码

1.- (void)dealloc  
2.{  
3.    [coverImage removeObserver:self forKeyPath:@"image"];  
4.  
5.}  

最后增加下面的方法:

Objective-c代码

1.- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context  
2.  
3.{  
4.  
5.    if ([keyPath isEqualToString:@"image"])  
6.  
7.    {  
8.  
9.        [indicator stopAnimating];  
10.  
11.    }  
12.  
13.}  

你必须在每个观察者类中实现这个方法。系统会在被观察的属性发送变化的时候通知观察者。在上面的代码中,当image属性变化的时候,你停止了封面上面的旋转提示器。这样以来,当图片加载完后,旋转提示器将会停止。
构建并运行的你的工程。旋转提示器应该会消失:
ios设计模式_第12张图片
注意:你要总是记得去移除已经销毁的观察者,否则当给不存在的观察者发送消息的时候,你的应用可能会崩溃。
如果你玩一回你的应用后终止它,你会发现你的应用状态没有被保存,你上次查看的专辑不是下次启动时候的缺省专辑。
为了修正这个问题,你可以使用列表中的下个模式:备忘录(Memento)模式.

你可能感兴趣的:(iOS)