本文翻译自:http://www.raywenderlich.com/46988/ios-design-patterns

iOS设计模式 - 你可能听到过这个术语,但是你知道是什么意思吗?虽然大多数的开发人员都认为设计模式是非常重要的,但是对于这方面的文章并不多,我们在写代码饿时候通常也很少花时间去考虑设计模式。

设计模式是软件设计可以解决很多问题。通过模板化的设计让你更加流畅的coding并且这样的代码更加让人容易理解和重用。同时使用恰当的设计模式可以使你的组件间解耦,让你很容易的去修改和替换已有组件。

如果你初学设计模式,那么这里有一个好消息!第一,由于Cocoa的良好框架你其实已经潜移默化的使用了大量的设计模式。第二,这个教程将会告诉你在Cocoa中普遍使用到的设计模式。

教程分为多个小节,一个小节对应一种设计模式。在每一个小节中,你讲阅读到如下解释项:

  • 这个设计模式是怎样的?
  • 为什么要使用这个设计模式。
  • 如何使用它,何时应该使用它,以及在使用中经常会遇到的一些问题。

在这个教程中,你讲创建一个音乐类的音乐他将展示专辑和相关信息。

在开发这个app过程中,你将了解如下设计模式,这些设计模式在Cocoa中被普遍使用:

  • 创建型:Singleton和Abstract Factory
  • 结构型:MVC,Decorator, Adapter, Facade and Composite
  • 表现型:Observer, Memento, Chain of Responsibility and Command.

请不要认为这篇文章只是一个理论教程;你将在开发过程中实际使用到这些知识点。当你完成本教程后,应用应该看起来是这样的:

开始

starter project下载初始工程,在xcode工具将它打开。

这个初始工程中并没有太过内容,只有默认的ViewController和一个简单的HTTP客户端空文件。

注意:你是否知道一般你使用xcode创建工程的时候其实已经在使用设计模式了?MVC, Delegate, Protocol, Singleton--Xcode默认已经帮助你完成了这些。

在你开始学习第一个设计模式前,你必须创建两个类拉存储和展示专辑信息。

点击“File\New\File…”(或者按下Command+N快捷键)。选择iOS > Cocoa Touch and then Objective-C class按下Next键。将类名称设定为Album继承自基类NSObject。点击Next创建。

打开Album.h文件,将如下代码加入到文件中:

#import <Foundation/Foundation.h>
 
@interface Album : NSObject
 
@property (nonatomic, copy, readonly) NSString *title, *artist, *genre, *coverUrl, *year;
 
- (id)initWithTitle:(NSString*)title artist:(NSString*)artist coverUrl:(NSString*)coverUrl year:(NSString*)year;
 
@end

注意到所有的属性都是只读模式,因为创建完Album后我们就没有必要再去修改它了。

方法是对象的初始化方法。当你创建了专辑对象后你需要将专辑名称,歌手,专辑发布时间和专辑资源进行设定。

打开Album.m将如下代码加入文件中:

#import "Album.h"

@implementation Album

- (id)initWithTitle:(NSString*)title artist:(NSString*)artist coverUrl:(NSString*)coverUrl year:(NSString*)year
{
    self = [super init];
    if (self)
    {
        _title = title;
        _artist = artist;
        _coverUrl = coverUrl;
        _year = year;
        _genre = @"Pop";
    }
    return self;
}

@end

这里应该不需要做太多解释;只是一个简单的构造方法去创建专辑实例。

接着,我们去创建专辑view名为AlbumView,继承自UIView

注意:如果你发现快捷键更加容易使用,Command+N将创建一个新文件, Command+Option+N 将创建一个新的组,Command+B将build你的工程,Command+R将运行工程。

打开AlbumView.h将下面的代码拷贝到文件中:

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

现在打开AlbumView.m文件,将@implementation后的所有代码替换成如下代码:

@implementation AlbumView
{
    UIImageView *coverImage;
    UIActivityIndicatorView *indicator;
}

- (id)initWithFrame:(CGRect)frame albumCover:(NSString*)albumCover
{
    self = [super initWithFrame:frame];
    if (self)
    {

        self.backgroundColor = [UIColor blackColor];
        // the coverImage has a 5 pixels margin from its frame
        coverImage = [[UIImageView alloc] initWithFrame:CGRectMake(, , frame.size.width-, frame.size.height-)];
        [self addSubview:coverImage];

        indicator = [[UIActivityIndicatorView alloc] init];
        indicator.center = self.center;
        indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;
        [indicator startAnimating];
        [self addSubview:indicator];
    }
    return self;
}

@end

你肯能注意到存在两个实例变量,其中coverImage代表专辑的封面。indicator指示当专辑下载时候的效果变量。

在构造方法的实现中将背景设定为黑色,创建一个image view和一个活动指示器。

注意:私有变量定义在实现文件中而不是定义在interface 文件中呢?那是因为外部的对象并不需要关心AlbumView对象的这两个变量所以只将这两个变量定义在实现文件中,他将只被类内部使用。

使用Command+B build你的工程以保证没有错误。你build完你的工程了吗?好的接下来我们就开始第一个设计模式的讲解。

MVC – 设计模式之王

Model View Controller (MVC)是Cocoa的一个内部使用最为广泛的设计模式。将对象按照不同功能角色隔离开,使得代码拥有清晰的进行分层结构。

三种功能角色:

  • Model(数据层):这些对象将拥有应用的所有数据,并且定义他们的操作。例如,在我们的应用中Model就是专辑类。
  • View(表现层): 这些对象是对于数据层的一个可视化表现,并且可以和用户进行交互;一般的所有UIViews和它的子类都属于这个层面。在我们的应用中AlbumView就是扮演这个角色。
  • Controller(控制层): 控制器是所有角色间的协调者。他从model角色对象中读取数据,使用views角色将数据表现出来,监控时间和组织数据。你能猜出在你的应用中哪一个是扮演这个角色吗?对了是:ViewController类。

在工程中我们可以按照角色的不同将不同的类放大不同的groups中。

可以使用下面这张图来解释他们三者之间的关系:

当任何数据发生变化的时候,Model将会通知Controller,接下来COntroller将会把Views中展示的数据更新。当用户触摸了View角色等情况下,View角色将会通知controller角色去更新数据或者要求controller去想model获取新的数据。

你可能要问,贼什么要有一个Controller角色在中介搅和呢?View角色和Model角色直接交互不更加简单?

如果这样的话就会将代码分层和重用性破坏。理想地,View应该和Model完全隔离。如果View和Model的实现没有关系,那么它就可以去连接不同的model来表现画面了。

例如,如果在未来你想在应用中增加电影或者图书项目,你依然可以使用AlbumView去展现他们。此外,如果你想重新创建一个新的工程区操作专辑,那么你大可直接使用现有的Album类,因为他不和人去View类相关联。这就是MVC的力量。

怎样使用MVC模式

首先你需要确认每一个在你的工程中的类是属于COntroller还是Model还是View;不要在一个类中包含两种角色。在现在的工程中你已经很好的完成了这一步,创建了Album类和AlbumView类。

Second, in order to ensure that you conform to this method of work you should create three project groups to hold your code, one for each category.

其次为了能够在写代码的时候更加清晰,你可以新建三个工程gropus来存放相应的代码文件。

例如我们可以将代码按照如下结构存放:

你的工程已经比之前看上去舒服多了。当然你可以在去新建其他的组去存放不同类型的代码,但是这三个组的代码将是你应用的核心代码存放的地方。

现在你组件已经被很好的组织起来了,你需要从某个地方将album数据获取到。你讲会创建一个API来管理你的数据---这个过程我们会在下一小节的Singleton模式中加以讨论。

Singleton 模式

Singleton design pattern(单例设计模式)确保在应用中你只有一份某对象的实例。它通常和懒加载一起使用,既一开始不创建这个对象等到第一次需要使用的时候才去创建它。

注意:Apple 也大量使用这种方式。例如[NSUserDefaults standardUserDefaults][UIApplication sharedApplication][UIScreen mainScreen][NSFileManager defaultManager]等都是返回一个单例对象。

你可能要问了,为什么要去抠抠搜搜的去只创建一个实例,代码和内存现在不是很便宜吗?

这里有几个使用场景来说明这件事情,例如我们不需要有多个Logger实例,除非你想同时往多个log文件中存放日志。我们系统只保持一份配置处理类:这样的话我们就能保证不会起配置冲突,例如对于配置文件的读写,可以防止多个实例同时去写造成的数据混乱。

怎样使使用 Singleton Pattern

请看一下如下类图:

在上述图片中展示了一个日志类,这个类只有一个属性(这个属性就是单例对象),和两个方法sharedInstance 和 init。

当第二次调用此类的sharedInstance方法的时候,instance将会立即返回而不会再去进行初始化。这个逻辑保证了任何时候都只有一个实例存在。

我们将在当前应用中使用此模式来创建一个类去管理所有的album数据。

你可能已经注意到一个叫做API的组存在于当前工程中;这个组里面我们将存放应用的所有服务类。在这个组里面我们创建一个继承自NSOBject的类叫做LibraryAPI。

然后打开LibraryAPI.h文件增加如下代码:

@interface LibraryAPI : NSObject

+ (LibraryAPI*)sharedInstance;

@end

现在去LibraryAPI.m将如下代码拷贝:

#import "LibraryAPI.h"

@implementation LibraryAPI
+ (LibraryAPI*)sharedInstance
{
    // 1 创建一个静态属性,属性将成为类属性,用于记录当前实例
    static LibraryAPI *_sharedInstance = nil;

    // 2 创建一次性执行标记位,确保第三步骤只会被执行一次
    static dispatch_once_t oncePredicate;

    // 3 使用GCD方法来执行初始化方法,此方法将根据oncePredicate来执行,值执行一次
    dispatch_once(&oncePredicate, ^{
        _sharedInstance = [[LibraryAPI alloc] init];
    });
    return _sharedInstance;
}
@end

当第二次调用sharedInstance方法的时候,存在于dispatch_once的block将不会被执行,将直接返回第一次执行后生成的实例。

注意:如果想要了解更多关于GCD知识请阅读Multithreading and Grand Central Dispatch 和 How to Use Blocks

你现在需要一个单例队形作为管理专辑的入口点。所以我们在进一步的去创建一个类去处理应用数据的固化。

在API组中创建一继承自NSObject的新类PersistencyManager。打开.h文件,将如下代码放入其中:

#import <Foundation/Foundation.h>
#import "Album.h"

@interface PersistencyManager : NSObject
- (NSArray*)getAlbums;
- (void)addAlbum:(Album*)album atIndex:(int)index;
- (void)deleteAlbumAtIndex:(int)index;
@end

上面定义了三个操作专辑数据的原型方法。

打开.m文件将如下代码拷贝到文件中:

@interface PersistencyManager () {
    // an array of all albums
    NSMutableArray *albums;
}

上面使用到了类的extension,这是另外一种将私有属性和方法加入到类中的方法。这里我们定义了一个NSMutableArray类型的属性去保存专辑的信息。可变数组保证了专辑数据可以方便的增加和删除。

现在将下面代码实现放入PersistencyManager.m文件中:

- (id)init
{
    self = [super init];
    if (self) {
        // a dummy list of albums
        albums = [NSMutableArray arrayWithArray:
                 @[[[Album alloc] initWithTitle:"],
                 [[Album alloc] initWithTitle:"],
                 [[Album alloc] initWithTitle:"],
                 [[Album alloc] initWithTitle:"],
                 [[Album alloc] initWithTitle:"]]];
    }
    return self;
}

在初始化init方法中将5分专辑数据放到数组中。当然专辑的连接信息你无法打开,敬请修改成其他专辑信息,这只是一个数据于是而已。

现在在PersistencyManager.m文件总增加如下方法实现代码:

- (NSArray*)getAlbums
{
    return albums;
}

- (void)addAlbum:(Album*)album atIndex:(int)index
{
    if (albums.count >= index)
        [albums insertObject:album atIndex:index];
    else
        [albums addObject:album];
}

- (void)deleteAlbumAtIndex:(int)index
{
    [albums removeObjectAtIndex:index];
}

这些方法可以让你获取,增加和删除专辑信息。

build 工程已确认没有什么语法上的错误。

在此时,你可能关心PersistencyManager是什么鬼,它好像也不是单例模式。关于LibraryAPI和PersistencyManager的关系我们将在下一小节中的Facade Design Pattern(外观模式)进行讲解。

The Facade Design Pattern(外观模式)

外观设计模式让复杂的系统开放一个简单的接口。但是他不会暴露过多的类的细节,它只会暴露一个简单的统一的API。

下面就是这个模式的概念图:

API用户完全感知不到内部处理上的逻辑复杂度。这个模式在处理大量类的时候非常好用,特别是当这些类对于客户来说又难用又难理解。

外观模式将接口和实现分离;减少外部和内部代码的依赖。当内部代码改变的时候外部代码依然可以通过原有API进行运行。

例如,如果有一天你想替换原有的内部服务,那么你不必对客户端代码做任何修改,因为API并不需要改变。

How to Use the Facade Pattern

现在你拥有一个叫做PersistencyManager去保存album的数据并且使用HTTPClient来处理远程通信。其他类不应该关心此逻辑。

为了实现这个模式,只有 LibraryAPI来存有PersistencyManager 和 HTTPClient的实例。然后LibraryAPI会暴露一个简单的API来共外部使用。

注意:一般地,一个单例会在整个应用生命周期内只有一个实例。所以不要在其他对象中对指向单例的属性做强引用。

我们设计应该看起来像这样:

LibraryAPI将会暴露给外部,但是他会隐藏对于HTTPClient 和 PersistencyManager的复杂的逻辑细节。
打开LibraryAPI.h将如下代码拷贝进文件:
#import <Foundation/Foundation.h>
#import "Album.h"

@interface LibraryAPI : NSObject
+ (LibraryAPI*)sharedInstance;

- (NSArray*)getAlbums;
- (void)addAlbum:(Album*)album atIndex:(int)index;
- (void)deleteAlbumAtIndex:(int)index;
@end

以上这个方法就是暴露在外的API。

现在我们去LibraryAPI.m进行实现处理

#import "PersistencyManager.h"
#import "HTTPClient.h"

@interface LibraryAPI () {
    PersistencyManager *persistencyManager;
    HTTPClient *httpClient;
    BOOL isOnline;
}

@end
isOnline标记位决定了是否对于专辑列表进行更新,因为有可能专辑增加或者删除了。

当然我们需要在初始化方法中进行变量的初始化:

- (id)init
{
    self = [super init];
    if (self) {
        persistencyManager = [[PersistencyManager alloc] init];
        httpClient = [[HTTPClient alloc] init];
        isOnline = NO;
    }
    return self;
}

HTTP 客户端实际并不做任何工作,只是用于证明外观模式,所以isOnline用用是NO。

接下来我们将如下实现拷贝进LibraryAPI.m:

- (NSArray*)getAlbums
{
    return [persistencyManager getAlbums];
}

- (void)addAlbum:(Album*)album atIndex:(int)index
{
    [persistencyManager addAlbum:album atIndex:index];
    if (isOnline)
    {
        [httpClient postRequest:@"/api/addAlbum" body:[album description]];
    }
}

- (void)deleteAlbumAtIndex:(int)index
{
    [persistencyManager deleteAlbumAtIndex:index];
    if (isOnline)
    {
        [httpClient postRequest:@"/api/deleteAlbum" body:[@(index) description]];
    }
}

观察一下addAlbum:atIndex:方法。这个方法先更新了本地数据,然后如果发现有远程连接的话就更新远端服务器。这就是外观模式的强大之处,当外部类通过API增加一个新专辑的时候他并不需要知道这内部复杂的逻辑处理。

注意:当为你的系统设计外观模式的时候,记住外部客户端绝对不能直接连接这些“隐藏”起来的类对象的方法。不要吝啬防御性代码,不要以为外部客户端使用了同样的逻辑而舍弃外观模式的访问方式。

build和运行你的程序,你会发现眼前一片黑,就像这样:

你需要一些view来展示你的专辑信息-- 这一个步骤也是讲解下一个设计模式的好例子:装饰者模式。

The Decorator Design Pattern 装饰者模式

装饰者模式自动将行为和责任加入到一个对象中但是不会修改这个对象的任何代码。这个设计和继承可以二取一,继承是在原有基类的基础上将行为等包装进一个新的对象中。

在Objective-c中存在两种实现此设计的方法:Category 和 Delegation

Category 分类

Category是一种强大的机制它允许你将方法增加到已有的类中(不是通过继承)。新的方法在编译的时候被加入,这些方法和原有的方法使用起来并没有任何不同。和装饰者的定义稍稍不同的是,一个Category并不会持有被扩展的类的任何实例。

注意:除了可以扩展自定义的类外,你也可以扩展Cocoa框架的类。

如何去使用Categories

下图是专辑信息的基本样式:

专辑的标题是哪里来的?Album是一个Model对象,作为Model对象其实并不太关心你是如何表现他的。你需要一些额外的代码将此种表现功能赋予Album类,但是记住千万不能直接修改这个Model类。

你讲创建一个category作为Album的扩展;他将定义一个新的方法将返回一种易于UITableView接受的数据结构。

数据结构如下:

为了增加Album的一个Category,我们可以按照如下方式创建:

注意:你是否注意到新的文件产生了,名为Album+TableRepresentation意为对于Album类的扩展。这中命名方式非常重要,易读并且对于一个类可以进行多次扩展,所以这样命名在扩展功能上也一目了然。

打开Album+TableRepresentation.h文件 增加如下方法:

- (NSDictionary*)tr_tableRepresentation;

注意到方法的前缀 tr_,这个是category的名称的缩写:TableRepresentation。这样写的目的也是为了在使用的时候便于区分各个category的方法!

注意:当你在category中定义的方法和原有类的方法或者原有类的子类的方法亦或者基于同一个类的其他category中的方法名称一样的时候回出现问题,由于是当此类属于Cocoa或者其他框架。

所以使用合理的前缀也一定程度上避免了这个问题。

打开Album+TableRepresentation.m复制如下代码:

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

停下来想一下,这个模式带来怎样的强大功能:

  • 你所使用的属性是直接来自Album类的。
  • 你在Album类中增加了方法但是没有继承他。当然如果你需要继承Album也可以这么做。
  • 新方法加入到类而没有改变任何Album的代码。

Apple 在框架类中大量使用Categories。如果你想了解他们是怎么做的你可以打开NSString.h文件找一下。

Delegation委托代理

另外一个装饰设计模式,委托代理是一种机制,它让一个对象作用域自身或者作用于另外一个对象。例如,当使用UITableView的时候我们必须实现方法tableView:numberOfRowsInSection:

你不能确定一个UITableVIew中有多少行,我们可以将这个计算行数的任务交给UITableView的delegate。这样使得UItableView在显示的时候不会过多依赖于当前的数据。

这里有一个创建UITableView的为代码说明:

UITableView对象用于显示表视图。然后,显示的时候也需要一些其他额外的信息。这样他就想代理发送消息询问这些附加信息。在Objective-C语言的代理模式实现中可以使用protocol技术来定义一个协议,其中包含一组可选择和必须的方法。在后续的教程中我们会稍微设计一点protocol的知识。

你可能会想我们只要将相关类进行子类化,然后在子类中覆盖实现相关方法不就行了吗?但是你有没有想过你只能继承单个类。如果你想让一个对象成为多个对象的代理那就搞不定了。

注意:只是一个特别重要的模式。Apple在大对数的UIkit类中使用此模式:UITableViewUITextViewUITextFieldUIWebViewUIAlertUIActionSheetUICollectionViewUIPickerViewUIGestureRecognizerUIScrollView.

怎样使用代理模式

打开ViewController.m文件增加如下代码:

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

@interface ViewController ()
{
    UITableView *dataTable;
    NSArray *allAlbums;
    NSDictionary *currentAlbumData;
    int currentAlbumIndex;
}
@end

然后,我们将@interface行变更如下

@interface ViewController () <UITableViewDataSource, UITableViewDelegate> {

这样的代码结构表示你的代理将准守一个协议--让ViewController准守代理申明的方法。这里你只是ViewController将会遵守UITableViewDataSource 和 UITableViewDelegate协议。当然UITableVIew中必须的方法需要在代理中实现。

Next, replace viewDidLoad: with this code:

下一步我们将修改viewDidLoad:方法:

- (void)viewDidLoad
{
    [super viewDidLoad];
    //1. 修改背景色
    self.view.backgroundColor = [UIColor colorWithRed:];
    currentAlbumIndex = ;

    //2. 通过API获取专辑信息,你不能直接去使用PersistencyManager的方法
    allAlbums = [[LibraryAPI sharedInstance] getAlbums];

    //3. 创建UITableView。将代理设置为当前对象也就是ViewController
    // the uitableview that presents the album data
    dataTable = [[UITableView alloc] initWithFrame:CGRectMake(, , self.view.frame.size.width, self.view.frame.size.height-) style:UITableViewStyleGrouped];
    dataTable.delegate = self;
    dataTable.dataSource = self;
    dataTable.backgroundView = nil;
    [self.view addSubview:dataTable];
}

现在去ViewController.m也就是表的代理去实现下列方法:

 
- (void)showDataForAlbumAtIndex:(int)albumIndex
{
    // defensive code: make sure the requested index is lower than the amount of albums
    if (albumIndex < allAlbums.count)
    {
        // fetch the album
        Album *album = allAlbums[albumIndex];
        // save the albums data to present it later in the tableview
        currentAlbumData = [album tr_tableRepresentation];
    }
    else
    {
        currentAlbumData = nil;
    }

    // we have the data we need, let's refresh our tableview
    [dataTable reloadData];
}

showDataForAlbumAtIndex:方法从专辑数组中获取必要的专辑数据。当你想呈现新的数据的时候,你只要调用一下reloadData即可。这将使得UITableVIew对象去询问代理诸如多少区域多少行每一行应该如何显示等。

将如下语句加入到viewDidLoad方法中:

[self showDataForAlbumAtIndex:currentAlbumIndex];

如果此时运行工程。你崩溃!!

这是肿么一回事情呢?你将ViewController作为UITableView的代理和数据源。但是你却没有实现相关方法包括--tableView:numberOfRowsInSection:等

将下列代码复制到ViewController.m文件中:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [currentAlbumData[@"titles"] count];
}

- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"cell"];
    }

    cell.textLabel.text = currentAlbumData[@"titles"][indexPath.row];
    cell.detailTextLabel.text = currentAlbumData[@"values"][indexPath.row];

    return cell;
}

tableView:numberOfRowsInSection: 返回行数

tableView:cellForRowAtIndexPath: 返回每一行的cell

再一次的运行你的工程,应该效果如下:

看起来不错了吧。但是想一想一开始我们给出的效果图还差什么呢?,那就是在表的上部缺少一个水平的scroller用于切换多个专辑信息。

为了使得这个视图具有重用性,所有的实现细节都将会在一个对象中:一个代理对象。水平的scroller应该在他的代理中实现代理方法,就好比UITableView代理方法一个道理。我们将在一个设计模式中讨论和实现他们。

The Adapter Pattern 适配器模式

适配器使得不相容的接口能够协调一起工作。它将自身包进一个对象中并暴露出一个标准的接口去和其他对象进行交互。

如果你熟悉适配器模式那么你会注意到Apple对于实现这个模式的时候语法上有一点稍微不一样 - Apple 使用协议来完成这件事情。你可能复习协议比如UITableViewDelegate,UIScrollViewDelegateNSCoding and NSCopying.通过NSCopying协议,任何类都会提供一个标准的拷贝方法。

怎样使用适配器模式

之前提出的水平的scroller 看起来就像这样:

开会实现它吧,在View 组上邮件然后新建HorizontalScroller类,它继承自UIView。

打开HorizontalScroller.h将如下代码加入其中:

#import <UIKit/UIKit.h>

@interface HorizontalScrollerand : UIView

@end

@protocol HorizontalScrollerDelegate <NSObject>
// methods declaration goes in here
@end

我们定义了一个叫做HorizontalScrollerDelegate的协议,它继承自NSObject 基础协议。在定义协议的时候讲NSObject基础协议作为超类是相当重要的。接下来的讨论中你将感受到为什么说如此重要。

接下来我们将定义此协议中必须的和可选的方法,所以增加下面的协议方法:

 
@protocol HorizontalScrollerDelegate <NSObject>
@required
// ask the delegate how many views he wants to present inside the horizontal scroller
- (NSInteger)numberOfViewsForHorizontalScroller:(HorizontalScroller*)scroller;

// ask the delegate to return the view that should appear at <index>
- (UIView*)horizontalScroller:(HorizontalScroller*)scroller viewAtIndex:(int)index;

// inform the delegate what the view at <index> has been clicked
- (void)horizontalScroller:(HorizontalScroller*)scroller clickedViewAtIndex:(int)index;

@optional
// ask the delegate for the index of the initial view to display. this method is optional
// and defaults to 0 if it's not implemented by the delegate
- (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller*)scroller;
@end

上述代码定义了必须的和可选的方法。必须的方法必须被代理所实现,一般这种方法通常会包含一些数据返回。在这个场景中,我们必须要知道有多少view,指定index出的view已经当被触摸后的行为等。可选方法是指首个初始化的View,如果没有实现让就返回首个index的view。

接下来,你需要将新的代理放到HorizontalScroller类的定义中去。但是协议的定义目前仅仅在类的定义中的话,还是没有正真实现所以也是不可使用的。该怎么做呢?

所以在 HorizontalScroller.h按照如下设定代码:

#import <UIKit/UIKit.h>
@protocol HorizontalScrollerDelegate;

@interface HorizontalScroller : UIView
@property (weak) id<HorizontalScrollerDelegate> delegate;
- (void)reload;
@end

@protocol HorizontalScrollerDelegate <NSObject>
@required

上述代码中的代理属性是weak类型的。这就是为了避免循环引用。如果一个雷按照strong类型去指向它的代理,而代理有保持strong类型的指向类,那么引用将会产生内存泄露。

将.m的内容更换成如下:

#import "HorizontalScroller.h"

#define VIEW_PADDING 10
#define VIEW_DIMENSIONS 100
#define VIEWS_OFFSET 100

@interface HorizontalScroller () <UIScrollViewDelegate>
@end

@implementation HorizontalScroller
{
    UIScrollView *scroller;
}

@end

让我们来说明一下各个代码块的作用:

  1. 定义常量,用于view在scrollerview的相对位置。
  2. 让HorizontalScroller类遵守UIScrollViewDelegate协议。既然HorizontalScroller需要使用UIScrollView来滚动显示专辑封面,所以他需要知道用户的一些事件诸如用户停止滚动等。
  3. 创建scroll view来存放专辑 view。

接下来实现初始化方法,增加如下代码:

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
        scroller = [[UIScrollView alloc] initWithFrame:CGRectMake(, , frame.size.width, frame.size.height)];
        scroller.delegate = self;
        [self addSubview:scroller];
        UITapGestureRecognizer *tapRecognizer = [[                                                                                                    UITapGestureRecognizer alloc] initWithTarget:self action:@selector(scrollerTapped:)];
        [scroller addGestureRecognizer:tapRecognizer];
    }
    return self;
}

Scroll View将完全填充满HorizontalScroller。UITapGestureRecognizer手势指明当有touches作用于scroll view的时候检查是否是一个专辑封面被点击了。如果是则通知HorizontalScroller委托代理。

增加下列手势处理逻辑:

- (void)scrollerTapped:(UITapGestureRecognizer*)gesture
{
    CGPoint location = [gesture locationInView:gesture.view];
    // we can't use an enumerator here, because we don't want to enumerate over ALL of the UIScrollView subviews.
    // we want to enumerate only the subviews that we added
    ; index<[self.delegate numberOfViewsForHorizontalScroller:self]; index++)
    {
        UIView *view = scroller.subviews[index];
        if (CGRectContainsPoint(view.frame, location))
        {
            [self.delegate horizontalScroller:self clickedViewAtIndex:index];
            [scroller setContentOffset:CGPointMake(view.frame.origin.x - self.frame.size.width/ + view.frame.size.width/, ) animated:YES];
            break;
        }
    }
}

手势会将位置信息传入从而你可以知道触摸事件的发生地点。

接下来会调用委托的numberOfViewsForHorizontalScroller:方法。

对于scroll view中的每一个view来说,都是会通过hit test来确定谁被触摸了。当view被发现,将向委托发送horizontalScroller:clickedViewAtIndex:消息。在跳出for loop之前,将被触摸的view置于scroller中间位置。

现在增加如下代理去重新加载scroller:

 
- (void)reload
{
    // 1 - nothing to load if there's no delegate
    if (self.delegate == nil) return;

    // 2 - remove all subviews
    [scroller.subviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        [obj removeFromSuperview];
    }];

    // 3 - xValue is the starting point of the views inside the scroller
    CGFloat xValue = VIEWS_OFFSET;
    ; i<[self.delegate numberOfViewsForHorizontalScroller:self]; i++)
    {
        // 4 - add a view at the right position
        xValue += VIEW_PADDING;
        UIView *view = [self.delegate horizontalScroller:self viewAtIndex:i];
        view.frame = CGRectMake(xValue, VIEW_PADDING, VIEW_DIMENSIONS, VIEW_DIMENSIONS);
        [scroller addSubview:view];
        xValue += VIEW_DIMENSIONS+VIEW_PADDING;
    }

    [scroller setContentSize:CGSizeMake(xValue+VIEWS_OFFSET, self.frame.size.height)];

    // 6 - if an initial view is defined, center the scroller on it
    if ([self.delegate respondsToSelector:@selector(initialViewIndexForHorizontalScroller:)])
    {
        int initialView = [self.delegate initialViewIndexForHorizontalScroller:self];
        [scroller setContentOffset:CGPointMake(initialView*(VIEW_DIMENSIONS+(*VIEW_PADDING)), ) animated:YES];
    }
}

上述代码的解释为:

  1. 如果不存在委托,那么不做任何处理直接返回。
  2. 将之前加载到scroll view中的子view全部删除。
  3. 所有view的位置起点都已经规定,现在的值是100,你可以通过修改#DEFINE定义来修改此值。
  4. HorizontalScroller去询问委托挨个将所有view进行水平放置。
  5. 一旦所有的view放置完成了,将scroll view的内容区域设定为所有view的范围,这样就能表现所有的专辑信息了。
  6. HorizontalScroller检查用户是否规定了初始化的专辑封面,如果有就将scroll view offset到其位置首个就展现他。默认为展现第一个view。

当数据信息被修改后你需要执行reload方法。同时当增加一个专辑信息(view)的时候你也需要调用这个方法。将下面的代码复制到HorizontalScroller.m文件中。

- (void)didMoveToSuperview
{
    [self reload];
}

当某个view被加入到其他view中被当做subview的时候didMoveToSuperview消息会被触发。这也是重新加载scroller的时刻。

HorizontalScroller的最后一个步骤是确保你正在观看的专辑是被放在scroll view的正中间。为了实现这个,你需要做一些计算。

再次进入HorizontalScroller.m文件,拷贝如下代码:

- (void)centerCurrentView
{
    ) + VIEW_PADDING;
    *VIEW_PADDING));
    xFinal = viewIndex * (VIEW_DIMENSIONS+(*VIEW_PADDING));
    [scroller setContentOffset:CGPointMake(xFinal,) animated:YES];
    [self.delegate horizontalScroller:self clickedViewAtIndex:viewIndex];
}

上述代码代码将view置于正中间。最后一行非常重要:一旦view被居中,你需要通知委托被选view已经发生了改变。

为了察觉到用户托转==拖拉完成,需要覆盖付下几个UIScrollViewDelegate的方法

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    if (!decelerate)
    {
        [self centerCurrentView];
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    [self centerCurrentView];
}

scrollViewDidEndDragging:willDecelerate:方法告诉委托你已经完成拖曳。当scroll view还处于滚动的时候decelerate参数为true。当scroll 动作全部完成后,系统会调用scrollViewDidEndDecelerating方法。这两种场景下我们都需要做view 居中操作。

HorizontalScroller已经准备好去使用了!查看现有的代码,我们发现一句关于Album或者AlbumView类相关的信息都没有。这非常好,这说明我们这个类是独立的课重用的。

Build你的工程,看有什么错误没有?

现在HorizontalScroller已经完成,现在可以去使用它了,打开ViewController.m增加如下imports:

#import "HorizontalScroller.h"
#import "AlbumView.h"

增加HorizontalScrollerDelegate协议

@interface ViewController ()<UITableViewDataSource, UITableViewDelegate, HorizontalScrollerDelegate>

增加如下对应的实例变量:

现在你可以实现你的那些委托方法了;你肯定对于这么少的几行代码就实现了委托方法的功能而感到吃惊。

将下面代码加入ViewController.m:

#pragma mark - HorizontalScrollerDelegate methods
- (void)horizontalScroller:(HorizontalScroller *)scroller clickedViewAtIndex:(int)index
{
    currentAlbumIndex = index;
    [self showDataForAlbumAtIndex:index];
}

这个方法将当前view的index保存起来,并将所通过showDataForAlbumAtIndex:来准备所需要用于显示的数据。

注意:一般在实践中都会讲方法放在#pragma mark命令行下面。编译器会忽视这个命令行但是如你你可以通过xcode来快速的查看某个方法的详细信息。

接着增加如下代码

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

这个方法就是协议方法用于返回在scroll View中存在多少个view。既然scroll view会覆盖所有的专辑数据,所以返回专辑数组的长度即可。

现在,增加如下代码:

- (UIView*)horizontalScroller:(HorizontalScroller*)scroller viewAtIndex:(int)index
{
    Album *album = allAlbums[index];
    , , , ) albumCover:album.coverUrl];
}

这里讲创建AlbumView对象传给HorizontalScroller对象。

就是这样叼!只用三个段的方法就能去完美的显示一个漂亮的水平的scroller。

对,你还需要正真的去创建scroller 并且放到主View中,但是在这之前我们需要做:

- (void)reloadScroller
{
    allAlbums = [[LibraryAPI sharedInstance] getAlbums];
    ) currentAlbumIndex = ;
    ;
    [scroller reload];

    [self showDataForAlbumAtIndex:currentAlbumIndex];
}

这个方法通过LibraryAPI加载专辑数据,然后当当前view的inde是否小于0则认为没有view被选择,那么第一个专辑将会被显示,否则最后一个专辑将会被显示。

现在在 [self showDataForAlbumAtIndex:0];代码行之前初始化scroller:

    scroller = [[HorizontalScroller alloc] initWithFrame:CGRectMake(, , self.view.frame.size.width, )];
    scroller.backgroundColor = [UIColor colorWithRed:];
    scroller.delegate = self;
    [self.view addSubview:scroller];

    [self reloadScroller];

上述代码简单的创建了一个HorizontalScroller实例,设定了背景颜色和委托,并将scroller增加到了主view上,最后加载自己的字view去显示专辑信息。

注意:如果一个协议变得太过庞大并且充斥了过多的方法,你应该考虑将他分解为几个小的协议。UITableViewDelegate和UITableViewDataSource就是一不错的例子,

再一次运行应用,应该能够显示出专辑scroller了:

饿~等等。水平的scroller已经显示,但是封面呢?

是的--你还没有实现下载封面饿代码。既然LibraryAP是与服务器连接的对象,那么久需要在其中增加新的方法来完成这一个动作,但是在做之前需要考虑如下内容:

  1. AlbumView不应该和LibraryAPI有直接联系,你不应该将表现逻辑和通信逻辑像混杂。
  2. 基于同样的理由,LibraryAPI不应该知道任何AlbumView的任何细节。
  3. LibraryAPI当封面一旦下载成功需要及时通知AlbumView去显示封面。

听起来是不是很难?不要绝望,你讲通过学习Observer模式来做到这个功能:]

The Observer Pattern 观察者模式

在观察者模式中,一个对象会在状态发生改变的时候去通知另外一个对象。对象之前不需要知道对方的存在--这样就形成了脱钩的设计。这种模式必须通常用于当某个属性发生改变的时候去通知对此属性关心的对象。

通常的做法是讲一个观察者对象注册到另外一个对象的某个状态属性上。当状态属性值发生变化的时候哦,所有观察者对象就会得到通知。Apple的Push Notification服务就是一个全局的例子。

如果你想映射到MVC概念时,你需要允许Model对象去和view对象进行通信,但是两者不能直接联系。这样观察者模式就会派上用场了。

Cocoa 实现观察者模式有两个友好的方法:Notifications和Key-Value Observing (KVO)。

Notifications 通知

不要将Push和本地notifications搞错,Notifications是基于 subscribe-and-publish 模型的,它允许一个对象(出版者)去想另外一个对象(订阅者/监听者)发送消息。出版者永远不需要知道任何关于订阅者的信息。

Notifications被Apple大量运用。例如,当键盘显示/关闭 系统将发送UIKeyboardWillShowNotification/UIKeyboardWillHideNotification通知,当你的应用进入后台运行的时候,系统将发送UIApplicationDidEnterBackgroundNotification通知。

注意:打开UIApplication.h文件,在文件饿最后你将看到有超过20个的系统通知。

怎样使用通知

打开 AlbumView.m文件将下列代码复制:

- (id)initWithFrame:(CGRect)frame albumCover:(NSString*)albumCover
{
    self = [super initWithFrame:frame];
    if (self)
    {

        self.backgroundColor = [UIColor blackColor];
        // the coverImage has a 5 pixels margin from its frame
        coverImage = [[UIImageView alloc] initWithFrame:CGRectMake(, , frame.size.width-, frame.size.height-)];
        [self addSubview:coverImage];

        indicator = [[UIActivityIndicatorView alloc] init];
        indicator.center = self.center;
        indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;
        [indicator startAnimating];
        [self addSubview:indicator];

        [[NSNotificationCenter defaultCenter] postNotificationName:@"BLDownloadImageNotification"
                                                            object:self
                                                          userInfo:@{@"imageView":coverImage, @"coverUrl":albumCover}];
    }
    return self;
}

这条代码将会通过NSNotificationCenter这个单例进行通知的发送。通知中包含专辑图片视图和url。这个对于封面下载来说信息已经足够了。

将下面代码加入到LibraryAPI.m文件:

- (id)init
{
    self = [super init];
    if (self) {
        persistencyManager = [[PersistencyManager alloc] init];
        httpClient = [[HTTPClient alloc] init];
        isOnline = NO;
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadImage:) name:@"BLDownloadImageNotification" object:nil];
    }
    return self;
}

这是另外对应的代码:观察者。每一次Album'View类发出BLDownloadImageNotification通知的时候,当LibraryAPI已经注册了同样的通知,那么系统将会通知LibraryAPI。LibraryAPI对象将会执行downloadImage:方法作为回应。

然而,在你实现downloadImage:方法之前你必须记住当类销毁的时候务必将订阅者取消订阅。如果你没有这样做,通知可能会被发送给一个已经销毁的实例,这样就会导致应用的崩溃。

所以讲下述代码加入到LibraryAPI.m:

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

当这个类被销毁的时候,将会删除所有他注册过的通知。

还有一间事情需要处理。我们需要将下载完成后的封面保存到本地,这样就不用重复去下载了。

打开PersistencyManager.h增加如下两个方法声明:

- (void)saveImage:(UIImage*)image filename:(NSString*)filename;
- (UIImage*)getImage:(NSString*)filename;

接着去实现这两个方法:

- (void)saveImage:(UIImage*)image filename:(NSString*)filename
{
    filename = [NSHomeDirectory() stringByAppendingFormat:@"/Documents/%@", filename];
    NSData *data = UIImagePNGRepresentation(image);
    [data writeToFile:filename atomically:YES];
}

- (UIImage*)getImage:(NSString*)filename
{
    filename = [NSHomeDirectory() stringByAppendingFormat:@"/Documents/%@", filename];
    NSData *data = [NSData dataWithContentsOfFile:filename];
    return [UIImage imageWithData:data];
}

这个代码相当直观。下载好饿图片会被保存到Documents文件夹,当本地不存在相关图片信息的时候getImage:将会你会nil。

现在在网LibraryAPI增加如下代码:

- (void)downloadImage:(NSNotification*)notification
{

    UIImageView *imageView = notification.userInfo[@"imageView"];
    NSString *coverUrl = notification.userInfo[@"coverUrl"];

    imageView.image = [persistencyManager getImage:[coverUrl lastPathComponent]];

    if (imageView.image == nil)
    {

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, ), ^{
            UIImage *image = [httpClient downloadImage:coverUrl];

            dispatch_sync(dispatch_get_main_queue(), ^{
                imageView.image = image;
                [persistencyManager saveImage:image filename:[coverUrl lastPathComponent]];
            });
        });
    }
}
 

下面是代码的注释:

  1. downloadImage方法将会通知到达的时候被调用,所以方法会将通知对象作为参数进行接收。UIImageView 和 image URL信息都会从通知对象中获取。
  2. image将会从PersistencyManager对象中去获取之前被下载过图片文件。
  3. 如果图片之前没有被下载过,那么就会使用HTTPClient像远程发送下载请求。
  4. 当下载完成的时候,将会在imageView上显示他,并会使用PersistencyManager对象去保存为本地图片,以便下次使用。

你正在使用Facade模式来影藏下载图片的细节。这样通知发送者就不会关心到底这个图片是来自于网络还是来自于本地文件系统。

再一次的运行你的应用:

译者(http://www.cnblogs.com/ios123/)注:这里遇到两个问题:

1.系统禁止了网络访问

解决:

http://stackoverflow.com/questions/31254725/transport-security-has-blocked-a-cleartext-http

打开info.list 增加如下选项,这个将会允许所有的网络连接,你也可以单独指定那些网络类型是允许的,这样比较安全。

2.所提供的图片URL可能由于墙或者资源过期已经不存在,无法浏览器打开。

解决:替换为其他资源:其他信息没有变,只是将图片URL变更了。

- (id)init
{
    self = [super init];
    if (self) {
        // a dummy list of albums
        albums = [NSMutableArray arrayWithArray:
                  @[[[Album alloc] initWithTitle:"],
                    [[Album alloc] initWithTitle:"],
                    [[Album alloc] initWithTitle:"],
                    [[Album alloc] initWithTitle:"],
                    [[Album alloc] initWithTitle:"]]];
    }
    return self;
}

显示如下:

关闭你的应用然后再次运行。发现在线应该没有图片延迟显示的现象,这是因为我们已经将这些图片保存在本地了。你甚至可以关闭网络再来测试,依然可以显示图片。然后这时候你还会发现一个问题,为毛菊花还在不停的转?!

当你正在下载图片的时候你启动了菊花,但是你没有实现当图片下载完成后的菊花逻辑。你可以设定一个通知,但是你也可以使用另外一个观察者模式:KVO。

Key-Value Observing (KVO)

在KVO中,一个对象可以去观察某个指定的属性的变化;不论这个属性属于自己的还是其他的对象。如果你感兴趣,你可以阅读Apple’s KVO Programming Guide.

怎样使用KVO模式

如前所述,KVO机制允许一个对象去观察某个属性的变化。在我们的场景中,你可以使用KVO去观察UIImageView的image属性是否已经保存了图片信息。

打开AlbumView.m文件,在initWithFrame:albumCover:方法中增加如下代码:

        [indicator startAnimating];
        [self addSubview:indicator];

        [[NSNotificationCenter defaultCenter] postNotificationName:@"BLDownloadImageNotification"
                                                            object:self
                                                          userInfo:@{@"imageView":coverImage, @"coverUrl":albumCover}];

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

这里将自身对象注册为观察者,将coverImage对象饿image属性作为被观察对象。

你也需要在观察者对象被销毁的时候取消注册:

- (void)dealloc
{
    [coverImage removeObserver:self forKeyPath:@"image"];
}
 

接下来在观察者对象中加入如下代码,也就是在AlbumView.m文件中:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"image"])
    {
        [indicator stopAnimating];
    }
}

你必须在每一个作为观察者身份的对象内实现该方法系统将执行在观察属性发生变化的时候去执行观察者的词此方法。在上述代码中,当image内值发生变化的时候,命令菊花停止转动。这样当image被下载完成后菊花就停止工作(转动)了。

再一次试一下你的应用:

注意:一定要时刻记住当观察者销毁的时候去取消它的订阅,否则你的应用汇由于向不存在的对象推送通知而崩溃的。

如果你运行了一会你的应用然后关掉了它,当再次打开应用的时候你会发现最后一次你查看的专辑并不在第一个位置。

为了去解决这个问题,你需要下一个模式来解:备忘录模式。

The Memento Pattern 备忘录模式

备忘录模式可以将一个对象的内部状态抓取到并将其保存在外部。换句话说,它将在某个地方保存你的信息。之后,再将外部状态信息重新设定到你的对象中,而且是在不破坏封装的情况下,这些对象的属性该是私有的还是私有的。

怎样使用备忘录模式

将下面两个方法加入到ViewController.m文件中:

- (void)saveCurrentState
{
    // When the user leaves the app and then comes back again, he wants it to be in the exact same state
    // he left it. In order to do this we need to save the currently displayed album.
    // Since it's only one piece of information we can use NSUserDefaults.
    [[NSUserDefaults standardUserDefaults] setInteger:currentAlbumIndex forKey:@"currentAlbumIndex"];
}

- (void)loadPreviousState
{
    currentAlbumIndex = [[NSUserDefaults standardUserDefaults] integerForKey:@"currentAlbumIndex"];
    [self showDataForAlbumAtIndex:currentAlbumIndex];
}

saveCurrentState方法将现在的专辑index保存进NSUserDefaults - NSUserDefaults 是iOS提供的一个标准的数据存储器,用于保存用于的设置和其他数据。

loadPreviousState 方法将会加载前一次保存的index。

现在,将下面的代码加入到ViewController.m文件的viewDidLoad方法中,要在scroller初始化之前:

 [self loadPreviousState];

当用于启动的时候这个方法将前一次保存的信息读取出被设定到对应的私有属性上。但是什么时候去保存这个信息呢?你讲使用通知机制去完成这个动作。当应用进入后台运行的时候,iOS系统会发送UIApplicationDidEnterBackgroundNotification 通知。你只要使用此通知去调用saveCurrentState方法即可。是不是很方便?

将下面的代码加入到viewDidLoad方法饿末尾处:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(saveCurrentState) name:UIApplicationDidEnterBackgroundNotification object:nil];

现在,当应用即将进入后台的时候,ViewController将通过saveCurrentState方法自动保存现在的状态。

Now, add the following code:

现在,增加下面的代码:

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

这将保证当你ViewController被销毁的时候将其取消观察者身份。

再一次build应用并启动它。点击某个专辑,然后通过Command+Shift+H 快捷键(如果你使用的模拟器的话)让应用进入后台,然后在关闭你的应用。再一次去打开应用,是不是应用直接显示你上次查看的专辑?

我们看一下好像专辑的信息是对的,但是好像scroller上的专辑图片依然保持在第一个,怎么回事情?

这就是由于我们没有实现可选方法initialViewIndexForHorizontalScroller:的原因!如果我们没有实现这个委托方法,ViewController将在初始化的时候一直将第一个作为默认。

所以解决这个问题的方法就是在ViewController.m文件中加入如下代码:

- (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller *)scroller
{
    return currentAlbumIndex;
}

现在HorizontalScroller第一个view将会按照currentAlbumIndex所保存的index来显示了。

再一次加载你的应用,看是不是正常了:

如果你查看一下 PersistencyManager的init方法,你会发现专辑数据是被硬编码的,每一次 PersistencyManager对象生成的时候都会创建。如果我们将这些信息放在一个列表中保存在文件中会更加好一些。怎样做呢?

我们可以通过plist的方式将专辑信息存放奥plist文件中当专辑实例创建的时候在读取它,但这并不是最好的选择,因为闷闷需要为专辑信息的保存定制额外的代码,想象一下如果接下来我们又要展示电影信息怎么办,由于属性的不同,我们还得重新为电影编写这些保存逻辑代码。

此外,你不能存储私有属性,这就是为什么Apple创建了Archiving(归档)机制。

Archiving 归档

Apple的另外一个实现备忘录模式的例子就是归档。他将一个对象转换成一个流,它在保存和还原的视乎不需要对象去将私有属性暴露给外部对象。你可以通过阅读 Apple’s Archives and Serializations Programming Guide来了解更多。

怎样使用归档

第一步,你需要将Album的声明做出修改,让它遵守NSCoding协议。打开 Album.h文件,按照如下修改:

@interface Album : NSObject <NSCoding>

增加下述方法到Album.m:

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:self.year forKey:@"year"];
    [aCoder encodeObject:self.title forKey:@"album"];
    [aCoder encodeObject:self.artist forKey:@"artist"];
    [aCoder encodeObject:self.coverUrl forKey:@"cover_url"];
    [aCoder encodeObject:self.genre forKey:@"genre"];
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if (self)
    {
        _year = [aDecoder decodeObjectForKey:@"year"];
        _title = [aDecoder decodeObjectForKey:@"album"];
        _artist = [aDecoder decodeObjectForKey:@"artist"];
        _coverUrl = [aDecoder decodeObjectForKey:@"cover_url"];
        _genre = [aDecoder decodeObjectForKey:@"genre"];
    }
    return self;
}

当你归档类的对象的时候使用encodeWithCoder:方法。相反的,你可以使用initWithCoder:方法来创建Album对象。简单而又强大。

现在Album类已经具有归档的能力了,增加代码来实际操作保存和加载专辑数据的工作。

将下述方法原型加入到PersistencyManager.h:

- (void)saveAlbums;

他将负责保存专辑信息的工作。

现在,增加实现到PersistencyManager.m:

- (void)saveAlbums
{
    NSString *filename = [NSHomeDirectory() stringByAppendingString:@"/Documents/albums.bin"];
    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:albums];
    [data writeToFile:filename atomically:YES];
}

NSKeyedArchiver类将专辑数组归档到名为albums.bin的文件中。

当你归档的对象中包含其他对象的时候,归档机制也会自动递归地归档子对象和任何子对象的子对象。。。。。。

在这个实例中,归档对象为albums数组,数组中保存着Album实例。既然NSArray和Album都支持NSCopying接口,在数组中保存的实例都会自动归档。

现在将PersistencyManager.m的初始化方法替换如下:

- (id)init
{
    self = [super init];
    if (self) {
        NSData *data = [NSData dataWithContentsOfFile:[NSHomeDirectory() stringByAppendingString:@"/Documents/albums.bin"]];
        albums = [NSKeyedUnarchiver unarchiveObjectWithData:data];
        if (albums == nil)
        {
            albums = [NSMutableArray arrayWithArray:
                      @[[[Album alloc] initWithTitle:"],
                        [[Album alloc] initWithTitle:"],
                        [[Album alloc] initWithTitle:"],
                        [[Album alloc] initWithTitle:"],
                        [[Album alloc] initWithTitle:"]]];
            [self saveAlbums];
        }
    }
    return self;
} 

在新的代码中,NSKeyedUnarchiver从文件中将专辑数据加载,如果数据不存在,将创建专辑数据并立即去保存它,以便下次应用启动的时候去从归档文件中加载。

你或许也想每次当应用转入后台的时候讲专辑数据保存。现在看来并没有什么必要,但是当你的应用后续有了修改专辑信息的时候我想就有必要了。

将下述代码增加到LibraryAPI.h文件

- (void)saveAlbums;

既然主应用都是通过LibraryAPI来和服务接触的,所以我们将让PersitencyManager去保存专辑信息。

在LibraryAPI.m文件中增加如下代码:

- (void)saveAlbums
{
    [persistencyManager saveAlbums];
}

这个代码简单的将数据传递给PersistencyMangaer。

下述代码增加到ViewController.m的saveCurrentState方法末尾:

[[LibraryAPI sharedInstance] saveAlbums];

上述代码在ViewController保存应用状态同时使用LibraryAPI去触发保存专辑数据。

再一次的运行应用。

很不辛,这个修改很难在应用上做检查,你可以通过观察应用在模拟器上的文件目录中查看是否生成了数据文件。

译者(http://www.cnblogs.com/ios123/):我的路径为:可能UUID会不一样。

/Users/apple/Library/Developer/CoreSimulator/Devices/8E2826AC-C429-4A39-B0EE-C261E155E191/data/Containers/Data/Application/7B2349E8-915D-4D7E-8D3E-11114B0E66A9/Documents/albums.bin
对于Mac上拷贝文件路径是不是很痛苦,你可以参考我的另外一篇博文:[how to]简单易用的拷贝Mac文件路径方法
 

除了修改专辑数据外,你不像在library中增加一个删除专辑的功能吗?或者,你不想增加一个回退删除操作的功能吗?

这些需求为我们接下来介绍的另外一个模式提供了绝好的机会:命令模式。

The Command Pattern 命令行模式

命令行设计模式将一个request或者action压缩进一个Object中。相比较松散的一个request被压缩的request将会非常灵活,它可以在对象间相互传递,延迟存储和动态修改,或者将其放入一个队列中进行调度。Apple的Target-Action机制和调用就是基于此模式的设计实现。

在Apple的官方文档上你能获取到更多关于Target-Action的信息,但是对于调度来说是使用的NSInvocationclass类,它包含一个目标对象,一个方法和一些参数。这个对象可以被动态修并执行。Target-Action可以说是讲解命令行设计模式的最佳例子。他将命令发出对象和接受对象完美的解耦并且可以处理一个或者一组请求。

怎样使用命令行模式

在你运用actions的之前,你需要让框架来知道如何处理undoing action。所以你必须定义个UIToolBar 和NSMutableArray来存undo栈。

将如下代码加入ViewController.m的extension中:

   UIToolbar *toolbar;
    // We will use this array as a stack to push and pop operation for the undo option
    NSMutableArray *undoStack;

这个toobar将会显示一个按钮来承载新的actions,而undoStack将会作为命令行队列。

将下属代码加入到viewDidLoad:方法开始处

- (void)viewDidLoad
{
    [super viewDidLoad];

    toolbar = [[UIToolbar alloc] init];
    UIBarButtonItem *undoItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemUndo target:self action:@selector(undoAction)];
    undoItem.enabled = NO;
    UIBarButtonItem *space = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
    UIBarButtonItem *delete = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(deleteAlbum)];
    [toolbar setItems:@[undoItem,space,delete]];
    [self.view addSubview:toolbar];
    undoStack = [[NSMutableArray alloc] init];

    //1. 修改背景色
    self.view.backgroundColor = [UIColor colorWithRed:];
    currentAlbumIndex = ;

上述代码创建了一个带有两个按钮的toolbar 。同时创建了一个undo栈。undo按钮一开始会处于无效状态,因为undo栈中没有任何东西。

另外注意到,toolbar并没有做任何的frame上的设定,因为viewDidLoad并不是所有的视图frame设定的最后机会点。我们可以通过下面的代码来初始化toolbar的frame:

- (void)viewWillLayoutSubviews
{
    toolbar.frame = CGRectMake(, self.view.frame.size.height-, self.view.frame.size.width, );
    dataTable.frame = CGRectMake(, , self.view.frame.size.width, self.view.frame.size.height - );
}

我们往ViewController.m文件中增加三个方法,这三个方法分别去处理专辑的add, delete, and undo.

第一个方法是去处理专辑增加:

- (void)addAlbum:(Album*)album atIndex:(int)index
{
    [[LibraryAPI sharedInstance] addAlbum:album atIndex:index];
    currentAlbumIndex = index;
    [self reloadScroller];
}

在这里你增加了一个专辑,将现在index设定为此专辑index,并且重新reload了scroller。

接下来实现删除方法:

- (void)deleteAlbum
{

    Album *deletedAlbum = allAlbums[currentAlbumIndex];

    NSMethodSignature *sig = [self methodSignatureForSelector:@selector(addAlbum:atIndex:)];
    NSInvocation *undoAction = [NSInvocation invocationWithMethodSignature:sig];
    [undoAction setTarget:self];
    [undoAction setSelector:@selector(addAlbum:atIndex:)];
    [undoAction setArgument:&deletedAlbum atIndex:];
    [undoAction setArgument:&currentAlbumIndex atIndex:];
    [undoAction retainArguments];

    [undoStack addObject:undoAction];

    [[LibraryAPI sharedInstance] deleteAlbumAtIndex:currentAlbumIndex];
    [self reloadScroller];

    [toolbar.items[] setEnabled:YES];
}

下面是对代码的注释:

  1. 获取需要被删除的专辑的信息。
  2. 定义一个NSMethodSignature对象去创建NSInvocation对象,当用户决定回退删除操作的时候通过这个对象来实现。NSInvocation需要知道三件事情:selector(发送什么消息),target(消息的接受者是谁)和消息的参数。在这个例子中消息当决定回退删除操作时候回发送一个相反的方法(既增加),因为所谓回退即使将删除的专辑信息再次增加而已。
  3. 当undoAction对象呗穿件后你需要将它加入到undoStact栈中,这个action将会被加到数组的末尾,就如普通的栈一样。
  4. 使用LibraryAPI对象去删除专辑并从新加载scroller。
  5. 既然回退栈也就是undoStack中有值了那么久需要将undo按钮设置为可用。

注意 :在使用NSInvocation的时候你需要在心中记住以下几点:

  • 参数必须通过指针传入。
  • 参数其实index为2;0和1 已经被target和Selector做占据。
  • 如果参数有可能会被销毁,为了在调用时候不出现野指针参数的情况,需要调用retainArguments来保留参数实例的引用。

最后,增加undo action的方法:

- (void)undoAction
{
    )
    {
        NSInvocation *undoAction = [undoStack lastObject];
        [undoStack removeLastObject];
        [undoAction invoke];
    }

    )
    {
        [toolbar.items[] setEnabled:NO];
    }
}

undo 操作将会在undoStack数组中获取最后一条对象(模拟栈的特性)。这个对象一般都是NSInvocation并且可以通过invoke方法被调用。这将实施你之前在删除专辑时候所穿件的undo 命令,随后会将被删除的专辑信息重新加入到专辑列表中去。因为你也将undo 命令从栈弹出,所以我们需要再次检查栈是否为空,如果空则在没有其他undo actions能够被处理,所以讲undo 按钮重新设定为无效。

重新运行应用来测试undo机制,删除一两个专辑信息,然后按下Undo 按钮看会发生什么:

这也是测试在备忘录模式下做的专辑信息保存的好机会,现在,如果你删除了一个专辑,然后将应用转入后台,接着关闭应用,然后再一次启动应用的时候看一下被删除的应用是否已经不存在了。

去向何方?

这里有一份已经完成的工程代码:BlueLibrary-final

译者(http://www.cnblogs.com/ios123/):译者同样跟随着教程完成了工程,你也可以访问  https://github.com/xufeng79x/designPattern2 来获取

还有两个重要设计模式没有在这个教程项目中加入进去,但是非常重要,他们是: Abstract Factory (aka Class Cluster) 和 Chain of Responsibility (aka Responder Chain)。尽快阅读,开阔你的设计模式视野。

在这个教程中你应该明白了iOS设计模式的威力,他将复杂的任务使用直观的松耦合的语法去组织。已经学习了很多iOS的设计模式和概念了:Singleton, MVC, Delegation, Protocols, Facade, Observer, Memento, and Command。

现在你的代码是松耦合,可复用和高可读的,如果另外一个开发者阅读你的代码,他应该很快明白到底应用的逻辑和每一个类的作用。

并不是说每一行都要去运用设计模式。当你考虑去解决特定问题的时候稍微运用一下设计模式的知识,特别是在早期设计你的应用阶段。合理使用设计模式将会使你的开发生活简单高效,让你的代码更加清新可人。

[New learn] 设计模式的更多相关文章

  1. [New learn] 设计模式思考

    本文是对上文[New learn] 设计模式的思考总结 1.大框架 无论应用使用多少种设计模式和技巧,此模式都是应用的大框架.下图为本项目的基本架构图: 1.上图中大框架为经典的MVC模式. 2.Co ...

  2. [New learn]SDWebImage框架的基本使用

    代码:https://github.com/xufeng79x/SDWebImage 1.简介 SDWebImage是一个第三方框架,它能够帮助我们有效管理应用图片下载,沙盒保存和内存保存的任务.通过 ...

  3. PHP设计模式笔记三:三种基本设计模式(工厂模式、单例模式、注册树模式) -- Rango韩老师 http://www.imooc.com/learn/236

    一.工厂设计模式 index.php $db = IMooc\Factory::createDatabase(); 使用工厂类的静态方法直接创建一个dababase对象,当类名发生修改时,在工厂里修改 ...

  4. PHP设计模式笔记六:数据对象映射模式 -- Rango韩老师 http://www.imooc.com/learn/236

    数据对象映射模式 1.数据对象映射模式,是将对象和数据存储映射起来,对一个对象的操作会映射为对数据存储的操作 2.在代码中实现数据对象映射模式,我们将实现一个ORM类,将复杂的SQL语句映射成对象属性 ...

  5. PHP设计模式笔记九:装饰器模式 -- Rango韩老师 http://www.imooc.com/learn/236

    装饰器模式(Decorator) 概述 1.装饰器模式可以动态地添加修改类的功能 2.一个类提供了一项功能,如果要在修改并添加额外的功能,传统的编程模式,需要写一个子类继承它,并重新实现类的方法 3. ...

  6. PHP设计模式笔记八:原型模式 -- Rango韩老师 http://www.imooc.com/learn/236

    原型模式 概述: 1.与工厂模式作用类似,都是用来创建对象 2.与工厂模式的实现不同,原型模式是先创建好一个原型对象,然后通过clone原型对象来创建新的对象,这样就免去了类创建时重复的初始化操作 3 ...

  7. PHP设计模式笔记七:观察者模式 -- Rango韩老师 http://www.imooc.com/learn/236

    观察者模式 概述: 1.观察者模式(Observer),当一个对象状态发生改变时,依赖他的对象全部会收到通知,并自动更新 2.场景:一个事件发生后,要执行一连串更新操作,传统的编程方式,就是在事件的代 ...

  8. PHP设计模式笔记五:策略模式 -- Rango韩老师 http://www.imooc.com/learn/236

    策略模式 1.概述:策略模式,将一组特定的行为和算法封装成类,以适应某些特定的上下文环境,这种模式称为策略模式 例如:一个电商网站系统,针对男性女性用户要各自跳转到不同的商品类目,并且所有广告位展示不 ...

  9. PHP设计模式笔记四:适配器模式 -- Rango韩老师 http://www.imooc.com/learn/236

    适配器模式 1.适配器模式,可以将截然不同的函数接口封装成统一的API 2.实际应用举例,PHP的数据库操作有mysql.mysqli.pdo三种,可以用适配器模式统一成一致,类似的场景还有cache ...

随机推荐

  1. [NOIP2016] 天天爱跑步 桶 + DFS

    ---题面--- 题解: 很久以前就想写了,一直没敢做,,,不过今天写完没怎么调就过了还是很开心的. 首先我们观察到跑步的人数是很多的,要一条一条的遍历显然是无法承受的,因此我们要考虑更加优美的方法. ...

  2. 使用C#解析并运行JavaScript代码

    如果想在C#编程中解析并运行JavaScript代码,常见的方式有两种: 利用COM组件“Microsoft Script Control”,可参见:C#使用技巧之调用JS脚本方法一 利用JScrip ...

  3. bzoj 2654 tree 二分+kruskal

    tree Time Limit: 30 Sec  Memory Limit: 512 MBSubmit: 2739  Solved: 1126[Submit][Status][Discuss] Des ...

  4. Hadoop Yarn事件处理框架源码分析

    由于想在项目中使用类似yarn的事件处理机制,就看了实现.主要是由Dispatcher.java,EventHandler.java,Service.java这3个类撑起来的. 在事件处理之前,先注册 ...

  5. js ejs for语句的第二种遍历用法

    var A = {a:1,b:2,c:3,d:"hello world"}; for(var k in A) { console.log(k,A[k]); var h = new ...

  6. j2ee 项目部署指引

    j2ee相关的项目一般是web工程或java application,部署到linux服务器上,本文结合自己的经验.教训,总结下部署的过程. 一.准备阶段 部署前要做的事情: 1.明确自己的产品都包含 ...

  7. 详解ListView加载网络图片的优化

    我们来了解一些ListView在加载大量网络图片的时候存在的常见问题: 1.性能问题,ListView的滑动有卡顿,不流畅,造成非常糟糕的用户体验. 2.图片的错位问题. 3.图片太大,加载Bitma ...

  8. 洛谷 P3709 大爷的字符串题

    https://www.luogu.org/problem/show?pid=3709 题目背景 在那遥远的西南有一所学校 /*被和谐部分*/ 然后去参加该省省选虐场 然后某蒟蒻不会做,所以也出了一个 ...

  9. 2050年这些职业将逐渐被AI(人工智能)取代

    耳熟能详的人工智能   深蓝Deep Blue是美国IBM公司生产的一台超级国际象棋电脑,重1270公斤,有32个大脑(微处理器),每秒钟可以计算2亿步."深蓝”输入了一百多年来优秀棋手的对 ...

  10. centos6.8使用脚本一键搭建apache+svn服务

    服务器环境: 脚本如下: #!/bin/bash yum install wget -y mv /etc/yum.repos.d/*.repo /tmp wget -O /etc/yum.repos. ...