Ios 设计模式,你可能听说过,但是你真正知道这是什么意思么?大部分的开发者大概都同意设计模式很重要,但是关于这一部分却没有很多的文章去介绍它,我们开发者很多时候写代码的时候也并不重视设计模式.
设计模式是在软件设计上去解决普通问题的可重用的方法.他们是是帮助你让所写的代码更加容易理解和提高可重用性的模板.它们还可以帮你创建松散耦合的代码是你能不费很大功夫就可以改变或者替代你的代码中的一部分.
如果你对设计模式感到生疏,那么我有个好消息告诉你!首先,你已经用了很多ios设计模式多亏了Cocoa 内建的方法。其次,这个教程会带你加快对在Cocoa中最常用的ios设计模式的认识。
这个教程会分割成几部分,每一部分都是一个设计模式,在每一个部分,你都会读到以下内容:这是什么设计模式,为什么使用这个设计模式,怎么使用这个设计模式,当使用这个设计模式时要注意的常见的陷阱。
在这个教程中,你会创建一个音乐库app-会显示专辑和他们相应的信息。
在开发这款app的过程中,你将会熟悉以下Cocoa中最常见的设计模式。
1创建类型的:单例模式,抽象工厂模式
2结构化类型的:MVC, Decorator, Adapter, Facade and Composite
3行为类型的:Observer, Memento, Chain of Responsibility andCommand。
不要以为这个是关于理论的一篇文章,你将会学到如何使用这些设计模式的大部分去创建这款app。
开始:
下载开始工程,这只是默认的ViewController和一个简单的包含空的实现文件的HTTP客户端。
常识:你知道当你创建一个空的Xcode文件工程那你的代码已经充满了设计模式?MVC,Delegate,Protocol,Singleton。
在你涉足到第一个设计模式时,你要创建两个类去hold和展示专辑的数据。
按住Command + N键去创建一个名为Album的object-c文件,继承于NSObject。打开Album.h文件,加入以下的属性和方法。
@property (nonatomic, copy, readonly) NSString *title, *artist, *genre, *coverUrl, *year; - (id)initWithTitle:(NSString*)title artist:(NSString*)artist coverUrl:(NSString*)coverUrl year:(NSString*)year;
注意到这些属性是只读的,因为当Album创建以后没有必要去改变他们。
这个方法是这个对象的初始化方法,当你创建了一个专辑,你会传给它专辑的名字,艺术家的名字,专辑封面的URL和专辑的年份。
打开Album.m然后将下面代码添加到 @implementation和@end
之间。
这里没有什么神奇的; 只是一个简单的init方法来创建新Album的实例。
再次,导航到文件\新建\文件...。选择Cocoa
Touch,然后Objective-C类,然后单击下一步。将类名AlbumView,但这次设定的子类UIView的。单击下一步,然后创建。
打开ALbumView.h文件,在imlementation.h中添加以下代码:
@imlementation AlbumView { UIImageView*coverImage; UIActivityIndicatorView*indicator; }
在这里你需要注意的第一件事情是有一个实例变量名为coverImage,它代表了专辑的封面图片,第二个变量是indicator,它的旋转去指示图片正在被下载。
在实现部分的初始化中将背景设为黑色,创建的image view
有5像素边缘,然后将指示器indicator添加到试图中。
小贴士:为什么将私有变量定义在实现部分而不是接口部分?这是因为外部的类不需要知道这些变量的存在因为它们只是在这个类中的实现部分被使用。这个约定是十分重要的,如果你在创建一个库或者框架给其他开发者使用。
编译你的工程(Comamand +B)确保没有问题,接下来准备好去接受你的第一个设计模式吧。
MVC -
设计模式之王
Model View Controller 是Cocoa的基石之一,且毫无疑问的是所有设计模式中最常用的设计模式,它根据你的应用中的一般角色去分类对象,鼓励在完全分离的模式下分角色。
Model:这个对象hold住你的应用数据,且定义如何去操作它,例如本例中就是Album类。
View:这个对象掌管了Model的可视化显示,和控制用户的交互,基本上所有的都是UIView和它的子类。在本例中这个就是被分离成的AlbumView类。
Controller:控制器是调节所有工作的调节器,它访问模型中的数据,然后用视图去显示它,根据要求监听事件和操作数据。你能想象在这个哪个是Controller么,就是ViewController.
视图和模型通过控制器去交流的场景可以被描述成以下图:
如果在Model中有任何数据变化,那么它就会通知Controller,反过来,Controller更新在View中的数据,View可以通知Controller关于用户的行为,然后Controller要么根据需要或者检索要求的数据去更新Model。
你也许会怀疑为什么不只是创建了Controller然后将View
和Model
一起放到里面去实现?那样看起来不是更容易么?
这所有的所有都是为了是代码分离化和提高可重用性。理想情况下,视图应该会从Model中完全分离出来,如果视图不依赖于某个具体的Model的实现部分,那么它可以用不同的Model去展示其他一些数据来实现它的可重用性。
例如:如果将来你想添加一些电影和书籍到你的库中去,你仍然可以使用相同的AlbumView去展示你的电影和书籍的对象,更进一步说,如果你想去创建一个工程去处理专辑,你可以很简单的去重用你的Album类,因为它不依赖于人海一个试图。这就是MVC的魔力。
如何实现MVC模式
首先,你需要确保你的工程中的每一个类都是Controller,或者View,或者Model,不要讲任何两个中的角色的任务连接在一块,通过创建Album和AlbumV类你已经做了一个很好的工作。
其次,为了确保遵守这个工作方法,你应该创建三个工程组去hold住你的代码,每类一个分组。
按住Command+option+N键,创建一个组,名为Model,同样创建View和Controller,将Album.h和Album.m拖入Model中,拖动AlbumView.h和AlbumView.m的视图组,最后拖ViewController.h和ViewController.m到控制器组.
先在看起来没有那些文件浮在四周,看着好多了。显然你可以有其他的组和类,但是这个应用中的核心就是包含在这三个类中的。
既然你的组成部分已经被组织起来了,你需要从别的地方去获得album的数据,你将会创建一个API类去在全部的代码中去管理这些数据-这将会在你的下一个设计模式-单例中得到展示。
单例模式
单例模式确保为一个确定的类只有一个实例存在,而且有一个全局的访问指针只想它,他经常使用延时加载去在第一次使用的时候创建一个简单的实例。
小贴士:苹果使用这个方法很频繁。比如:[NSUserDefaults standarUserDefaults], [UIApplicationsharedApplication],[UIScreen mainScreen],[NSFileManager defaultManager],都返回一个单例。
你可能会以为为什么你会介意一个类周围会有不止一个实例,代码和内存都很简化,对不对?
有一些情况下一个类只有一个实例会很有意义。比如:没有必要去有很多的Logger实例,除非你想要同时写入几个日记文件,或者,拥有一个全聚德配置控制的类,很容易就实现一个线程安全的单个共享资源,比如配置文件,而不是多个类同时修改同一个配置文件。
如何使用单例
看一下右边的这个图表:
它显示了一个Logger类只有一个属性,(也是一个实例),和两个方法init和sharedInstance;
第一次客户端发送sharedInstance消息,这和属性的实例还没有初始化,所以你要创建这个类的一个新的实例然后返回一个指针指向它。
下一次你调用sharedInstance方法,实例就会立马返回而不需要任何的初始化,这个逻辑保证任何时候都只有一个实例存在。
你将要实现这和模式通过创建一个单例的类去管理所有的album 数据。
你将会意识到在这个组中有一个叫做API的租在这个工程中,这是你将会将所有的提供给你的app服务的类放到其中。在这个组中创建一个叫做LibraryAPI的类,继承于NSObject。
打开LibraryAPI.h,加入以下方法:
+(LibraryAPI*)sharedInstance;
在LibraryAPI.m中加入这些方法:
+(LibraryAPI*) sharedInstance{ //1 staticLibraryAPI* _sharedInstance; //2 staticdispatch_once_t once; //3 dispatch_once(&once,^{ _sharedInstance = [[LibraryAPI alloc]init]; }); return _sharedInstance; }
这个简单的方法中有很多内容:
1. 定义了一个静态变量去hold住类中的实例,确保它是全局可用的在你的类中。
2. 定义了一个静态变量dispatch_once_t类型的去确保这个初始化代码只执行一次。
3. 使用Grand Central Dispatch(GCD)去执行一个初始化LibraryAPI的实例的block,这就是单例设计模式的本质:这个初始化永远不会再次被调用一旦这个类被实例化。
下次你调用sharedInstance方法是,这个dispatch_once里面的block代码不会被执行了,你会得到一个先前创建这个LibraryAPI的一个引用。
现在你有了一个单例对象作为albums的入口指针,更进一步去创建一个类去处理库中数据的持久化。
在API组中创建一个PersistencyManager的继承于NSObject的类。打开它的.h文件,加入
#import “Album.h”再加入以下代码:
- (NSArray *)getAlbums; - (void)addAlbum:(Album *)album atIndex:(int)index; - (void)deleteAlbumAtIndex:(int)index;
以上是加入了三个方法去处理数据的。
打开.m文件在 @implemetation的上方加入这些代码:
@interface PersistencyManager () { // an array of all albums NSMutableArray *albums; }
上面的代码添加了一个类扩展,这是另一个添加私有变量和方法带类中所以外部的类并不只带他们,在这里,你声明了一个可变数组去hold住album的数据,这个数据是可变的以便你可以轻松地添加和删除专辑。
现在添加下面代码到PersistencyManager.m中去
- (id)init { self = [super init]; if (self) { albums = [NSMutableArray arrayWithArray:@[[[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"], [[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"], [[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"], [[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"], [[Album alloc] initWithTitle:@"American Pie" artist:@"Madonna" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png" year:@"2000"]]]; } return self; } -(NSArray *)getAlbums { return albums; } -(void)addAlbum:(Album *)album atIndex:(int)index { if (index < [albums count]) { [albums insertObject:album atIndex:index]; }else{ [albums addObject:album]; } } - (void)deleteAlbumAtIndex:(int)index { [albums removeObjectAtIndex:index]; }
在init这个初始化函数中你用五个实例转会填充了这个数组,其他几个方法允许你去得到,添加额删除albums。编译你的工程确保仍能正确编译。
在这个时候你可能会疑问为什么PersistenceManager不是单例,它和LibraryAPI的关系会下下一部分的外观(Fa?ade)设计模式中见到。
外观设计模式向复杂的子系统提供了简单的接口,相比将一系列的类和他们的接口暴露给用户,你只需要暴露一些简单的未定义的API。
接下来的图片解释了这一概念。
使用这些API接口的人完全没有意识到你这下面隐藏的复杂性,在有一系列类,特别是他们使用很复杂或者难以理解的时候,这个模式是非常好的。
外观设计模式使用从接口层面去使用,在实现技术上隐藏而将代码解藕了。它也减少了你外部的代码对于内部子系统代码的依赖性。它在外观模式可能要进行改变的情况下也是很有用的,因为外观的类仍然可以保持相同的API当背后的情况发生了变化时。比如,有一天你想改变背后的服务代码,你不用去改变这些代码因为这些API不会改变。
如何使用外观设计模式
目前你有PersistencyManager类去本地保存album的数据,而HTTPClient去处理远程的数据交流。工程里面的其他代码不应该意识到这个逻辑。
要实现这个LiabraryAPI你应该hold住PersistencyManager和HTTPClient的一个实例。然后LiabraryAPI会暴露一个简单的接口去访问这些服务。
小贴士:通常一个单例会在app的整个生命周期都会存在,你不应该持有过多的单例指针指向其他物体,因为它们在app关闭之前不会被释放。这个设计应该是像下面的这个图这样。
LiabraryAPI 会暴露给其他代码,但是会隐藏PersistenceManager和HTTPClient针的复杂性。
打开LiabraryAPI.h,添加#import “album.h”,接下来添加这些方法的人声明到里面。
- (NSArray *)getAlbums; - (void)addAlbum:(Album *)album atIndex:(int)index; - (void)deleteAlbumAtIndex:(int)index;
从现在开始,这些方法将会被你暴露给外部使用。
打开LiabraryAPI.m文件。加入
#import "HTTPClient.h" #import "PersistencyManager.h"
这是你导入这些类唯一的地方,记住:你的复杂的系统只能由你的API唯一的访问。现在,添加这些私有变量通过类扩展。(在@implementation上方)。
@interfaceLibraryAPI () { HTTPClient *client; PersistencyManager *manager; BOOL isOnLine; }
isOnline决定了服务器石油应该根据album
表中的变化进行更新,例如添加或者删除albums.
你需要在init里面对它们进行初始化。在LiabraryAPI.m文件中添加下面代码:
-(id)init { if (self = [superinit]) { client = [[HTTPClientalloc]init]; manager = [[PersistencyManageralloc] init]; isOnLine = NO; } returnself; }
HTTPClient这个并不会真正的和一个服务器配合工作,在这里只是为了演示外观设计模式,所以isOnline总是NO。接下来在LiabraryAPI.m文件中添加这些方法。
-(NSArray *)getAlbums { return [managergetAlbums]; } - (void)addAlbum:(Album *)album atIndex:(int)index { [manageraddAlbum:album atIndex:index]; if (isOnLine) { [clientpostRequest:@"/api/addAlbum"body:[album description]]; } } - (void)saveAlbums { [managersaveAlbums]; } - (void)deleteAlbumAtIndex:(int)index { [managerdeleteAlbumAtIndex:index]; if (isOnLine) { [clientpostRequest:@"/api/deleteAlbum"body:[@(index) description]]; } }
看一下这个
- (void)addAlbum:(Album *)album atIndex:(int)index<span style="color:black;"></span>
方法,这个类首先本地更新数据,然后如果由网络连接,就远程更新,这就是外观模式的魅力,如果在你系统以外的类加入了一些新的album,它不知道,也不需要去知道,这些复杂性都被隐藏在下面了。
提醒:当为你的子系统中的类设计一个外观模式,记住没有什么做什么去阻止客户端去直接访问这些隐藏的属性,不要假设所有的外部客户端都需要向你在外观模式下使用它们的方法那样去使用它们。
编译和运行你的程序,你会看见如下的空白的黑色屏幕。
你需要做点事情去在屏幕上去显示album的数据-那就是接下来的下一个设计模式:装饰设计模式。Decorator.
装饰设计模式
装饰设计模式动态的添加一些行为和任务到一个对象中且不需要去修改它的代码。当然你也可以选择用继承的方式-通过包装成另一个对象去改变它的行为。
在objective-c中由两个非常常用的实现方式:分类和代理。(Category, Delegate)
Category
分类是一种非常有用的机制,它允许你去添加一些方法到已经存在的类中且不用去继承它。这些新方法会在编译的时候添加上去,且可以像这个被扩展的类中的其他方法一样被执行。它和典型的装饰设计模式由一点轻微的不同,因为它并不hold住它所扩展的类的实例。
小贴士:除了向你自己的类去添加方法,你还可以向cocoa中的类去添加方法。
如何使用分类
想像这是你想要在tableView中显示album数据的一种方案。
这些album titles从哪里来?Album是一个模型对象,所以它并不在意你是怎么样显示数据的,你将要需要一些额外的代码去将这些功能添加到Album类中去,但是不要直接去修改这些类。
你将要创建一个分类去扩展Album。它要定义一个新的方法去返回一个数据结构来被UITableViews轻松的使用。这些数据结构看起来应该是这样的。
为了添加一个分类到Album中,新建一个文件选择Objective-C category 模版,在category field中键入TableRepresentation,在Category On
上写入Album。
在Album+TableRepresentation.h加入以下方法的声明。
- (NSDictionary *)tr_tableRepresentation;
注意到这里有一个tr_在方法名的前面,就像一个分类的名字的前缀。这样的命名约定有利于防止和其他方法冲突。
注意:如果在分类中声明的方法名字和在原来的类中的方法名字一样,或者和另一个类扩展中的方法名字一样,那么就会显示未定义,因为这些方法的实现是在运行时,如果你用分类去扩展你自己定义的类,那么出现问题的概率不大,但是如果你用分类添加方法到Cocoa或者CocoaTouch的类中,那有可能会产生很严重的问题。
在Album+TableRepresentation.m添加下面的方法
- (NSDictionary *)tr_tableRepresentation { return @{@"title": @[@"Artist",@"Album",@"Genre",@"Year"], @"values": @[self.artist, self.title, self.genre, self.year]}; }
想一下这个设计模式有多强大:
1你可以直接使用Album中的属性。
2 你可以不继承就可以向一个类添加方法。当然如果你想继承的话,也可以继承。
3 它可以简单的让你返回一个UITableView-ish的Album的显示类型,而不用修改Album的代码。
苹果公司使用分类在很多在基础的类上。要想去看他们是怎样实现的,可以打开NSString.h,找到@interface NSString ,那你将会看到这个类的定义和一下三个分类紧紧联系在一起:
NSStirngExtensionMethods, NSExtendedStringPropertyListParsing,NSStringDeprecated。分类把这些方法有序的组织起来而又分成几部分。
代理(delegateon)
另一个装饰设计模式就是代理,就是一个对象可以代表或者协助另一个对象的一种机制。例如,当你使用UITableView,其中你必须实现的方法就是tableView:number numberOfRowsInSection.
你不能指望UITableView去知道你想在每一个分区里面有多少行。因此,计算每个分区有多少行的任务就交给了UITableView的代理。这让UITableView
可以和它要显示的数据进行独立
UITableView 对象的工作是显示一个table view,然后最终它还是需要一些它根本没有拥有的数据。那么,它会向它的代理去发送一条消息去请求一些额外的信息。在Objective-C中的代理设计模式的实现中,一个类可以通过协议protocol声明必须的required或者可选的optional的方法。你将会实现这些协议在这个教程的后半部分。
似乎看起来通过继承一个对象然后去重写它的需要的方法更容易,凡事考虑到你你只能继承一个类。如果你向一个对象成为两个或者更多对象的代理,那你不能通过继承去实现这个目标。
小贴士:这是一个很重要的模式,苹果公司使用这个方法在大部分的UIKit的类中:UITableView,UITextView,UITextField,UIWebView,UIAlert,UIAction,UICollectionView,UIPickerView,UIGestureRecognizer,UIScrollerView等等。
如何使用代理模式:
在ViewController.m中加入导入这些文件的头文件。
#import "LibraryAPI.h" #import "Album+TableRepresentation.h"
利用类扩展去添加这些私有变量。
@interfaceViewController () { UITableView *dataTable; NSArray *allAlbums; NSDictionary*albumData; intcurrentAlbumIndex; } @end
然后在@interfaceViewController ()
加上 <UITableViewDataSource,
UITableViewDelegate>
这是你怎样使你的代理遵从一个协议-想像它是一个被代理去完成方法的协议的一个约定,在这里你让ViewController去遵守UITableViewDataSource和UITableViewDelegate协议,这个方法使得UITableView可以绝对保证这些必须方法会被它的代理所实现。
接下来,在viewDidLoad中加入这些代码:
self.view.backgroundColor = [UIColorcolorWithRed:0.76f green:0.81f blue:0.87f alpha:1]; currentAlbumIndex = 0; allAlbums = [[LibraryAPIsharedInstance]getAlbums]; dataTable = [[UITableViewalloc]initWithFrame:CGRectMake(0, 120, self.view.frame.size.width, self.view.frame.size.height-120) style:UITableViewStyleGrouped]; dataTable.delegate = self; dataTable.dataSource = self; dataTable.backgroundColor = nil; [self.viewaddSubview:dataTable];
这里是这些代码的讲解:
1首先将背景颜色改成了一个相对友好的背景色。
2通过API而不是PersistencyManager得到了albums
的列表。
3在这里创建了UITableView,你声明了这个控制器是UITableView的delegate/dataSource,因此UITableView的所有必须的方法都会由控制器提供。
然后添加这个方法在控制器实现代码中:
- (void)showDataForAlbumAtIndex:(int)index { if (index < [allAlbumscount]) { Album *album = [allAlbumsobjectAtIndex:index]; albumData = [album tr_tableRepresentation]; }else{ albumData = nil; } [dataTablereloadData]; }
showDataForAlbumAtIndex:从albums这个数组中获取了所需答album的数据。然后你之需要去调用reloadData.这会让UITableView询问它的代理诸如在table
view的每个分区中显示多少行,多少个分区,每一行应该怎么样之类的事情。
在viewDidLoad的结尾处加上[self showDataForAlbumAtIndex:currentAlbumIndex];
这会在应用启动的时候load现在的album,然后因为currentAlbumIndex原先被初始化为0,所以只是在会显示第一个album。
编译运行你的工程,你会遇到一个崩溃伴随一个异常的显示在调试控制台。
这怎么了?因为你声明了控制器成为UITableView的delegate和dataSource,但是这样的话你必须遵守去实现它的必须的方法包括numberOfRowsInSection这个你还没实现的方法。向ViewController.m中加入这两个方法。
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [albumData[@"title"] count]; } -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell =[tableView dequeueReusableCellWithIdentifier:@"Album"]; if (!cell) { cell = [[UITableViewCellalloc]initWithStyle:UITableViewCellStyleValue1reuseIdentifier:@"Album"]; } cell.textLabel.text = [albumData [@"title"] objectAtIndex:indexPath.row]; cell.detailTextLabel.text = [albumData[@"values"]objectAtIndex:indexPath.row]; return cell; }
前一个方法返回在table view中要显示的行数,在这里和数据结构中的titles的数量相同。后者创建并返回了一个带有title和value的cell。
编译和运行工程,你的应用应该是向这样展示在你的面前。
到目前为止,事情好像看起老很棒,但是你如果你调用第一张图片去展示启动后的app,那将会由一个水平的滚动条在屏幕的albums转换之间的顶部。与其创建一个单一用途的水平滚动条,为什么不使它成为一个通用的视图。
为了是这个视图可重用,所有的关于它的内容的决定都该留给另一个对象-它的代理。这个horizontal scroller应该定义一些方法让它的代理去实现以至于去和scroller一起工作,和UITableViewde
的代理方法想类似。我们将会在下一个设计模式中去讨论这个设计模式。
适配器设计模式(The Adapter Pattern)
适配器模式让不同的类之间的不兼容的接口可以一起工作。它将自己包装成一个对象,然后暴露一个标准的接口去让外界和这个对象去交互。
如果你对适配器模式熟悉,那么你会注意到苹果用一个稍微不同的方法去实现它-苹果使用协议去做这个工作,你也许会熟悉像UITableViewDelegate,UIScrollViewDelegate,NSCoding,NSCopying这样的协议,例如,通过NSCopying协议,任何的类都可以提供一个标准的copy方法。
如何使用适配器模式
这个之间提到的horizontal scroller应该是像下面的这个图这样子。
首先新建一个Objective-C的类,让它继承于UIView,打开它的.h文件,在@end的下面写上这行代码。
@protocolHorizontalScrollerDelegate <NSObject> @end
这定义了一个名为HorizontalScrollerDelegate的协议,继承于 NSObject协议。这是一个很好的实践去遵从NSObject协议-或者去遵从一个已经遵从NSObject协议的协议,这会让你可以向HorizontalScroller的代理对象发送NSObject中定义的消息。你将会看到这为什么是那么的重要。
你要定义它的代理必须和可选实现的方法在@protocol
和@end之间
@required - (int)numberOfviewsForHorizontalScroller:(HorizontalScroller *)horizontalScroller; - (UIView *)horizontalScroller:(HorizontalScroller *)scrollerviewAtIndex:(int)index; - (void)horizontalScroller:(HorizontalScroller *)scrollerclickViewAtIndex:(int)index; @optional - (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller *)scroller;
在这里你既有可选的也有必须的方法,必须的方法必须被代理实现,而通常这回包含一些这个类绝对需要的数据。在这个例子中,这些必须的分别是视图的数量,在特定的位置的视图和当一个视图被点击后的行为。这个可选的方法是初始化的使用,如果它不被代理所实现,那么默认就是第一个视图。
接下来,你需要将这个类的定义饮用到你的代理中去。但是这个协议的定义是在累的定义之下的,因此还剩不可见的,那你该怎么办呢。
解决的办法奇偶说前向的定义一个协议来让编译器去知道这样一个协议是可用的,所以,添加这行代码在@interface的上方。
然后在@interface
和@end之间下入如下代码。
这个代理的属性是weak类型的,为了防止循环引用这是必须的,如果一个类持有一个强指针指向它的代理,而它的代理也持有一个强的指针指向它,那么你的应用会因为任何一个类都不能彼此释放内存而造成内存泄漏。而id
类型表示你只可以成为遵守HorizontalScrollerDelefate的对象的assign方,给了你一定程度上的类型安全。
这个reload方法是一个在UITableView之后被重新刷新了,它reload了所有的用于构建horizontal
scroller的数据。
用下面的代码体大地HorizontalScroller.m中所有的代码。
#import "HorizontalScroller.h" #define VIEW_PADDING 10 #define VIEW_DIMENSION 100 #define VIEW_OFFSET 100 @interfaceHorizontalScroller ()<UIScrollViewDelegate> { UIScrollView*scroller; } @end @implementationHorizontalScroller @end
看一下这些注释:
1. 定义了一些常量去使更容易在设计时修改布局。视图在这个scroller中的面积是100*100有一个和它相近的矩形有一个10。
2. HorizontalScroller遵守<UIScrollViewDelegate>,这是因为HorizontalScroller适应一个UIScrollView去滚动album,它需要去知道用户的行为比如用户停止了滚动。
3. 创建了一个scroll view容器。
下一步你需要去实现这个初始化方法,添加下面这个方法。
- (id)initWithFrame:(CGRect)frame { self = [superinitWithFrame:frame]; if (self) { scroller = [[UIScrollViewalloc]initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; scroller.delegate = self; [selfaddSubview:scroller]; UITapGestureRecognizer *tap = [[UITapGestureRecognizeralloc]initWithTarget:selfaction:@selector(tapAction:)]; [scrolleraddGestureRecognizer:tap]; } returnself; }
这个scroll view完全填充了这个HorizontalScroller,一个UITapGestureRecognizer检测有没有在这个scroll view
上有触摸行为和检查一个album cover是否被点击。如果有的话,它会通知HorizontalScroller的代理。加入这个方法。
- (void)tapAction:(UITapGestureRecognizer *)gestureRecognizer; { CGPoint location =[gestureRecognizer locationInView:gestureRecognizer.view]; for (int index = 0; index< [self.delegatenumberOfviewsForHorizontalScroller:self]; index++) { UIView *view = scroller.subviews[index]; if (CGRectContainsPoint(view.frame, location)) { [self.delegatehorizontalScroller:selfclickViewAtIndex:index]; [scrollersetContentOffset:CGPointMake(view.frame.origin.x - self.frame.size.width/2 + view.frame.size.width/2, 0)]; break; } } }
手势被当成locationInView的一个参数去传递让你精确的知道位置,接下来你唤醒了它的代理的numberOfviewsForHorizontalScroller
方法,这个HorizontalScroller
的实例除了知道它可以安全的向一个遵守了HorizontalScrollerDelegate的对象发送方法以外其他的一无所知。对于在uiscroll view
中的每一个视图,用CGRectContainsPoint方法去找出被点击的视图。当这个视图找到了以后,向它的代理发送clickViewAtIndex消息。在不跳出这个循环之前,将这个被点击的视图居中。添加下面这个方法去reload
这个scroller。
- (void)reload { if (self.delegate == nil) { return; } [scroller.subviewsenumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { [obj removeFromSuperview]; }]; // 3 - xValue is the starting point of theviews inside the scroller CGFloat xValue = VIEW_OFFSET; for (int i=0; i<[self.delegatenumberOfviewsForHorizontalScroller:self]; i++) { // 4 - add a view at theright position xValue += VIEW_PADDING; UIView *view = [self.delegatehorizontalScroller:selfviewAtIndex:i]; view.frame = CGRectMake(xValue, VIEW_PADDING, VIEW_DIMENSION, VIEW_DIMENSION); [scrolleraddSubview:view]; xValue += VIEW_DIMENSION+VIEW_PADDING; } // 5 [scrollersetContentSize:CGSizeMake(xValue+VIEW_OFFSET, self.frame.size.height)]; if ([self.delegaterespondsToSelector:@selector(initialViewIndexForHorizontalScroller:)]){ int initialViewIndex = [self.delegateinitialViewIndexForHorizontalScroller:self]; [scrollersetContentOffset:CGPointMake(initialViewIndex*(VIEW_DIMENSION+(2*VIEW_PADDING)), 0) animated:YES]; } }
通过逐行注释来看看这些代码。
1. 如果没用代理,那么没用什么可以做的事情,那你可以直接返回了。
2. 将原先添加到scroll view中的字视图全部移除掉。
3. 所有的视图都在一个给丁的距离开始,现在它是100,但是可以轻松的通过改变上面那个定义的常量去改变,
4. HorizontalScroller每次询问它的代理让它们彼此水平的挨着在一块。
5. 一旦所有的视图都安置好了,设置这个滚动视图的contenOffset
让用户可以滚动所有的album covers。
6. 这个HorizontalScroller
检查它的代理是否响应initialViewIndexForHorizontalScroller这个方法,如果响应的话这个代码就会将这个滚动视图放在它的代理定义的初始视图的中心,否则默认的就是0
当你的数据发生了改变以后你要执行reload
操作,你也可以执行调用这个方法在你将HorizontalScroller
添加到其他的视图上的时候。加入一下代码到HorizontalScroller.m文件中,
- (void)didMoveToSuperview { [selfreload]; }
didMoveToSuperview这个消息当一个视图要添加到另一个视图上作为一个子视图上时被调用。这个时候就是reload scroller
的内容的正确的时机了。HorizontalScroller的最后一个难题时确保你正在看见的album总是在scroll
view的正中间。因此你们要去实现一些计算当用户用手指拖动这个scroll view
时。
将这个代码添加到HorizontalScroller.m中
- (void)centerCurrentView { int xFinal = scroller.contentOffset.x + (VIEW_OFFSET/2) + VIEW_PADDING; int viewIndex =xFinal / (VIEW_DIMENSION+(2*VIEW_PADDING)); xFinal =viewIndex * (VIEW_DIMENSION+(2*VIEW_PADDING)); [scrollersetContentOffset:CGPointMake(xFinal,0) animated:YES]; [self.delegatehorizontalScroller:selfclickViewAtIndex:viewIndex]; }
上面这段代码计算当前的scroll view
的骗一直和面积和视图的padding去计算当前的视图离中心的距离。最后一行是很重要的,一旦这个视图移动到中心了,通知它的代理这个显示的视图被改变了。
为了检测用户在scroll view的拖动,你必须添加如下的UIScrollViewDelegate方法:
- (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView { [selfcenterCurrentView]; } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollViewwillDecelerate:(BOOL)decelerate { if (!decelerate) { [selfcenterCurrentView]; } }
scrollViewDidEndDragging: willDecelerate:会通知它的代理当用户结束拖动的时候。如果用户还没有结束拖动那么这个decelerate参数就为真。当用户结束拖动,那么系统就会调用前面那个方法。两个方法中我们都调用了新的方法去让当前的视图居中因为这个当前的视图已经在用户拖动后发生了改变。
现在这个HorizontalScroller已经准备好使用了。回顾一下你刚才所写的代码,一点都没有提及到album和albumView这些类,这是非常好的,因为这意味着你的代码是独立的和可重用的。
现在编译使工程确保没事。
现在HorizontalScroller已经准备好了,是时候去使用它了。打开viewController.m加入如下的代码:
#import "HorizontalScroller.h" #import "AlbumView.h"
添加HorizontalScrollerDelegate,使
@interfaceViewController () <UITableViewDataSource, UITableViewDelegate, HorizontalScrollerDelegate>
添加下面的这个实例变量到它的类扩展中。
HorizontalScroller *scroller;
现在你可以实现这些代理方法了,你将会惊讶只用几行代码就可以实现很多的功能。
添加下面的代码到ViewController.m中
- (void)horizontalScroller:(HorizontalScroller *)scrollerclickViewAtIndex:(int)index { currentAlbumIndex = index; [selfshowDataForAlbumAtIndex:currentAlbumIndex]; }
这里设置了这些变量去存储暂时的album然后调用showDataForAlbumAtIndex方法去显示一个下那的album的数据。
小贴士:将一些方法放在一起通过@pragma mark
是一种很好的习惯。编译器会忽略这一行但是你会在你的Xcode的jump bar中将这些方法列起来。这会帮助你在Xcode中更好的组织代码。然后添加下面的代码
-(int)numberOfviewsForHorizontalScroller:(HorizontalScroller *)horizontalScroller { return [allAlbumscount]; }
这就是像你能辨认出来的那样,这是协议方法返回在scroll view中的视图的个数。因为折合scorll view
为所有的album
的数据显示covers ,这个count就是album
记录的数据。接下来添加这个方法:
-(UIView *)horizontalScroller:(HorizontalScroller *)scrollerviewAtIndex:(int)index { Album *album = [allAlbumsobjectAtIndex:index]; return [[AlbumViewalloc]initWithFrame:CGRectMake(0, 0, 100, 100) albumCover:album.coverUrl]; }
在这里你创建了一个新的AlbumView然后将它传递给了horizontalScroller.
就是这么多,你只用了三个很简短的方法就显示了一个很好看的horizontalScroller。
是的,你仍要去真正的创建一个scroller然后将它添加到你的主要的view中但是在坐这个之前。添加这个方法:
- (void)reloadScroller { // allAlbums = [[LibraryAPI sharedInstance]getAlbums]; if (currentAlbumIndex < 0) { currentAlbumIndex = 0; }elseif(currentAlbumIndex >= [allAlbumscount]){ currentAlbumIndex = [allAlbumscount]-1; } [scrollerreload]; [selfshowDataForAlbumAtIndex:currentAlbumIndex]; }
这个方法loads album的数据通过LibraryAPI然后设置当前啊的现实的视图在机遇当前的视图的index的值上,如果当前的view
index 小于0,那么意味着没有当前的视图选择,那就将第一张album显示,否则就是最后一张album被显示。
现在初始化这个scroller通过添加下面的代码到你的viewController.m中
scroller = [[HorizontalScrolleralloc]initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 120)]; scroller.delegate = self; [self.viewaddSubview:scroller]; [selfreloadScroller];
上面的仅仅是创建了一个新的HorizontalScroller实例,添加到main view中,然后loads
所有的子视图去显示album
的数据。
小贴士:如果一个协议变得很大,然后有很多的方法,那么你应该考虑将它分解成几个小的协议,UITableViewDelegate
和UITableViewDataSource
就是一个非常好的例子。尝试着去设计你的戏而已让每一个控制一个具体的功能。
编译和创建米的工程,看一下你的这个很棒的新的horizontal scroller:如下图:
等下,这个horizontal scroller是在里面了,但是这个album cover在哪里呢?
额,那九堆了-你还没有去实现下载cover的代码。所以你将要去添加一个下载图片的方法,计算你的所以访问的服务都是通过LibraryAPI,那么那里就是你放这些方法的地方,然后,首先还要考虑以下几个问题:
1 AlbumView不应该直接和LibraryAPI去搭配工作,你不要去将视图逻辑和交流逻辑混合起来.
2 相同的原因,LibraryAPI也不该知道有关AlbumView的事情。
3 LibraryAPI应该去通知AlbumView一旦这些covers已经下载好了因为AlbumView要去显示它们。
听起来像个谜,不要回信,你将会学到怎么样去使用Observer模式.
观察者模式
在观察者模式中,一个对象将会通知其他对象的任何状态的改变。这些相关的对象并不需要去知道另一个对象-这样就造成了一个非耦合的设计。这个模式大部分用在去通知一个感兴趣的对象它的一个属性已经发生了改变。
一般的实现需要一个对象注册成为它感兴趣的状态的观察者,当这个状态改变了,所有的观察者对象都会接收到通知。苹果的Push Notification服务就是对这个最好的例子。
如果你想要坚持MVC设计模式的概念,你需要去允许Model对象去和View对象交流,但是它们之间并没有直接的引用,这就是观察站模式引入的原因。
Cocoa实现观察者模式有两种常用的方法:Notification和Key-Value-Observing(KVO)
Notificaions
不要和和push或者本地的通知相混淆,Notifications是基于一个订阅-分发的模型去允许一个对象发送一些消息给其他对象。这个对象不需要去知道关于订阅者的任何信息。Notifications被苹果公司用的很多。例如,当键盘显示或者隐藏时,系统将会发送一个UIKeyboardWillShowNotification/UIKeyboardWillHideNotification,响应的当你进入到后台,系统将会发送一个UIApplicationDidEnterBackgroundNotification的通知.
提醒:打开UIApplication.h在文件的结尾你会看到一系列超过20条系统发送的通知。
如何使用Notifications
打开AlbumView.m加入下面的代码到[self addSubView:indicator]后面:
[[NSNotificationCenterdefaultCenter] postNotificationName:@"DownloadImageNotification" object:self userInfo:@{@"imageView":coverImage, @"coverUrl":albumCover}];
这一行通过NSNotificationCenter
单例发送一个通知,这个通知的info里面包括了UIImageView去计算和要下载的cover image的URL,这就是全部的你需要去实现下载任务的信息。
添加下面的代码到LibraryAPI的init方法中,直接在isOnline = No的后面。
[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(downloadImageAction:) name:@"DownloadImageNotification"object:nil];
这就是方程式的另一端,观察者。每次一个AlbumView类发送一个DownloadImageNotification的通知的是很好,因为Library已经注册成为它的一个观察者,所以系统会通知它,那么它就会相应的去执行downloadImage。
然而,在你实现downloadImage之前,你必须记住当你的类deallocated的时候去取消观察者的状态。否则的话,一个通知可能会被发送给一个已经被deallocated的对象,那么就会导致app
的崩溃。
添加下面的方法到Library中:
- (void)dealloc { [[NSNotificationCenterdefaultCenter]removeObserver:self]; }
当这个类结束后,它会移除它所有的通知中的观察者状态。
还有一件事要去做。如果你将下载的covers保存起就是一个好主意,因为我们不用一次又一次的去下载相同的covers。打开PersistencyManager.h然后添加下面的这两个方法的原型:
- (UIImage *)getImageFromFileName:(NSString *)fileName; -(void)saveAlbums;
然后将它们的实现代码添加到.m文件中:
-(void)saveImage:(UIImage *)image fileName:(NSString *)filename { filename = [NSHomeDirectory() stringByAppendingFormat:@"/Documents/%@",filename]; NSData *data = UIImagePNGRepresentation(image); [data writeToFile:filename atomically:YES]; } -(UIImage *)getImageFromFileName:(NSString *)fileName { fileName = [NSHomeDirectory() stringByAppendingFormat:@"/Documents/%@",fileName]; NSData *data = [NSDatadataWithContentsOfFile:fileName]; return [UIImageimageWithData:data]; }
这几个代码是十分简洁的,它下载的图片将会被保存在Documents
目录下,然后getImage:将会是nil如果在Documents目录下一个匹配的文件都没有。
然后添加下面的方法到Library.m中:
- (void)downloadImageAction:(NSNotification *)notification { UIImageView *imageView =notification.userInfo[@"imageView"]; NSString *coverUrl =notification.userInfo[@"coverUrl"]; imageView.image = [managergetImageFromFileName:[coverUrl lastPathComponent]]; if ( imageView.image == nil) { NSLog(@"notificatiom%@",notification.userInfo); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ UIImage *image = [clientdownloadImage:coverUrl]; dispatch_async(dispatch_get_main_queue(), ^{ imageView.image = image; [managersaveImage:image fileName:[coverUrl lastPathComponent]]; }); }); } }
这是上面代码的讲解:
1. downloadImage会通过通知被执行,所以这个方法以通知为参数,UIImageView和image URL从通知中得到。
2. 如果先前已经下载的话那么就从PersistencyManager中去获取图片
3. 如果没下载的话就通过HTTPClient去下载。
4. 当下载结束,在uiimage view 上显示图片,再用manager去将它在本地保存。
你又一次使用外观设计模式去隐藏了下载图片的复杂性,这个nofitifation根本不会去关心这个图片是通过网页下载的还是本地获得的。
编译和运行你的app,你将会看到下面这个美丽的covers在你的HorizentalScroller中。
停止你的app然后再次运行,只一道没有任何的延时在加载covers的时候,你可以断开网络然后你的应用还是运行的完美无瑕。然而你会发现,这个spinning并没有停止运行。这是怎么回事?
你在开始下载的时候开始启动这个spingning,但是你并没有实现在图片开始下载完好的时候去停止它的逻辑。你可以在每次图片已经下载完的时候发送一个notification,但是另外你可以使用另一个观察者模式-KVO。
Key-Value_Observing(KVO)
在KVO中,一个对象可以请求去在一个具体的属性开始变化的时候得到它的一个通知,不论这个属性属于它自己还是另一个对象。在这个例子里面,你可以使用KVO去观察加载image的UIImageView中的这个image属性的改变。
大家AlbumView.m,添加下面这个代码到initWIthFrame:albumCover:中,添加在[selfaddSubview:indicator]之后。
[coverImageaddObserver:selfforKeyPath:@"image"options:0 context:nil];
这添加了self,也当前的类成为coverImage的image属性的一个观察者。
你也需要吧去在你结束的时候取消成为观察者.仍在AlbumView.m中添加下面的代码:
-(void)dealloc { [coverImageremoveObserver:selfforKeyPath:@"image"]; }
最后,添加这个方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"image"]) { [indicatorstopAnimating]; } }
你必须在每个你作为一个观察者的类里面吧去实现这个方法。每次当被观察的属性变化时系统就会执行这个方法,在上面的方法中当image属性改变的时候就会调用这个方法。就这样,当一个图片下载完成,这个spinning就会停止.
编译运行你的工程,这个spinnning 就会消失。
小贴士:一定要记得去remove你的observers在它们deallocated时,否则当这个对象视图像一个不存在的观察者发送一个消息时那么你的app就会崩溃掉。
如果你运行以下app然后滚动一下这个covers,之后停止运行它,你会注意到app的状态并没有保存下来。你看到的最后一个视图并没有在应用再次启动的时候成为默认的设置。
要改正这一点,你可以充分的利用下一个设计模式:Memento设计模式(备忘录模式)
备忘录模式
备忘录设计模式将一个对象的内部状态进行捕捉并外部化,换句话说就是你将你的东西保存在某个地方。以后这个外部话的转台不需要借助封装就可以被回复,也就是私有的数据还是私有的。
如何使用备忘录设计模式
接下来将下面两个方法添加在ViewController.m中
- (void)saveCurrentState { [[NSUserDefaultsstandardUserDefaults] setInteger:currentAlbumIndexforKey:@"currentAlbumIndex"]; } - (void)loadPreviousState { currentAlbumIndex = [[NSUserDefaultsstandardUserDefaults]integerForKey:@"currentAlbumIndex"]; [selfshowDataForAlbumAtIndex:currentAlbumIndex]; }
saveCurrentState将当前的album
的index保存到NSUserDefaults,NSUserDefaults是一个iOS为了保存应用的具体的设置和数据而提供的一个标准的数据存储的类。
loadPreviousState加载了先前保存的index这并不是完整的备忘录设计模式,但是你现在达到了目的。
现在添加下面这行代码到ViewController的viewDidLoad中的在scroller被初始化的代码之前。
[self loadPreviousState];
这会在app启动的时候加载先前被保存的状态。但是你在那里保存从后台回来的的app的状态。你要使用通知去完成这个任务。iOS在app被放到后台的时候会发送一个UIApplicationDidEnterBackgroundNotification的通知,你可以使用这个通知去调用saveCurrentState,是不是很方便啊?
加入下面的这行代码到ViewDidLoad的结尾。
[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(saveCurrentState)
name:UIApplicationDidEnterBackgroundNotificationobject:nil];
当app将要被挂起到后台的时候,ViewController就会自动地调用saveCurrentState去保存状态。
现在添加下面的代码:
-(void)dealloc { [[NSNotificationCenterdefaultCenter]removeObserver:self]; }
这会保证当ViewController被deallocate时候将这个累中从观察者去移去。
编译运行你的应用,拖动到某一张album,使用Command+shift+H去将app设置到后台,然后关闭你的app,重新启动,看看原先你拖动到的那张图片是不是设置在中间。
看起来这个album的数据是对的嘛,但是这个scroller并没有将正确的album设置在中心,这是怎么回事?
这是因为这个可选的方法: initialViewIndexForHorizontalScroller的意义所在,因为你没有实现这个方法,所以它总是被设置在默认的第一张。
去修正它,添加下面的代码到ViewController中:
- (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller *)scroller { returncurrentAlbumIndex; }
现在这个HorizontalScroller的第一张数一被设置成表示album位置的currentAlbumIndex,这是一个很好的方法去确保这个app
保持个人化和可重用的双重便利。
再次运行你的app,滚动一个 app
,停止app,再启动,确保问题已经被解决。
如果你看看你的PersistencyManager的init方法,你会注意到这个album的数据被写死了,而每次当PersistencyManager被创建的时候就被重新创建,但是是不是只创建一次albums的列表然后将它们保存在一个文件中更好呢?但是你怎么将你的Album数据保存再一个文件中呢?
一个选择就是使用Album的property属性,将它们保存再一个plist文件中然后当需要的时候重新去创建Album实例,这不是最好的选择,因为它需要你去写一个具体的依赖于再每一个类中的数据或者properties的代码,例如说如果你后来创建了一个具有不同的属性的Movie的类,那保存和重新读取数据将要一个新的代码去完成。
此外,你不能将一个私有变量保存在每一个类的实例中因为它们是不能被外界所访问的。这就是苹果公司创建这个Achiving机制的原因。
Achiving
Achiving是苹果公司具体实现备忘录模式之一。它将一个对象转换成一个可以被保存后面可以回复但是不需要将私有变量暴露给外部类。
如何使用Achiving
首先你需要去通过遵守NSCoding这个协议表明这个Album类可以被压缩打开Album.h然后将它变成遵守这个协议:
@interface Album :
NSObject <NSCoding>
然后添加下面的代码到Album.m中:
- (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:_titleforKey:@"title"]; [aCoder encodeObject:_artistforKey:@"artist"]; [aCoder encodeObject:_coverUrlforKey:@"coverUrl"]; [aCoder encodeObject:_yearforKey:@"year"]; [aCoder encodeObject:_genreforKey:@"genre"]; } - (id)initWithCoder:(NSCoder *)aDecoder { if (self = [superinit]) { _title = [aDecoder decodeObjectForKey:@"title"]; _genre = [aDecoder decodeObjectForKey:@"genre"]; _year = [aDecoder decodeObjectForKey:@"year"]; _artist = [aDecoder decodeObjectForKey:@"artist"]; _coverUrl = [aDecoder decodeObjectForKey:@"coverUrl"]; } returnself; }
你调用encodeWithCoder当你压缩一个类的实例变量的时候,相反你调用initWithCoder当你解压一个实例去创建一个Album的实例的时候,很简单却很强大。
现在这个Album类可以被压缩那就添加时机保存和重载albums的列表的类。
添加下面的方法声明到你的PersistencyManager.h中:
-(void)saveAlbums;
这将会在调用保存albums数据的时候被调用,下面添加这个实现到PersistencyManager.m中
-(void)saveAlbums { NSString *path = [NSHomeDirectory() stringByAppendingString:@"/Documents/,albums.bin"]; NSData *data = [NSKeyedArchiverarchivedDataWithRootObject:albums]; [data writeToFile:path atomically:YES]; }
NSKeyedArchiver黄这个album数组压缩到一个albums.bin的文件中。当你u压缩一个包含其他对象的对象时,那么这个Archiver会自动的尝试去递归地压缩子对象和子对象的子对象等等。在这个实例中,这个archival开始于albums-这时一个Album各种实例的数组,因为NSArray和Album都支持NSCoping接口,所以这个数组被自动的压缩了。
现在替换PersistencyManager.m文件中的init文件。
<pre name="code" class="objc">- (id)init { self = [superinit]; if (self) { NSString *path = [NSHomeDirectory() stringByAppendingString:@"/Documents/,albums.bin"]; NSData *data = [NSDatadataWithContentsOfFile:path]; albums = [NSKeyedUnarchiverunarchiveObjectWithData:data]; if (!albums) { albums = [NSMutableArrayarrayWithArray:@[[[Albumalloc] initWithTitle:@"Best of Bowie"artist:@"David Bowie"coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_david%20bowie_best%20of%20bowie.png"year:@"1992"],[[Albumalloc] initWithTitle:@"It's MyLife"artist:@"NoDoubt"coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_no%20doubt_its%20my%20life%20%20bathwater.png"year:@"2003"],[[Albumalloc] initWithTitle:@"NothingLike The Sun"artist:@"Sting"coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_sting_nothing%20like%20the%20sun.png"year:@"1999"],[[Albumalloc] initWithTitle:@"Staringat the Sun"artist:@"U2"coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_u2_staring%20at%20the%20sun.png"year:@"2000"],[[Albumalloc] initWithTitle:@"AmericanPie"artist:@"Madonna"coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png"year:@"2000"]]]; [selfsaveAlbums]; } } returnself; }
在这个新的代码中NSKeyedUnarchiver从文件中加载这个album的数据,如果存在的话,如果不存在,它就会创建一个album数据然后立刻保存它让下次启动app的时候使用。
你也想要去在每次app进入到后台的时候保存album的数据,这似乎不需要但是如果后面你想要添加一个去改变album数据的选择呢?然后你想要去确保所有的变化都被保存了的。添加下面的方法声明到LibraryAPI.h中;
-(void)saveAlbums;
既然主要的application访问所有的服务都是通过LibraryAPI,这就是应用怎么样让PersistencyManager去知道它是否需要保存数据,现在添加下面的方法实现到LibraryAPI.m中。
- (void)saveAlbums { [managersaveAlbums]; }
这个代码仅仅传递了一个去让PersistencyManager去保持albums的一个调用。
然后添加下面这行代码到ViewController.m的saveCurrentState
中
[[LibraryAPIsharedInstance]saveAlbums];
上面的代码使用LibraryAPI去当ViewCOntroller想要去保存数据的时候触发保存album数据的方法。
现在变异你的app去确保编译通过。
不幸的是,没有什么简单的方法去检查数据的固话是否完全正确,你可以在模拟器的Mocunments文件夹下面去检查album数据文件是创建的,但是为了企业看到温和的改变你需要去添加一个可以去改变album数据的草种。
与其去改变数据,如果你添加一个去删除你不想让它存在在albums中的一个album的选择项的话是不是更好?此外添加一个撤销的操作以免被误删除呢?
这就提供了一个很好的机会去介绍最后一个设计模式:
命令模式
命令模式
这个设计模式将对象封装成了一个请求获取操作,这个封装请求比一个原始的请求更加的灵活,且可以在对象之间传递,稍后存储,动态修改获取放到一个队列之中。苹果公司是用Target-action机制和Invocation实现的,你可以在苹果的官方文档中去知道更多的关于Target-Action,但是Invocation使用包含一个Target对象的NSInvocation类,一个方法选择器和一些参数。这个对象可以根据需要动态的改变和执行,在命令模式中这是一个完美的例子,它解除耦合了发送对象和接收对象,且可以持续的坚持一个或者一系列请求。
如何使用命令模式:
早莫深入动作的invocation之前,你需要去设置撤销(undo action)动作的框架,所以你必须定义一个UIToolBar 和undo stack(撤销栈)上所需要的可变数组。
在ViewController.m的扩展中键入这些代码:
UIToolbar *toolBar; NSMutableArray *undoStack;
这创建了一个为了这些操作而添加显示的toolbar,还要一个数组去去作为命令队列。
添加下面的代码到ViewDidLoad的结尾:
toolBar = [[UIToolbaralloc]init]; UIBarButtonItem *undoBtm= [[UIBarButtonItemalloc]initWithBarButtonSystemItem:UIBarButtonSystemItemUndotarget:selfaction:@selector(undoAction)]; undoBtm.enabled = NO; UIBarButtonItem *space =[[UIBarButtonItemalloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpacetarget:nilaction:nil]; UIBarButtonItem *delete =[[UIBarButtonItemalloc]initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:selfaction:@selector(deleteAction)]; [toolBarsetItems:@[undoBtm,space,delete]]; [self.viewaddSubview:toolBar]; undoStack = [[NSMutableArrayalloc]init];
上面的代码创建了一个toolbar,它有两个按钮和一个可变的空格。这个撤销按钮被禁止了因为这个undo stack一开始是空的。
同样,因为这个toolbar没有根据frame来初始化,所以这个在viewDidload中frame的大小还没有设置。所以通过下面的一些代码在一旦视图的frame被最终设置好了以后设置这个frame。
-(void)viewWillLayoutSubviews { toolBar.frame = CGRectMake(0, self.view.frame.size.height-44, self.view.frame.size.width,44); dataTable.frame = CGRectMake(0, 130, self.view.frame.size.width, self.view.frame.size.height-200) ; }
你将会添加下面的三个方法到ViewController中去处理album的三个管理操作:添加,删除和撤销。
第一个方法是增加一个新的album:
- (void)addAlbum:(Album *)album atIndex:(int)index { [[LibraryAPIsharedInstance] addAlbum:album atIndex:index]; currentAlbumIndex = index; [selfreloadScroller]; }
在这里你添加了一个album,设置它的当前的album的index,然后重新加载scroller.接下来删除方法:
- (void)deleteAction { Album *deleteAlbum = [allAlbumsobjectAtIndex:currentAlbumIndex]; NSMethodSignature *sig = [selfmethodSignatureForSelector:@selector(addAlbum:atIndex:)]; NSInvocation*undoAction = [NSInvocationinvocationWithMethodSignature:sig]; undoAction.target = self; [undoAction setSelector:@selector(addAlbum:atIndex:)]; [undoAction setArgument:&deleteAlbumatIndex:2]; [undoAction setArgument:¤tAlbumIndexatIndex:3]; [undoAction retainArguments]; [undoStackaddObject:undoAction]; [[LibraryAPIsharedInstance] deleteAlbumAtIndex:currentAlbumIndex]; [selfreloadScroller]; [toolBar.items[0] setEnabled:YES]; }
在上面的这些代码里面有一些新的有趣的特性,所以解析一下:
1. 获得要删除的album,
2. 定义一个NSMethodSignature类型去创建一个NSInvocation,它将用于去在用户决定撤销一个删除时做和删除相反的操作。这个NSInovation需要知道三个事情:The selector(发送的消息),目标:((发送给谁)和发送的消息的一些参数。在这个例子里,消息时发送给删除的相反方,因为当你撤销一个删除操作,你需要将它们重新添加这个删除的album。、
3. 当这个undoAction被创建后你将它添加到undoStack中,这个动作将会被添加到一个数组的末尾,正像一个普通的堆栈一样。
4. 使用LibraryAPI去从数据结构中去删除album然后重新加载scroller。
5. 因为在undoStack中有了一个动作,所以你需要去将undo按钮使能。
注意:当你使用NSInvocation,你需要去记住下面的三个点:
1. 参数必须通过指针来传递。
2.参数起始于index 2,因为0和1是保留给target 和action用的。
3. 如果这些参数有可能被销毁(dealocate),你应该使用retainArguments.
最后添加下面的代码用于撤销:
- (void)undoAction { if ([undoStackcount] > 0) { NSInvocation*undoAction = [undoStacklastObject]; [undoStackremoveLastObject]; [undoActioninvoke]; }elseif( [undoStackcount] == 0){ [toolBar.items[0] setEnabled:NO]; } }
这个撤销操作会pops堆栈中的最后一个对象。这个对象永远是NSInvocation类型的而且可以通过调用invoke被激活。这回激活你你先前在album被删除时创建的命令,然后添加背删除的album到album列表中。因为你也可以删除在你撤销后最后一个对象,所以你需要判断这个栈是否时空的,如果是的话那么将undo按钮禁止交互。编译和运行你的app去特使这个undo 机制。删除这个album然后点击撤销操作去看看效果。
还有两种设置模式没用将它们应用在这个app中,但是也是很重要的:抽象工厂模式和责任链模式。但官方文档去扩展你的设计模式水平
在这个教程你已经看到怎么样去将发挥ios的设计模式的威力用很简单和耦合性地的方式去处理很复杂的任务。你已经学习了很多关于ios设计模式的概念:MVC, 但里,代理,协议,外观,贯彻着,备忘录,命令。
你最后的代码是耦合性很低的,可重用的且可读性该,如果其他开饭看你的代码它会立刻明白这是怎么回事和每个类的作用。
关键不是使用设计模式去写你的每一行代码而是当你在解决一个困难的问题特别是在设计应用的早期的时候意识到使用什么设计模式。它会让你的开发工作更容易,代码更高效。
iOS 设计模式