iOS开发-一个例子学习iOS中的常见设计模式

原文iOS Design Patterns
iOS设计模式 ,你可能已经听说过这个术语,但你知道这意味着什么吗?虽然大多数的开发人员认为设计模式是非常重要的,但目前关于这个问题的文章不是很多,我们的开发人员有时写代码有时不注重设计模式。
设计模式是软件设计常见问题的可重用的解决方案。它们帮助您编写容易理解和可重用的模块。他们还帮助您创建松散耦合的代码,这个可以方便的在你的代码中更改或替换组件。
如果你刚接触设计模式,那么我有一个好消息告诉你!首先,你在创建iOS工程中Cocoa框架已经使用了大量的iOS设计模式,很容易理解和重用。其次,本教程将带您快速了解Cocoa框架中所有重要的(和不那么重要)的iOS的设计模式。


本教程分为几个部分,每个设计模式一个部分。在每一节,你会读到以下的内容:

  • 设计模式是什么。
  • 为什么要使用它。
  • 如何使用它,并在适当情况下,使用该模式常见的陷阱需要提防。

在本教程中,您将创建一个音乐库的应用程序,可以显示您的相册及相关信息。
在开发这个应用程序的过程中,你会逐渐熟悉最常见的Cocoa设计模式:

  • 对象创建类型:单例模式、抽象工厂模式。
  • 结构类型:MVC、装饰者模式、适配器模式、门面模式、合成模式。
  • 行为类型:观察者模式、备忘录模式、责任链模式、命令模式。

不要误以为这是关于理论的文章,在你的音乐库应用中,会使用大多数设计模式,你的应用在结束时是这样子的。
最终效果


开始

下载这个项目,解压,用Xcode打开BlueLibrary.xcodeproj

这里没有很多东西,仅仅有一个默认的ViewController和一个简单的没有具体实现的HTTP客户端类。

你知道吗,只要你创建新的Xcode项目,你的代码已经实现了设计模式的。MVC,委托协议,单例 !

在你学习第一种设计模式,必须创建两个类来保存并显示专辑数据。
导航至File->New->File(或者简单的 Command+N),选择 iOS > Cocoa Touch 然后选择Objective-C class点击下一步,选择Album作为类名,父类是NSObject,选择下一步创建。

打开Album.h,在@interface@end中间添加属性和方法。

@property (nonatomic, copy, readonly) NSString *title, *artist, *genre, *coverUrl, *year;

- (id)initWithTitle:(NSString*)title artist:(NSString*)artist coverUrl:(NSString*)coverUrl year:(NSString*)year;

现在所有的属性都是只读的,一旦创建Album实例就没有必要改变。
这个方法是对象初始化函数。当你创建一个新的Albun实例时,你要传递title、artist、coverUrl、year属性。
打开Album.m,在@implementation和 @end中间添加下列代码:

- (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;
}

这很无趣,仅仅是一个创建Album实例的初始化方法。
再次,导航至File->New->File(或者简单的 Command+N),选择 iOS > Cocoa Touch 然后选择Objective-C class点击下一步,选择AlbumView作为类名,父类是UIView,选择下一步创建。

常见键盘快捷键
创建新文件:Command+N
创建一个组:Command+Option+N
编译: Command+B
运行:Command+R

打开AlbumView.h,在@interface@end中间添加方法。

- (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(5, 5, frame.size.width-10, frame.size.height-10)];
        [self addSubview:coverImage];

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

你在这里首先注意到的是,有一个名为coverImage实例变量。此变量代表专辑封面图片。第二个变量是indicator,代表正在下载的菊花。

在实现文件中,初始背景颜色为黑色。
在你设置的背景为黑色初始化的实施,创建并添加图像和菊花。

注:想知道为什么私有变量在实现文件中定义,而不是在接口文件?这是因为AlbumView类外没有类需要知道这些变量的存在,因为他们只用在类的内部功能的实现。如果你正在创建一个库或框架,其他开发者使用这种约定是非常重要的。

Command+B 编译项目,一切正常,学习第一个设计模式。


MVC-设计模式之王

MVC
模型 - 视图 - 控制器(Model View Controller)是Cocoa框架的基石之一,无疑是所有设计模式最常用的。它在你的应用中根据对象的作用进行分类,也鼓励基于类型角色的代码解耦。

  • Model:保存您的应用程序数据和定义了如何操纵它的对象。例如,在应用程序中的模型是你的相册Album类。
  • View:是负责模型数据的可视化和用户可以交互控件的对象,包括了所有的UIViews及其子类。在应用程序中View是AlbumView类。
  • Controller:控制器是协调所有工作的中枢。它从模型中获得数据并操纵它、用View展示、监听事件。在应用程序中是ViewController类。

一个很好的实现在应用这种设计模式意味着每个对象属于其中一个组。控制器到模型视图之间的通信可以用下面图来最好的描述:
MVC图

模型通知控制器更改数据,反过来,控制器更新视图中的数据。然后视图可以通知控制器用户执行的操作,并且控制器将在必要时更新模型或检索任何请求数据。

你可能想知道为什么你不能抛弃控制器,并实现视图和模型在同一个类,因为这似乎更容易。

这一切都归结于代码分离和可重用性。理想情况下,视图应与模型完全分离。如果视图不依赖于模型的特定实现,那么它可以被重用于不同的模型以呈现一些其他数据。

例如,如果将来您还想要将电影或书籍添加到您的图书馆,您仍然可以使用相同的AlbumView来显示您的电影和书籍对象。此外,如果你想创建一个与专辑有关的新项目,你可以简单地重用你的Album类,因为它不依赖于任何视图。这是MVC的力量!

怎样使用MVC模式

首先,需要确保项目中的每个类都是Controller,Model或View; 不要在一个类中组合两个角色的功能。 通过创建一个Album类和一个AlbumView类,你已经做了很好的工作。
其次,为了确保您符合这种工作方法,您应该创建三个文件夹来保存您的代码,每个类别一个。
导航到File\New\Group(或按Command + Option + N),并命名组模型。 重复相同的过程以创建视图和控制器组。

现在将Album.h和Album.m拖动到模型组。 将AlbumView.h和AlbumView.m拖动到View组,最后将ViewController.h和ViewController.m拖动到Controller组。

此时项目结构应如下所示:
项目结构图

你的项目已经看起来好多了,没有所有这些杂乱的文件。 显然你可以有其他组和类,但应用程序的核心包含在这三个文件夹。

现在你的组件是有组织的,你需要从某个地方获取专辑数据。 您将创建一个API类,以在您的代码中使用来管理数据 - 这提供了一个机会来讨论您的下一个设计模式 - 单例模式。

单例模式

单例模式确保对于给定类只存在一个实例,并且存在到该实例的全局访问点。 当第一次需要时,它通常使用延迟加载来创建单个实例。

苹果使用这种方法很多。 例如:[NSUserDefaults standardUserDefaults],[UIApplication sharedApplication],[UIScreen mainScreen],[NSFileManager defaultManager]全部返回一个单例对象。

你可能想知道为什么你关心一个类的超过多个实例的情况,因为感觉代码和内存很便宜,对吧?

在某些情况下,只有一个类的一个实例是有意义的。 例如,不需要有多个Logger实例,除非您想要一次写入多个日志文件。 或者,使用全局配置处理程序类:更容易实现对单个共享资源(例如配置文件)的线程安全访问,而不是允许许多类同时修改配置文件。

怎样使用单例模式

看下图:
单例模式

上图显示了一个具有单个属性(它是单个实例)的Logger类,以及两个方法:sharedInstance和init。
第一次发送sharedInstance消息时,属性实例尚未初始化,因此您创建一个类的新实例并返回一个引用。

下一次调用sharedInstance时,立即返回实例而不进行任何初始化。 这个逻辑承诺只有一个实例总是存在。

您将通过创建一个单例类来管理所有相册数据来实现此模式。

你会注意到在项目中有一个名为API的组; 这里是你将所有的类,将提供服务到你的应用程序。 在这个组里面创建一个新的类与iOS \ Cocoa Touch \ Objective-C类模板。 将类命名为LibraryAPI,并将其作为NSObject的子类。

打开LibraryAPI.h并将其内容替换为以下内容:

@interface LibraryAPI : NSObject

+ (LibraryAPI*)sharedInstance;

@end

现在转到Library API.m并在@implentation行后面插入此方法:

+ (LibraryAPI*)sharedInstance
{
    // 1
    static LibraryAPI *_sharedInstance = nil;

    // 2
    static dispatch_once_t oncePredicate;

    // 3
    dispatch_once(&oncePredicate, ^{
        _sharedInstance = [[LibraryAPI alloc] init];
    });
    return _sharedInstance;
}

在这个简短的方法有很多事情:

  1. 声明一个静态变量来保存你的类的实例,确保它可以在类中全局使用。
  2. 声明静态变量dispatch_once_t,它确保初始化代码只执行一次。
  3. 使用Grand Central Dispatch(GCD)执行初始化LibraryAPI实例的块。 这是单例设计模式的本质:初始化器从来不会在类被实例化时再次调用。

下一次调用sharedInstance时,dispatch_once块中的代码将不会被执行(因为它已经执行过一次),并且您接收到之前创建的LibraryAPI实例的引用。

您现在有一个单例对象作为管理专辑的入口点。 再进一步,创建一个类来处理你的持久性数据。
使用iOS \ Cocoa Touch \ Objective-C类模板在API组中创建一个新类。 将类命名为PersistencyManager,并将其作为NSObject的子类。

打开PersistencyManager.h。 将以下导入添加到文件顶部:

#import "Album.h"

接下来,在@interface行后面添加以下代码到PersistenceManager.h:

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

以上是处理相册数据所需的三种方法的接口。
打开PersistenceManager.m并在@implementation行正上方添加以下代码:

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

上面添加了一个类扩展,这是另一种方式来添加私有方法和变量到类,以便外部类不会知道它们。 在这里,你声明一个NSMutableArray来保存相册数据。 这个数组是可变的,以便您可以轻松添加和删除相册。

现在在@implementation行后面添加以下代码实现到PersistencyManager.m:

- (id)init
{
    self = [super init];
    if (self) {
        // a dummy list of albums
        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;
}

在init中,用五个示例专辑填充数组。 如果上述专辑不符合您的喜好,请随意用您喜欢的音乐取代。
现在将以下三个方法添加到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];
}

这些方法允许您获取,添加和删除相册。

构建你的项目只是为了确保一切仍然正确编译。

在这一点上,你可能想知道PersistencyManager类在哪里,因为它不是一个单例。 LibraryAPI和PersistencyManager之间的关系将在下一节中进一步探讨,您将在其中查看门面设计模式。

门面设计模式

门面设计模式

门面设计模式为复杂子系统提供单一接口。 不是将用户暴露给一组类及其API,而是只展示一个简单的统一API。

下图说明了这个概念:
[门面设计模式

API的用户完全不知道下面的复杂性。 这种模式是理想的,当使用大量的类,特别是当它们使用复杂或难以理解。

门面设计模式将使用系统的代码与您隐藏的类的接口和实现分离; 它还减少了外部代码对子系统内部工作的依赖性。 这对底层类的更改也是有用的,因为当类在后台改变时,门面上层类可以保留相同的API。
例如,如果您想要替换后端服务,那么您不必更改使用API的代码,因为它不会更改。

怎样使用门面模式

目前您有PersistencyManager在本地保存相册数据和HTTPClient来处理远程通信。 你项目中的其他类不应该意识到这个逻辑。

要实现此模式,只有LibraryAPI应该持有PersistencyManager和HTTPClient的实例。 然后,LibraryAPI将暴露一个简单的API来访问这些服务。

注意:通常,应用程序的生命周期中存在单例。 你不应该将单例中的太多强指针保存到其他对象,因为它们在应用程序关闭之前不会被释放。

门面模式

LibraryAPI将暴露给其他代码,但会从应用程序的其余部分隐藏HTTPClient和PersistencyManager复杂性。

打开LibraryAPI.h并将以下导入添加到文件的顶部:

#import "Album.h"

接下来,将以下方法定义添加到Library API.h:

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

现在,这些是你将暴露给其他类的方法。转到Library API.m并添加以下两个导入:

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

这将是您导入这些类的唯一的地方。 记住:您的API将是您的“复杂”系统的唯一访问点。

现在,通过类扩展(在@implementation行上面)添加一些私有变量:

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

isOnline确定是否应该更新服务器对相册列表所做的任何更改,例如添加或删除的相册。
现在需要通过init初始化这些变量。 将以下代码添加到LibraryAPI.m:

- (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 :。 该类首先在本地更新数据,然后如果有互联网连接,它会更新远程服务器。 这是门面的真正好处; 当你的系统外的一些类添加了一个新的专辑,它不知道 、并且不需要知道下面的复杂性。

注意:在为子系统中的类设计门面时,请记住,没有任何东西阻止客户端直接访问这些“隐藏”类。 不要吝啬防御性代码,不要假设所有的客户端都必须使用你的类,就像门面使用它们一样。

构建并运行您的应用程序。 你会看到一个令人难以置信的令人兴奋的空黑屏,像这样:
黑屏
你需要一些东西在屏幕上显示相册数据,这是你下一个设计模式的完美使用:装饰模式。

装饰模式

装饰器模式动态地将行为和责任添加到对象而不修改其代码。 这是一个替代子类化的地方,你通过用另一个对象包装它来修改类的行为。

在Objective-C中,有两种非常常见的模式实现:类(Category)和委托(Delegation)。

分类(Category)

分类是一种非常强大的机制,允许您在没有子类化的情况下将方法添加到现有类。 新方法在编译时添加,可以像扩展类的普通方法那样执行。 它与装饰器的经典定义略有不同,因为分类不包含它扩展的类的实例。

注意:除了扩展自己的类,你也可以添加任何Cocoa框架中类的方法!

怎样使用分类

想象一下,你想要在表视图中显示一个Album对象的情况:
Album对象
专辑名称从哪里来? Album是一个Model对象,所以它不在乎你如何呈现数据。 您将需要一些外部代码将此功能添加到Album类,但不直接修改类。
您将创建一个分类,它是Album的扩展名; 它将定义一个新的方法,返回一个可以很容易地与UITableViews一起使用的数据结构。

数据结构将如下所示:
数据结构

要将分类别添加到相册,导航到File \ New \ File …并选择Objective-C category template - 不要选择Objective-C类! 在分类字段中输入TableRepresentation,并在类别字段中输入Album。

注意:您注意到新文件的名称吗? Album + TableRepresentation表示您要扩展Album类。 这个约定很重要,因为它更容易阅读,并防止与您或其他人可能创建的其他类别的冲突。
转到Album + Table Representation.h并添加以下方法原型:

- (NSDictionary*)tr_tableRepresentation;

注意在方法名的开头有一个tr_,作为类别名称的缩写:TableRepresentation。 再次,这样的约定将有助于防止与其他方法的冲突!
转到Album + Table Representation.m并添加以下方法:

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

考虑一下这个模式有多么强大:

  • 您直接从相册使用属性。
  • 您已添加到Album类,但您尚未对其进行子类化。 如果你需要子类别的相册,你仍然可以这样做。
  • 这个简单的添加允许你返回一个相册的UITableView-ish表示,而不修改相册的代码。

苹果在基础类中使用了很多类别。 要看看他们如何做到这一点,打开NSString.h。 查找@interface NSString,您将看到类的定义以及三个类别:NSStringExtensionMethods,NSExtendedStringPropertyListParsing和NSStringDeprecated。 分类有助于保持方法的组织和分离。

委托(Delegation)

另一个装饰器设计模式,委托,是一个对象代表另一个对象或与另一个对象协同工作的机制。 例如,当您使用UITableView时,必须实现的方法之一是tableView:numberOfRowsInSection :

您不能期望UITableView知道您希望在每个部分中有多少行,因为这是应用程序特定的。 因此,计算每个部分中的行数的任务被传递给UITableView delegate。 这允许UITableView类独立于它显示的数据。

这里是一个大概的解释当你创建一个新的UITableView时发生了什么:

delegate

UITableView对象执行其显示表视图的任务。 然而,最终它将需要一些它没有的信息。 然后,它转向其代理,并发送一条消息,要求提供其他信息。 在Objective-C的委托模式实现中,类可以通过协议声明可选和必需的方法。 本教程稍后将介绍协议。

看起来更简单的是子类化一个对象并覆盖必要的方法,但考虑你只能基于一个类进行子类化。 如果你想让一个对象成为两个或多个其他对象的委托,你将不能通过子类化实现这一点。

注意:这是一个重要的模式。 Apple在大多数UIKit类中使用这种方法:UITableView,UITextView,UITextField,UIWebView,UIAlert,UIActionSheet,UICollectionView,UIPickerView,UIGestureRecognizer,UIScrollView等等。

怎样使用委托模式

转到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将符合UITableViewDataSource和UITableViewDelegate协议。 这样,UITableView可以绝对确定所需的方法是由其委托实现的。

接下来,将viewDidLoad:替换为此代码:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // 1
    self.view.backgroundColor = [UIColor colorWithRed:0.76f green:0.81f blue:0.87f alpha:1];
    currentAlbumIndex = 0;

    //2
    allAlbums = [[LibraryAPI sharedInstance] getAlbums];

    // 3
    // the uitableview that presents the album data
    dataTable = [[UITableView alloc] initWithFrame:CGRectMake(0, 120, self.view.frame.size.width, self.view.frame.size.height-120) style:UITableViewStyleGrouped];
    dataTable.delegate = self;
    dataTable.dataSource = self;
    dataTable.backgroundView = nil;
    [self.view addSubview:dataTable];
}

以下是上述代码的细分:
1. 将背景颜色更改为漂亮的海军蓝色。
2. 通过API获取所有相册的列表。 您不要直接使用PersistencyManager!
3. 这是您创建UITableView的位置。 您声明视图控制器是UITableView delegate/data source; 因此,UITableView所需的所有信息将由视图控制器提供。

现在,添加以下方法到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];

这会在应用启动时加载当前相册。 由于currentAlbumIndex之前设置为0,这将显示集合中的第一张专辑。
构建和运行您的项目; 您将遇到崩溃,并在调试控制台中显示以下异常:
异常

这里发生了什么? 您声明ViewController作为UITableView的委托和数据源。 但在这样做,你必须符合所有必需的方法tableView:numberOfRowsInSection:, 你还没有做到。

将以下代码添加到ViewController.m @implementation和@end行之间的任何地方:

- (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:创建并返回具有标题及其值的单元格。
构建并运行您的项目。 您的应用程式应该开始并显示以下画面:

tableView图像
这看起来相当不错。 但如果你回想起第一个图像显示完成的应用程序,在屏幕的顶部有一个水平卷轴在相册之间切换。 而不是编码一个单一目的的水平滚动,为什么不让它可重用于任何视图?

为了使这个视图可重用,关于它的内容的所有决定应该留给另一个对象:委托。 水平滚动器应该声明其委托实现的方法,以便使用滚动器,类似于UITableView委托方法的工作方式。 当我们讨论下一个设计模式时,我们将实现这一点。

适配器模式

适配器允许具有不兼容接口的类一起工作。 它将自身包裹在一个对象周围,并暴露一个标准接口来与该对象交互。

如果你熟悉适配器模式,那么你会注意到苹果以略有不同的方式实现它 - 苹果使用协议来完成这项工作。 您可能熟悉像UITableViewDelegate,UIScrollViewDelegate,NSCoding和NSCopying的协议。 作为示例,使用NSCopying协议,任何类都可以提供标准复制方法。

怎样使用适配器模式

前面提到的水平滚动条将如下所示:
水平滚动条

要开始实现它,右键单击Project Navigator中的View组,选择New File …并使用iOS \ Cocoa Touch \ Objective-C类模板创建一个类。 命名新类HorizontalScroller并使其从UIView子类。

打开HorizontalScroller.h并在@end行后插入以下代码:

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

这定义了一个名为HorizontalScrollerDelegate的协议,它继承自NSObject协议,与Objective-C类从其父类继承的方式相同。 遵循NSObject协议或符合自身符合NSObject协议的协议是一个好的习惯。 这允许您将由NSObject定义的消息发送到HorizontalScroller的委托。 你很快就会明白为什么这很重要。

您定义了委托将在@protocol和@end行之间实现的必需和可选方法。 所以添加以下协议方法:

@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;

这里有必要的和可选的方法。 必需的方法必须由委托来实现,并且通常包含该类绝对需要的一些数据。 在这种情况下,所需的详细信息是视图的数量,特定索引处的视图以及轻击视图时的行为。 这里的可选方法是初始视图; 如果没有实现,那么HorizontalScroller将默认使用第一个索引。

接下来,您需要从HorizontalScroller类定义中引用您的新委托。 但协议定义低于类定义,因此在这一点不可见。 那么你能怎么做呢?

解决方案是转发声明协议,以便编译器(和Xcode)知道这样的协议将可用。 为此,请在@interface行上面添加以下代码:

@protocol HorizontalScrollerDelegate;

仍然在HorizontalScroller.h中,在@interface和@end语句之间添加以下代码:

@property (weak) id<HorizontalScrollerDelegate> delegate;
 - (void)reload;

上面创建的属性的属性定义为weak。 这是必要的,以防止循环引用。 如果一个类保持一个强的指针到它的委托,并且委托保持一个强的指针回到合格的类,你的应用程序将泄漏内存,因为这两个类都不会释放分配给另一个的内存。
id意味着代理只能被赋予符合HorizontalScrollerDelegate的类,给你一些类型的安全性。
reload方法在UITableView中的reloadData之后调用; 它重新加载用于构造水平滚动条的所有数据。
使用以下代码替换HorizontalScroller.m的内容:

#import "HorizontalScroller.h"

// 1
#define VIEW_PADDING 10
#define VIEW_DIMENSIONS 100
#define VIEWS_OFFSET 100

// 2
@interface HorizontalScroller () <UIScrollViewDelegate>
@end

// 3
@implementation HorizontalScroller
{
    UIScrollView *scroller;
}

@end

依次考虑下面几个方面:
定义常量以便于在设计时修改布局。 视图在滚动器内的尺寸将为100 x 100,其包围矩形的边距为10。
HorizontalScroller符合UIScrollViewDelegate协议。 由于HorizontalScroller使用UIScrollView滚动相册封面,它需要知道用户事件,例如用户停止滚动时。
创建包含视图的滚动视图。

接下来,您需要实现初始化程序。 添加以下方法:

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
        scroller = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 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;
}

滚动视图完全填充HorizontalScrollerUITapGestureRecognizer检测滚动视图上的触摸,并检查是否已经点击了专辑封面。 如果是,它通知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
    for (int index=0; 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/2 + view.frame.size.width/2, 0) animated:YES];
            break;
        }
    }
}

作为参数传递的手势可让您通过locationInView提取位置。

接下来,调用delegate上的numberOfViewsForHorizontalScroller:。 HorizontalScroller实例没有有关委托的信息,除了知道它可以安全地发送此消息,因为委托必须符合HorizontalScrollerDelegate协议。

对于滚动视图中的每个视图,使用CGRectContainsPoint执行命中测试以查找已轻敲的视图。 当找到视图时,发送代理horizontalScroller:clickedViewAtIndex:消息。 在您跳出for循环之前,将滚动视图中的轻触视图居中。

现在添加以下代码重新加载滚动器:

- (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;
    for (int i=0; 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;
    }

    // 5
    [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+(2*VIEW_PADDING)), 0) animated:YES];
    }
}

逐步对代码进行解释:

  1. 如果没有委托,那么没有什么可做,你可以返回。
  2. 删除先前添加到滚动视图的所有子视图。
  3. 所有视图都从给定的偏移量开始定位。目前它是100,但它可以很容易调整通过更改文件的顶部的#DEFINE 值。
  4. Horizo​​ntalScroller一次请求一个视图的代理,并且它们用前面定义的填充水平地彼此相邻放置。
  5. 一旦所有视图都就位,设置滚动视图的内容偏移,以允许用户滚动浏览所有专辑封面。
  6. Horizo​​ntalScroller检查其委托是否响应initialViewIndexForHorizo​​ntalScroller:selector。这种检查是必要的,因为特定的协议方法是可选的。如果代理没有实现此方法,则使用0作为默认值。最后,这段代码设置滚动视图以居中由委托定义的初始视图。

当数据更改时执行重新加载。当您将Horizo​​ntalScroller添加到另一个视图时,还需要调用此方法。将以下代码添加到Horizo​​ntalScroller.m以包括后一种情况:

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

当它作为子视图添加到另一个视图时,didMoveToSuperview消息被发送到视图。 这是重新加载滚动条内容的正确时间。
HorizontalScroller拼图的最后一个部分是确保您正在查看的相册始终居中在滚动视图中。 为此,您需要在用户用手指拖动滚动视图时执行一些计算。

添加以下方法(再次到HorizontalScroller.m):

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

上述代码考虑了滚动视图的当前偏移以及视图的尺寸和填充,以便计算当前视图与中心的距离。 最后一行很重要:一旦视图居中,您就可以通知代理所选视图已更改。

要检测用户是否完成在滚动视图内的拖动,必须添加以下UIScrollViewDelegate方法:

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

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

scrollViewDidEndDragging:willDecelerate:当用户完成拖动时通知代理。 如果滚动视图尚未完全停止,则decelerationate参数为true。 当滚动操作结束时,系统调用scrollViewDidEndDecelerating。 在这两种情况下,我们应该调用新方法来使当前视图居中,因为用户拖动滚动视图后当前视图可能已更改。
您的HorizontalScroller已准备就绪! 浏览你刚才写的代码; 你会看到没有一个提到的Album或AlbumView类。 这是非常好的,因为这意味着新的滚动器是真正独立和可重用的。
构建您的项目,以确保一切正常编译。
现在,HorizontalScroller完成,是时候在你的应用程序中使用它。 打开ViewController.m并添加以下导入:

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

添加HorizontalScrollerDelegate到ViewController遵循的协议:

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

将水平滚动条的以下实例变量添加到类扩展:

HorizontalScroller *scroller;

现在你可以实现委托方法; 你会惊讶于几行代码如何实现很多功能。
将以下代码添加到ViewController.m:

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

这将设置存储当前相册的变量,然后调用showDataForAlbumAtIndex:显示新相册的数据。

注意:通常的做法是放置在#pragma mark伪指令后合并在一起的方法。 编译器会忽略这行,但是如果你通过Xcode的跳转栏下拉当前文件中的方法列表,你会看到一个分隔符和一个粗体标题的指令。 这有助于您组织代码,以便在Xcode中轻松导航。

下一步,添加这个方法

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

这个,你会认识到,是协议方法返回的滚动视图的数量。 由于滚动视图将显示所有相册数据的封面,所以计数是专辑记录的数量。

现在,添加以下代码:

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

在这里你创建一个新的AlbumView并将其传递给HorizontalScroller
只有三个短的方法来显示一个漂亮的水平卷轴。
是的,您仍然需要实际创建滚动条并将其添加到您的主视图,但在这之前,添加以下方法:

- (void)reloadScroller
{
    allAlbums = [[LibraryAPI sharedInstance] getAlbums];
    if (currentAlbumIndex < 0) currentAlbumIndex = 0;
    else if (currentAlbumIndex >= allAlbums.count) currentAlbumIndex = allAlbums.count-1;
    [scroller reload];

    [self showDataForAlbumAtIndex:currentAlbumIndex];
}

此方法通过LibraryAPI加载专辑数据,然后根据当前视图索引的当前值设置当前显示的视图。 如果当前视图索引小于0,意味着当前未选择任何视图,则将显示列表中的第一个专辑。 否则,将显示最后一个相册。

此方法通过LibraryAPI加载专辑数据,然后根据当前视图索引的当前值设置当前显示的视图。 如果当前视图索引小于0,意味着当前未选择任何视图,则将显示列表中的第一个专辑。 否则,将显示最后一个相册。
现在,通过在[self showDataForAlbumAtIndex:0]之前添加以下代码到viewDidLoad来初始化滚动器:

    scroller = [[HorizontalScroller alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 120)];
    scroller.backgroundColor = [UIColor colorWithRed:0.24f green:0.35f blue:0.49f alpha:1];
    scroller.delegate = self;
    [self.view addSubview:scroller];

    [self reloadScroller];

上面只是创建一个新的HorizontalScroller实例,设置其背景颜色和委托,将滚动条添加到主视图中,然后加载滚动条的子视图以显示相册数据。

注意:如果协议变得太大,并且包含许多方法,您应该考虑将其分成几个较小的协议。 UITableViewDelegateUITableViewDataSource是一个很好的例子,因为它们都是UITableView的协议。 尝试设计您的协议,以使每个协议处理一个特定的功能区域。

构建和运行你的项目,并看看你新的滚动水平条:
滚动水平条

呃,等等。 水平卷轴就位,但封面在哪里?
啊,没错,你没有实现代码下载封面。 为此,您需要添加一种方法来下载图片。 因为所有对服务的访问都通过LibraryAPI,这就是这种新方法必须去的地方。 但是,有一些事情需要考虑:

  1. AlbumView不应该直接与LibraryAPI一起工作。 您不要将视图逻辑与通信逻辑混合。
  2. 出于同样的原因,LibraryAPI不应该知道AlbumView。
  3. LibraryAPI需要在下载封面后通知AlbumView,因为AlbumView必须显示封面。
    听起来像一个难题? 不要绝望,你将学习如何使用观察者模式。

观察者模式

在观察者模式中,一个对象通知其他对象任何状态更改。 所涉及的对象不需要彼此了解 ,因此鼓励了分离设计。 此模式最常用于在属性更改时通知感兴趣的对象。

通常的实现需要观察者注册对另一个对象的状态的兴趣。 当状态改变时,向所有观察对象通知该改变。 苹果的推送通知服务是一个范例。

如果你想坚持MVC的概念,你需要允许Model对象与View对象通信,但是没有它们之间的直接引用。 这就是观察者模式的来源。

Cocoa以两种熟悉的方式实现观察者模式:通知(Notifications)和键值观察(KVO)。

通知(Notifications)

不要混淆推送通知或者本地通知,通知基于允许对象(发布者)向其他对象(订阅者/侦听者)发送消息的订阅和发布模型。 发布商从来不需要知道订阅者的任何信息。
苹果大量使用通知。 例如,当显示/隐藏键盘时,系统分别发送UIKeyboardWillShowNotification / UIKeyboardWillHideNotification。 当您的应用程序转到后台时,系统将发送UIApplicationDidEnterBackgroundNotification通知。

注意:打开UIApplication.h,在文件的末尾,您将看到系统发送的超过20个通知的列表。
转到AlbumView.m并在initWithFrame:albumCover :中的[self addSubview:indicator]后面插入以下代码:

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

此行通过NSNotificationCenter单例发送通知。 通知信息包含要填充的UIImageView和要下载的封面图像的URL。 这是执行封面下载任务所需的所有信息。
到LibraryAPI.m中的init,直接在isOnline = NO之后将以下行添加:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadImage:) name:@"BLDownloadImageNotification" object:nil];

这是模式的另一边:观察者。 每次AlbumView类发布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;

PersistenceManager.m的实现两个方法:

- (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目录中,如果在Documents目录中找不到匹配的文件,getImage:将返回nil。
现在将以下方法添加到LibraryAPI.m:

- (void)downloadImage:(NSNotification*)notification
{
    // 1
    UIImageView *imageView = notification.userInfo[@"imageView"];
    NSString *coverUrl = notification.userInfo[@"coverUrl"];

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

    if (imageView.image == nil)
    {
        // 3
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            UIImage *image = [httpClient downloadImage:coverUrl];

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

以下是上述代码的细分:

 downloadImage通过通知执行,因此方法接收通知对象作为参数。 将从通知中检索UIImageView和图像URL。
 如果先前已下载,则从PersistencyManager检索图像。
 如果图像尚未下载,则使用HTTPClient检索它。
 下载完成后,在图像视图中显示图像,并使用PersistencyManager在本地保存。

同样,您使用门面模式来隐藏从其他类下载图像的复杂性。 通知发件人不关心图像是来自Web还是来自文件系统。
构建和运行您的应用程序,检查您的HorizontalScroller中的美丽的封面:
封面

停止您的应用程式并再次执行。 请注意,加载封面没有延迟,因为它们已保存在本地。 你甚至可以从互联网断开连接,你的应用程序将正常工作。 但是,这里有一个奇怪的地方:菊花从来不停止旋转! 这是怎么回事?

您在下载映像时启动菊花,但是在下载映像后,您尚未实现停止菊花的逻辑。 你可以在每次下载图像时发出通知,也可以使用其他观察者模式-KVO。

键值观察(KVO)

在KVO中,对象可以要求通知特定属性的任何更改; 它自己的或另一个对象的。 如果你有兴趣,你可以阅读更多关于这个在苹果的KVO编程指南

如何使用KVO模式

如上所述,KVO机制允许对象观察对属性的改变。 在你的情况下,你可以使用KVO来观察保存图像的UIImageView的image属性的变化。

打开AlbumView.m到initWithFrame:albumCover :将紧接在[self addSubview:indicator]之后添加以下代码:

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

这增加了self,这是当前类,作为coverImage的image属性的观察者。
完成后,您还需要取消注册为观察员。 仍然在AlbumView.m中,添加以下代码:

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

最后,添加这个方法

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

您必须在充当观察者的每个类中实现此方法。 每次观察到的属性更改时,系统都会执行此方法。 在上面的代码中,当image属性更改时停止菊花。 这样,当加载图像时,菊花将停止旋转。
构建并运行您的项目。 菊花应该消失:
菊花消失

注意:始终记住删除您的观察者,当他们被释放,否则你的应用程序将崩溃时,主体尝试发送消息到这些不存在的观察者!

如果你玩一下你的应用程序,并终止它,你会注意到你的应用程序的状态不保存。 应用启动时,您查看的最后一个相册不会是默认相册。
要纠正这一点,您可以使用列表上的下一个模式:备忘录模式。

备忘录模式

备忘录模式捕获和外化一个对象的内部状态。 换句话说,它将你的东西保存在某个地方。 稍后,这种外部化状态可以被恢复而不违反封装; 也就是说,私有数据保持私有。

怎样使用备忘录模式

将以下两个方法添加到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将当前相册索引保存到NSUserDefaults - NSUserDefaults是iOS提供的标准数据存储,用于保存应用程序特定的设置和数据。
loadPreviousState加载先前保存的索引, 这不是完全实现的备忘录模式。
现在,在滚动条初始化之前,在ViewController.m中的viewDidLoad中添加以下行:

[self loadPreviousState];

当应用程序启动时加载之前保存的状态。 但是,你从哪里保存应用程序的当前状态加载? 您将使用通知来执行此操作。 当应用程序进入后台时,iOS会发送UIApplicationDidEnterBackgroundNotification通知。 您可以使用此通知调用saveCurrentState。
将以下行添加到viewDidLoad的末尾:

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

当应用程序即将进入后台时,ViewController将通过调用保存当前状态自动保存当前状态。
添加以下代码:

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

这确保当ViewController被释放时,将该类作为观察者删除。
构建并运行您的应用程序。 导航到其中一个相册,使用Command + Shift + H(如果您在模拟器上)将应用程序发送到后台,然后关闭您的应用程序。 重新启动,并检查先前选择的相册是否居中:
备忘录模式图片

它看起来像专辑数据是正确的,但滚轮不在正确的专辑的中心。
这是由于该方法initialViewIndexForHorizontalScroller:没有在委托中实现,在这种情况下ViewController,初始视图总是设置为第一个视图。

要解决这个问题,请将以下代码添加到ViewController.m:

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

现在HorizontalScroller的第一个视图设置为currentAlbumIndex指示的任何相册。 这是一个伟大的设计模式,以确保应用程序的体验保持个人和可恢复。
再次运行您的应用。 滚动到之前的相册,停止应用,然后重新启动以确保问题已修复:

备忘录模式图片

如果你看PersistencyManager的init,你会注意到相册数据是硬编码的,并在每次创建PersistencyManager时重新创建。 但最好是创建专辑列表一次,并将它们存储在一个文件。 如何将Album数据保存到文件?
一个选项是遍历Album的属性,将它们保存到plist文件,然后在需要时重新创建Album实例。 这不是最好的选择,因为它要求你编写特定的代码,这取决于每个类中有什么数据/属性。 例如,如果您以后创建了具有不同属性的Movie类,则保存和加载该数据将需要新的代码。
此外,您将无法为每个类实例保存私有变量,因为外部类不可访问它们。 这就是苹果创建存档机制的原因。

归档(Archiving)

苹果的专门实现备忘录模式的一种是归档。 这将一个对象转换为可以保存和以后恢复的流,而不会将私有属性暴露给外部类。或者在苹果的归档和序列化编程指南

怎样实现归档

首先,您需要声明该Album可以通过符合NSCoding协议进行存档。 打开Album.h并更改@interface行如下:

@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:。 相反,当您取消存档实例以创建Album实例时,解码时调用initWithCoder:。 它简单,但强大。
现在可以存档Album类,添加实际保存的代码并加载相册列表。
将以下方法原型添加到PersistencyManager.h:

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

NSKeyedArchiver将相册数组存档到一个名为albums.bin的文件中。
当归档包含其他对象的对象时,归档器会自动尝试递归归档子对象和子对象的任何子对象等。 在这种情况下,存档从专辑开始,专辑是一个Album实例的数组。 由于NSArray和Album都支持NSCopying接口,因此数组中的所有内容都会自动归档。
现在用以下代码替换PersistencyManager.m中的init:

- (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:@"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"]]];
            [self saveAlbums];
        }
    }
    return self;
}

在新代码中,NSKeyedUnarchiver从文件加载相册数据(如果存在)。 如果它不存在,它会创建相册数据,并立即保存以便下次启动应用程序。
每次应用程序进入后台时,您还需要保存相册数据。 这可能现在似乎不必要了,但如果你以后添加选项更改相册数据怎么办? 然后,您需要确保所有更改都保存。
将以下方法签名添加到LibraryAPI.h:

- (void)saveAlbums;

由于主应用程序通过LibraryAPI访问所有服务,因此这是应用程序将如何让PersitencyManager知道它需要保存相册数据。

现在将方法实现添加到LibraryAPI.m:

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

此代码只是传递对LibraryAPI的调用,以将相册保存到PersistenceManager。
将以下代码添加到ViewController.m中的saveCurrentState结尾处:

    [[LibraryAPI sharedInstance] saveAlbums];

上面的代码使用LibraryAPI触发专辑数据的保存每当ViewController保存其状态。
构建您的应用程序以检查一切编译。
不幸的是,没有简单的方法来检查数据持久性是否正确。 您可以在Finder中检查应用程序的模拟器文档文件夹,以查看相册数据文件是否已创建,但为了查看任何其他更改您必须添加到更改相册数据。
但是,如果您在媒体库中添加了一个选项来删除不再需要的相册,而不是更改数据? 此外,如果您错误地删除相册,是否会有一个撤消选项是不是更好?
这提供了一个很好的机会来谈论列表中的最后一个模式:命令模式。

命令模式

命令设计模式将请求或操作封装为对象。包封的请求是不是原始请求更加灵活和可目的,存储供以后,动态地修改,或者放置到队列之间传递。 Apple已经使用Target-Action机制和NSInvocation实现了这种模式。

你可以在苹果官方文档中阅读更多关于Target-Action的资料,但调用使用其中包含一个目标对象,一个方法选择器和一些参数的NSInvocation的类。 此对象可以动态更改,并在需要时执行。 这是在操作中的命令模式的一个完美的例子。 它将发送对象与接收对象或多个对象分离,并且可以持久存储请求或请求链。

怎样使用命令模式

在进入操作调用之前,您需要设置撤销操作的框架。 因此,必须定义撤销堆栈所需的UIToolBarNSMutableArray
将以下代码添加到ViewController.m中的类扩展,其中定义了所有其他变量:

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

这将创建一个工具栏,它将显示新操作的按钮,以及一个用作命令队列的数组。
将以下代码添加到viewDidLoad的开头:(在注释2之前):

    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];

上面的代码创建一个工具栏,有两个按钮和一个灵活的空间。 它还创建一个空的撤消堆栈。 在此处禁用撤消按钮,因为撤消栈从空开始。

此外,请注意,工具栏不是使用在初始化frame创建的,因为viewDidLoad中设置的frame大小不是最终frame大小。 因此,一旦通过向ViewController.m添加代码来定义视图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.m添加三个方法来处理相册管理操作:添加,删除和撤消。
第一种是添加新相册的方法:

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

在这里您添加相册,将其设置为当前相册索引,然后重新加载scroller。

接下来是删除方法:

- (void)deleteAlbum
{
    // 1
    Album *deletedAlbum = allAlbums[currentAlbumIndex];

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

    // 3
    [undoStack addObject:undoAction];

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

    // 5
    [toolbar.items[0] setEnabled:YES];
}

此代码中有一些新的和令人兴奋的功能,因此请考虑以下每个注释部分:

  1. 获取要删除的相册。
  2. 定义类型为NSMethodSignature的对象以创建NSInvocation,如果用户稍后决定撤消删除,将用于反转删除操作。 NSInvocation需要知道三件事:选择器(要发送的消息),目标(发送消息的对象)和消息的参数。在此示例中,发送的消息是删除的相反,因为当您撤消删除时,您需要添加回删除的相册。
  3. 创建undoAction后,将其添加到undoStack。 此操作将添加到数组的末尾,就像在正常堆栈中一样。
  4. 使用LibraryAPI从数据结构中删除相册并重新加载滚动器。
  5. 由于撤消堆栈中有一个操作,因此您需要启用撤消按钮。

注意:使用NSInvocation,您需要牢记以下几点:
- 参数必须通过指针传递。
- 参数从索引2开始; 索引0和1保留给目标和选择器。
- 如果有一个机会,参数将被释放,那么你应该调用retainArguments。

最后,添加undo动作的方法:

- (void)undoAction
{
    if (undoStack.count > 0)
    {
        NSInvocation *undoAction = [undoStack lastObject];
        [undoStack removeLastObject];
        [undoAction invoke];
    }

    if (undoStack.count == 0)
    {
        [toolbar.items[0] setEnabled:NO];
    }
}

undo操作“弹出”堆栈中的最后一个对象。 这个对象总是类型NSInvocation,可以通过调用… invoke来调用。 这会调用您之前在删除相册时创建的命令,并将删除的相册添加到相册列表中。 因为你也删除了堆栈中的最后一个对象,当你“弹出”它,你现在检查,看看堆栈是否为空。 如果是,则表示没有其他操作要撤消。 所以你禁用撤消按钮。

构建并运行您的应用程序,以测试您的撤消机制,删除一个或两个相册,然后点击撤消按钮查看它的操作:

撤消机制
这也是测试您的相册数据更改是否在会话之间保留的好地方。 现在,如果您删除相册,将应用程序发送到后台,然后终止应用程序,下次启动应用程序时,显示的相册列表应该删除。

下一步方向

这里是完成的项目的源代码:BlueLibrary-final
有两个其他设计模式应用程序没有讲到,但重要的是:抽象工厂责任链模式。随时阅读这些,扩大你的设计模式的视野。

在本教程中,您了解了如何利用iOS设计模式的强大功能以非常直接和松耦合的方式执行复杂的任务。您已经学习了很多iOS设计模式和概念:单例,MVC,委托,协议,门面,观察者,备忘录和命令模式。
您的最终代码应该是松耦合,可重复使用和可读。如果另一个开发人员查看您的代码,他们将立即了解发生了什么,以及每个类在您的应用中的作用。
关键是不要对你写的每一行代码使用设计模式。相反,当您考虑如何解决特定问题时,请注意设计模式,尤其是在设计应用程序的早期阶段。他们会让开发人员的写代码质量更高!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、 IOS设计模式的六大设计原则之单一职责原则(SRP,Single Responsibility Principle) 定义   就一个类而言,应该仅有一个引起它变化的原因。 定义解读   这是六大原则最简单的一种,通俗点说,就是不存在多个原因使得一个类发生变化,也就是一个类只负责一种职责的工作。 优点 类的复杂度降低,一个类只负责一个功能,其逻辑要比负责多项功能简单的多; 类的可读性增强,阅读起来轻松; 可维护性强,一个易读、简单的类自然也容易维护; 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。 问题提出   假设有一个类C,它负责两个不同的职责:职责P1和P2。当职责P1需求发生改变而需要修改类C时,有可能会导致原本运行正常的职责P2功能发生故障。 解决方案   遵循单一职责原则。分别建立两个类C1、C2,使C1完成职责P1,C2完成职责P2。这样,当修改类C1时,不会使职责P2发生故障风险;同理,当修改C2时,也不会使职责P1发生故障风险。   说到这里,大家会觉得这个原则太简单了。稍有经验的程序员,即使没有听说过单一职责原则,在设计软件时也会自觉的遵守这一重要原则。在实际的项目开发,谁也不希望因为修改了一个功能导致其他的功能发生故障。而避免出现这一问题的方法便是遵循单一职责原则。虽然单一职责原则如此简单,并且被认为是常识,即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。为什么会出现这种现象呢?因为有职责扩散。实际项目,因为某种原因,职责P被分化为粒度更细的职责P1和P2。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值