您的位置:首页 > 移动开发 > IOS开发

IOS design patterns

2015-07-10 00:19 721 查看
If you're new here, you may want to subscribe to my RSS
feed or follow me on Twitter.
Thanks for visiting!

iOS Design Patterns – you’ve probably heard the term, but do you know what it means?
While most developers probably agree that design patterns are very important, there aren’t many articles on the subject and we developers sometimes don’t pay too much attention to design patterns while writing code.

Design patterns are reusable solutions to common problems in software design. They’re templates designed to help you write code that’s easy to understand and reuse. They also help you create loosely coupled code so that you can change or replace components
in your code without too much of a hassle.

If you’re new to design patterns, then I have good news for you! First, you’re already using tons of iOS design patterns thanks to the way Cocoa is built and the best practices you’re encouraged to use. Second, this tutorial will bring you up to speed on all
the major (and not so major) iOS design patterns that are commonly used in Cocoa.

The tutorial is divided into sections, one section per design pattern. In each section, you’ll read an explanation of the following:

What the design pattern is.

Why you should use it.

How to use it and, where appropriate, common pitfalls to watch for when using the pattern.

In this tutorial, you will create a Music Library app that will display your albums and their relevant information.

In the process of developing this app, you’ll become acquainted with the most common Cocoa design patterns:

Creational: Singleton and Abstract Factory.

Structural: MVC, Decorator, Adapter, Facade and Composite.

Behavioral: Observer, Memento, Chain of Responsibility and Command.

Don’t be misled into thinking that this is an article about theory; you’ll get to use most of these design patterns in your music app. Your app will look like this by the end of the tutorial:





Getting Started

Download the starter
project, extract the contents of the ZIP file, and open BlueLibrary.xcodeproj in
Xcode.

There’s not a lot there, just the default
ViewController
and
a simple HTTP Client with empty implementations.

Note: Did you know that as soon as you create a new Xcode project your code is already filled with design
patterns? MVC, Delegate, Protocol, Singleton – You get them all for free! :]

Before you dive into the first design pattern, you must create two classes to hold and display the album data.

Navigate to “File\New\File…” (or simply press Command+N).
Select iOS > Cocoa Touch and then Objective-C
class
and click Next. Set the class name to
Album
and
the subclass to
NSObject
. Click Next and
then Create.

Open Album.h and add the following properties and method prototype between
@interface
and
@end
:

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

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

Note that all the properties are readonly, since there’s no need to change them after
the
Album
object is created.

The method is the object initializer. When you create a new album, you’ll pass in the album name, the artist, the album cover URL, and the year.

Now open Album.m and add the following code between
@implementation
and
@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;
}

There’s nothing fancy here; just a simple init method to create new Album instances.

Again, navigate to File\New\File…. Select Cocoa
Touch
and then Objective-C class and click Next.
Set the class name to AlbumView, but this time set the subclass to UIView.
Click Next and then Create.

Note: If you find keyboard shortcuts easier to use then, Command+N will
create a new file,Command+Option+N will create a new group, Command+B will
build your project, and Command+R will run it.

Open AlbumView.h and add the following method prototype between
@interface
and
@end


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

Now open AlbumView.m and replace all the code after
@implementation
with
the following code:

@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

The first thing you notice here is that there’s an instance variable named
coverImage
.
This variable represents the album cover image. The second variable is an indicator that spins to indicate activity while the cover is being downloaded.

In the implementation of the initializer you set the background to black, create the image view with a small margin of 5 pixels and create and add the activity indicator.

Note: Wondering why the private variables were defined in the implementation file and not in the interface
file? This is because no class outside the AlbumView class needs to know of the existence of these variables since they are used only in the implementation of the class’s internal functionality. This convention is extremely important if you’re creating a library
or framework for other developers to use.

Build your project (Command+B) just to make sure everything is in order. All good?
Then get ready for your first design pattern! :]


MVC – The King of Design Patterns





Model View Controller (MVC) is one of the building blocks of Cocoa and is undoubtedly the most-used design pattern of all. It classifies objects according to their general role in your application and encourages clean separation of code based on role.

The three roles are:

Model: The object that holds your application data and defines how to manipulate it.
For example, in your application the Model is your
Album
class.

View: The objects that are in charge of the visual representation of the Model and
the controls the user can interact with; basically, all the
UIView
s
and their subclasses. In your application the View is represented by your
AlbumView
class.

Controller: The controller is the mediator that coordinates all the work. It accesses
the data from the model and displays it with the views, listens to events and manipulates the data as necessary. Can you guess which class is your controller? That’s right:
ViewController
.

A good implementation of this design pattern in your application means that each object falls into one of these groups.

The communication between View to Model through Controller can be best described with the following diagram:





The Model notifies the Controller of any data changes, and in turn, the Controller updates the data in the Views. The View can then notify the Controller of actions the user performed and the Controller will either update the Model if necessary or retrieve
any requested data.

You might be wondering why you can’t just ditch the Controller, and implement the View and Model in the same class, as that seems a lot easier.

It all comes down to code separation and reusability. Ideally, the View should be completely separated from the Model. If the View doesn’t rely on a specific implementation of the Model, then it can be reused with a different model to present some other data.

For example, if in the future you’d also like to add movies or books to your library, you could still use the same
AlbumView
to
display your movie and book objects. Furthermore, if you want to create a new project that has something to do with albums, you could simply reuse your
Album
class,
because it’s not dependent on any view. That’s the strength of MVC!


How to Use the MVC Pattern

First, you need to ensure that each class in your project is either a Controller, a Model or a View; don’t combine the functionality of two roles in one class. You’ve already done a good job so far by creating an
Album
class
and an
AlbumView
class.

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.

Navigate to File\New\Group (or press on Command+Option+N)
and name the group Model. Repeat the same process to create View and Controller groups.

Now drag Album.h and Album.m to
the Model group. Drag AlbumView.h and AlbumView.m to
the View group, and finally drag ViewController.h and ViewController.m to
the Controller group.

At this point the project structure should look like this:





Your project already looks a lot better without all those files floating around. Obviously you can have other groups and classes, but the core of the application is contained in these three categories.

Now that your components are organized, you need to get the album data from somewhere. You’ll create an API class to use throughout your code to manage the data — which presents an opportunity to discuss your next design pattern — the Singleton.


The Singleton Pattern

The Singleton design pattern ensures that only one instance exists for a given class and that there’s a global access point to that instance. It usually uses lazy loading to create the single instance when it’s needed the first time.

Note: Apple uses this approach a lot. For example:
[NSUserDefaults
standardUserDefaults]
,
[UIApplication sharedApplication]
,
[UIScreen
mainScreen]
,
[NSFileManager defaultManager]
all
return a Singleton object.

You’re likely wondering why you care if there’s more than one instance of a class floating around. Code and memory is cheap, right?

There are some cases in which it makes sense to have exactly one instance of a class. For example, there’s no need to have multiple
Logger
instances
out there, unless you want to write to several log files at once. Or, take a global configuration handler class: it’s easier to implement a thread-safe access to a single shared resource, such as a configuration file, than to have many class modifying the
configuration file possibly at the same time.


How to Use the Singleton Pattern

Take a look at the diagram below:





The above image shows a Logger class with a single property (which is the single instance), and two methods:
sharedInstance
and
init
.

The first time a client sends the
sharedInstance
message,
the property
instance
isn’t yet initialized,
so you create a new instance of the class and return a reference to it.

The next time you call
sharedInstance
,
instance
is
immediately returned without any initialization. This logic promises that only one instance exists at all times.

You’ll implement this pattern by creating a singleton class to manage all the album data.

You’ll notice there’s a group called API in the project; this is where you’ll put all
the classes that will provide services to your app. Inside this group create a new class with the iOS\Cocoa
Touch\Objective-C class
template. Name the class LibraryAPI, and make
it a subclass of
NSObject
.

Open LibraryAPI.h and replace its contents with the following:

@interface LibraryAPI : NSObject

+ (LibraryAPI*)sharedInstance;

@end

Now go to LibraryAPI.m and insert this method right after the
@implentation
line:

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

// 2
static dispatch_once_t oncePredicate;

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

There’s a lot going on in this short method:

Declare a static variable to hold the instance of your class, ensuring it’s available globally inside your class.

Declare the static variable
dispatch_once_t
which
ensures that the initialization code executes only once.

Use Grand Central Dispatch (GCD) to execute a block which initializes an instance of
LibraryAPI
.
This is the essence of the Singleton design pattern: the initializer is never called again once the class has been instantiated.

The next time you call
sharedInstance
, the code
inside the
dispatch_once
block won’t be executed
(since it’s already executed once) and you receive a reference to the previously created instance of
LibraryAPI
.

Note: To learn more about GCD and its uses, check out the tutorials Multithreading
and Grand Central Dispatch and How
to Use Blocks on this site.

You now have a Singleton object as the entry point to manage the albums. Take it a step further and create a class to handle the persistence of your library data.

Create a new class with the iOS\Cocoa Touch\Objective-C class template inside the API
group. Name the class
PersistencyManager
, and
make it a subclass of
NSObject
.

Open PersistencyManager.h. Add the following import to the top of the file:

#import "Album.h"

Next, add the following code to PersistencyManager.h after the
@interface
line:

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

The above are prototypes for the three methods you need to handle the album data.

Open PersistencyManager.m and add the following code right above the
@implementation
line:

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

The above adds a class extension, which is another way to add private methods and variables to a class so that external classes will not know about them. Here, you declare an
NSMutableArray
to
hold the album data. The array’s mutable so that you can easily add and delete albums.

Now add the following code implementation to PersistencyManager.m after the
@implementation
line:

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

In
init
you populate the array with five sample
albums. If the above albums aren’t to your liking, feel free to replace them with the music you enjoy. :]

Now add the following three methods to 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];
}

These methods allow you to get, add, and delete albums.

Build your project just to make sure everything still compiles correctly.

At this point, you might wonder where the
PersistencyManager
class
comes in since it’s not a Singleton. The relationship between
LibraryAPI
and
PersistencyManager
will
be further explored in the next section where you’ll look at the Facade design pattern.


The Facade Design Pattern





The Facade design pattern provides a single interface to a complex subsystem. Instead of exposing the user to a set of classes and their APIs, you only expose one simple unified API.

The following image explains this concept:





The user of the API is completely unaware of the complexity that lies beneath. This pattern is ideal when working with a large number of classes, particularly when they are complicated to use or difficult to understand.

The Facade pattern decouples the code that uses the system from the interface and implementation of the classes you’re hiding; it also reduces dependencies of outside code on the inner workings of your subsystem. This is also useful if the classes under the
facade are likely to change, as the facade class can retain the same API while things change behind the scenes.

For example, if the day comes when you want to replace your backend service, you won’t have to change the code that uses your API as it wont’ change.


How to Use the Facade Pattern

Currently you have
PersistencyManager
to save
the album data locally and
HTTPClient
to handle
the remote communication. The other classes in your project should not be aware of this logic.

To implement this pattern, only
LibraryAPI
should
hold instances of
PersistencyManager
and
HTTPClient
.
Then,
LibraryAPI
will expose a simple API to access
those services.

Note: Usually, a singleton exists for the lifetime of the app. You shouldn’t keep too many strong pointers
in the singleton to other objects, since they won’t be released until the app is closed.

The design looks like the following:





LibraryAPI will be exposed to other code, but will hide the
HTTPClient
and
PersistencyManager
complexity
from the rest of the app.

Open LibraryAPI.h and add the following import to the top of the file:

#import "Album.h"

Next, add the following method definitions to LibraryAPI.h:

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

For now, these are the methods you’ll expose to the other classes.

Go to LibraryAPI.m and add the following two imports:

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

This will be the only place where you import these classes. Remember: your API will be the only access point to your “complex” system.

Now, add some private variables via a class extension (above the
@implementation
line):

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

@end

isOnline
determines if the server should be updated
with any changes made to the albums list, such as added or deleted albums.

You now need to initialize these variables via
init
.
Add the following code to LibraryAPI.m:

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

The HTTP client doesn’t actually work with a real server and is only here to demonstrate the usage of the facade pattern, so
isOnline
will
always be
NO
.

Next, add the following three methods to 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]];
}
}

Take a look at
addAlbum:atIndex:
. The class first
updates the data locally, and then if there’s an internet connection, it updates the remote server. This is the real strength of the Facade; when some class outside of your system adds a new album, it doesn’t know — and doesn’t need to know — of the complexity
that lies underneath.

Note: When designing a Facade for classes in your subsystem, remember that nothing prevents the client from
accessing these “hidden” classes directly. Don’t be stingy with defensive code and don’t assume that all the clients will necessarily use your classes the same way the Facade uses them.

Build and run your app. You’ll see an incredibly exciting empty black screen like this:





You’ll need something to display the album data on screen — which is a perfect use for your next design pattern: the Decorator.


The Decorator Design Pattern

The Decorator pattern dynamically adds behaviors and responsibilities to an object without modifying its code. It’s an alternative to subclassing where you modify a class’ behavior by wrapping it with another object.

In Objective-C there are two very common implementations of this pattern: Category and Delegation.


Category

Category is an extremely powerful mechanism that allows you to add methods to existing classes without subclassing. The new methods are added at compile time and can be executed like normal methods of the extended class. It’s slightly different from the classic
definition of a decorator, because a Category doesn’t hold an instance of the class it extends.

Note: Besides extending your own classes, you can also add methods to any of Cocoa’s own classes!


How to Use Categories

Imagine a situation in which you have an
Album
object
that you want to present inside a table view:





Where will the album titles come from?
Album
is
a Model object, so it doesn’t care how you present the data. You’ll need some external code to add this functionality to the
Album
class,
but without modifying the class directly.

You’ll create a category that is an extension of
Album
;
it will define a new method that returns a data structure which can be used easily with
UITableView
s.

The data structure will look like the following:





To add a Category to
Album
, navigate to File\New\File… and
select the Objective-C category template — don’t select the Objective-C
class
out of habit! Enter
TableRepresentation
in
the Category field and
Album
in
theCategory on field.

Note: Did you notice the name of the new file?
Album+TableRepresentation
means
you’re extending the
Album
class. This convention
is important, because it’s easier to read and it prevents a collision with other categories you or someone else might create.

Go to Album+TableRepresentation.h and add the following method prototype:

- (NSDictionary*)tr_tableRepresentation;

Notice there’s a
tr_
at the beginning of the method
name, as an abbreviation of the name of the category: TableRepresentation. Again, conventions like this will help prevent collisions with other methods!

Note: If the name of a method declared in a category is the same as a method in the original class, or the
same as a method in another category on the same class (or even a superclass), the behavior is undefinedas to which method implementation is used at runtime. This is less likely to be an issue if you’re using categories with your own classes, but
can cause serious problems when using categories to add methods to standard Cocoa or Cocoa Touch classes.

Go to Album+TableRepresentation.m and add the following method:

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

Consider for a moment how powerful this pattern can be:

You’re using properties directly from
Album
.

You have added to the
Album
class but you haven’t
subclassed it. If you need to sub-class
Album
,
you can still do that too.

This simple addition lets you return a
UITableView
–ish representation
of an
Album
, without modifying
Album
‘s
code.

Apple uses Categories a lot in the Foundation classes. To see how they do this, open NSString.h.
Find
@interface NSString
, and you’ll see the definition
of the class together with three categories:
NSStringExtensionMethods
,
NSExtendedStringPropertyListParsing
and
NSStringDeprecated
.
Categories help keep the methods organized and separated into sections.


Delegation

The other Decorator design pattern, Delegation, is a mechanism in which one object acts on behalf of, or in coordination with, another object. For example, when you use a
UITableView
,
one of the methods you must implement is
tableView:numberOfRowsInSection:
.

You can’t expect the
UITableView
to know how many
rows you want to have in each section, as this is application-specific. Therefore, the task of calculating the amount of rows in each section is passed on to the
UITableView
delegate.
This allows the
UITableView
class to be independent
of the data it displays.

Here’s a pseudo-explanation of what’s going on when you create a new
UITableView
:





The
UITableView
object does its job of displaying
a table view. However, eventually it will need some information that it doesn’t have. Then, it turns to its delegates and sends a message asking for additional information. In Objective-C’s implementation of the delegate pattern, a class can declare optional
and required methods through a protocol. You’ll cover protocols a bit later in this tutorial.

It might seem easier to just subclass an object and override the necessary methods, but consider that you can only subclass based on a single class. If you want an object to be the delegate of two or more other objects, you won’t be able to achieve this by
subclassing.

Note: This is an important pattern. Apple uses this approach in most of the UIKit classes:
UITableView
,
UITextView
,
UITextField
,
UIWebView
,
UIAlert
,
UIActionSheet
,
UICollectionView
,
UIPickerView
,
UIGestureRecognizer
,
UIScrollView
.
The list goes on and on.


How to Use the Delegate Pattern

Go to ViewController.m and add the following imports to the top of the file:

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

Now, add these private variables to the class extension so that the class extension looks like this:

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

@end

Then, replace the
@interface
line in the class
extension with this one:

@interface ViewController () <UITableViewDataSource, UITableViewDelegate> {

This is how you make your delegate conform to a protocol — think of it as a promise made by the delegate to fulfil the method’s contract. Here, you indicate that
ViewController
will
conform to the
UITableViewDataSource
and
UITableViewDelegate
protocols.
This way
UITableView
can be absolutely certain
that the required methods are implemented by its delegate.

Next, replace
viewDidLoad:
with this code:

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

Here’s a breakdown of the above code:

Change the background color to a nice navy blue color.

Get a list of all the albums via the API. You don’t use
PersistencyManager
directly!

This is where you create the
UITableView
. You
declare that the view controller is the
UITableView
delegate/data
source; therefore, all the information required by
UITableView
will
be provided by the view controller.

Now, add the following method to 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:
fetches the required
album data from the array of albums. When you want to present the new data, you just need to call
reloadData
.
This causes
UITableView
to ask its delegate such
things as how many sections should appear in the table view, how many rows in each section, and how each cell should look.

Add the following line to the end of
viewDidLoad


[self showDataForAlbumAtIndex:currentAlbumIndex];

This loads the current album at app launch. And since
currentAlbumIndex
was
previously set to 0, this shows the first album in the collection.

Build and run your project; you’ll experience a crash with the following exception displayed in the debug console:





What’s going on here? You declared that
ViewController
as
the
UITableView
‘s delegate and data source. But
in doing so, you must conform to all the required methods — including
tableView:numberOfRowsInSection:

which you haven’t done yet.

Add the following code to ViewController.m anywhere between the
@implementation
and
@end
lines:

- (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:
returns the number
of rows to display in the table view, which matches the number of titles in the data structure.

tableView:cellForRowAtIndexPath:
creates and returns
a cell with the title and its value.

Build and run your project. Your app should start and present you with the following screen:





Things are looking pretty good so far. But if you recall the first image showing the finished app, there was a horizontal scroller at the top of the screen to switch between albums. Instead of coding a single-purpose horizontal scroller, why not make it reusable
for any view?

To make this view reusable, all decisions about its content should be left to another object: a delegate. The horizontal scroller should declare methods that its delegate implements in order to work with the scroller, similar to how the
UITableView
delegate
methods work. We’ll implement this when we discuss the next design pattern.


The Adapter Pattern

An Adapter allows classes with incompatible interfaces to work together. It wraps itself around an object and exposes a standard interface to interact with that object.

If you’re familiar with the Adapter pattern then you’ll notice that Apple implements it in a slightly different manner – Apple uses protocols to do the job. You may be familiar with protocols like
UITableViewDelegate
,
UIScrollViewDelegate
,
NSCoding
and
NSCopying
.
As an example, with the
NSCopying
protocol, any
class can provide a standard
copy
method.


How to Use the Adapter Pattern

The horizontal scroller mentioned before will look like this:





To begin implementing it, right click on the View group in the Project Navigator, select New File… and
create a class with the iOS\Cocoa Touch\Objective-C class template. Name the new class HorizontalScroller and
make it subclass from
UIView
.

Open HorizontalScroller.h and insert the following code after the
@end
line:

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

This defines a protocol named
HorizontalScrollerDelegate
that
inherits from the
NSObject
protocol in the same
way that an Objective-C class inherits from its parent. It’s good practice to conform to the
NSObject
protocol
— or to conform to a protocol that itself conforms to the
NSObject
protocol.
This lets you send messages defined by
NSObject
to
the delegate of
HorizontalScroller
. You’ll soon
see why this is important.

You define the required and optional methods that the delegate will implement between the
@protocol
and
@end
lines.
So add the following protocol methods:

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

Here you have both required and optional methods. Required methods must be implemented by the delegate and usually contain some data that is absolutely required by the class. In this case, the required details are the number of views, the view at a specific
index, and the behaviour when the view is tapped. The optional method here is the initial view; if it’s not implemented then the
HorizontalScroller
will
default to the first index.

Next, you’ll need to refer to your new delegate from within the
HorizontalScroller
class
definition. But the protocol definition is below the class definition and so is not visible at this point. What can you do?

The solution is to forward declare the protocol so that the compiler (and Xcode) knows that such a protocol will be available. To do this, add the following code above the @interface line:

@protocol HorizontalScrollerDelegate;

Still in HorizontalScroller.h, add the following code between the
@interface
and
@end
statements:

@property (weak) id<HorizontalScrollerDelegate> delegate;

- (void)reload;

The attribute of the property you created above is defined as
weak
.
This is necessary in order to prevent a retain cycle. If a class keeps a
strong
pointer
to its delegate and the delegate keeps a strong pointer back to the conforming class, your app will leak memory since neither class will release the memory allocated to the other.

The
id
means that the delegate can only be assigned
classes that conform to
HorizontalScrollerDelegate
,
giving you some type safety.

The
reload
method is modelled after
reloadData
in
UITableView
;
it reloads all the data used to construct the horizontal scroller.

Replace the contents of HorizontalScroller.m with the following code:

#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

Taking each comment block in turn:

Define constants to make it easy to modify the layout at design time. The view’s dimensions inside the scroller will be 100 x 100 with a 10 point margin from its enclosing rectangle.

HorizontalScroller
conforms to the
UIScrollViewDelegate
protocol.
Since
HorizontalScroller
uses a
UIScrollView
to
scroll the album covers, it needs to know of user events such as when a user stops scrolling.

Create the scroll view containing the views.

Next you need to implement the initializer. Add the following method:

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

The scroll view completely fills the
HorizontalScroller
.
A
UITapGestureRecognizer
detects touches on the
scroll view and checks if an album cover has been tapped. If so, it notifies the
HorizontalScroller
delegate.

Now add this mehtod:

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

The gesture passed in as a parameter lets you extract the location via
locationInView:
.

Next, you invoke
numberOfViewsForHorizontalScroller:
on
the delegate. The
HorizontalScroller
instance
has no information about the delegate other than knowing it can safely send this message since the delegate must conform to the
HorizontalScrollerDelegate
protocol.

For each view in the scroll view, perform a hit test using
CGRectContainsPoint
to
find the view that was tapped. When the view is found, send the delegate the
horizontalScroller:clickedViewAtIndex:
message.
Before you break out of the for loop, center the tapped view in the scroll view.

Now add the following code to reload the 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;
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];
}
}

Stepping through the code comment-by-comment:

If there’s no delegate, then there’s nothing to be done and you can return.

Remove all the subviews previously added to the scroll view.

All the views are positioned starting from the given offset. Currently it’s 100, but it can be easily tweaked by changing the
#DEFINE
at
the top of the file.

The
HorizontalScroller
asks its delegate for the
views one at a time and it lays them next to each another horizontally with the previously defined padding.

Once all the views are in place, set the content offset for the scroll view to allow the user to scroll through all the albums covers.

The
HorizontalScroller
checks if its delegate
responds to the
initialViewIndexForHorizontalScroller:
selector.
This check is necessary because that particular protocol method is optional. If the delegate doesn’t implement this method, 0 is used as the default value. Finally, this piece of code sets the scroll view to center the initial view defined by the delegate.

You execute
reload
when your data has changed.
You also need to call this method when you add
HorizontalScroller
to
another view. Add the following code to HorizontalScroller.m to cover the latter scenario:

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

The
didMoveToSuperview
message is sent to a view
when it’s added to another view as a subview. This is the right time to reload the contents of the scroller.

The last piece of the
HorizontalScroller
puzzle
is to make sure the album you’re viewing is always centered inside the scroll view. To do this, you’ll need to perform some calculations when the user drags the scroll view with their finger.

Add the following method (again to 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];
}

The above code takes into account the current offset of the scroll view and the dimensions and the padding of the views in order to calculate the distance of the current view from the center. The last line is important: once the view is centered, you then inform
the delegate that the selected view has changed.

To detect that the user finished dragging inside the scroll view, you must add the following
UIScrollViewDelegate
methods:

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

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

scrollViewDidEndDragging:willDecelerate:
informs
the delegate when the user finishes dragging. The
decelerate
parameter
is true if the scroll view hasn’t come to a complete stop yet. When the scroll action ends, the the system calls
scrollViewDidEndDecelerating
.
In both cases we should call the new method to center the current view since the current view probably has changed after the user dragged the scroll view.

Your
HorizontalScroller
is ready for use! Browse
through the code you’ve just written; you’ll see there’s not one single mention of the
Album
or
AlbumView
classes.
That’s excellent, because this means that the new scroller is truly independent and reusable.

Build your project to make sure everything compiles properly.

Now that
HorizontalScroller
is complete, it’s
time to use it in your app. Open ViewController.m and add the following imports:

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

Add
HorizontalScrollerDelegate
to the protocols
that
ViewController
conforms to:

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

Add the following instance variable for the Horizontal Scroller to the class extension:

HorizontalScroller *scroller;

Now you can implement the delegate methods; you’ll be amazed at how just a few lines of code can implement a lot of functionality.

Add the following code to ViewController.m:

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

This sets the variable that stores the current album and then calls
showDataForAlbumAtIndex:
to
display the data for the new album.

Note: It’s common practice to place methods that fit together after a
#pragma
mark
directive. The compiler will ignore this line but if you drop down the list of methods in your current file via Xcode’s jump bar, you’ll see a separator and a bold title for the directive. This helps you organize your code for easy navigation in
Xcode.

Next, add the following code:

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

This, as you’ll recognize, is the protocol method returning the number of views for the scroll view. Since the scroll view will display covers for all the album data, the count is the number of album records.

Now, add this code:

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

Here you create a new
AlbumView
and pass it to
the
HorizontalScroller
.

That’s it! Only three short methods to display a nice looking horizontal scroller.

Yes, you still need to actually create the scroller and add it to your main view but before doing that, add the following method:

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

This method loads album data via
LibraryAPI
and
then sets the currently displayed view based on the current value of the current view index. If the current view index is less than 0, meaning that no view was currently selected, then the first album in the list is displayed. Otherwise, the last album is
displayed.

Now, initialize the scroller by adding the following code to
viewDidLoad
before
[self
showDataForAlbumAtIndex:0];
:

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

The above simply creates a new instance of
HorizontalScroller
,
sets its background color and delegate, adds the scroller to the main view, and then loads the subviews for the scroller to display album data.

Note: If a protocol becomes too big and is packed with a lot of methods, you should consider breaking it
into several smaller protocols.
UITableViewDelegate
and
UITableViewDataSource
are
a good example, since they are both protocols of
UITableView
.
Try to design your protocols so that each one handles one specific area of functionality.

Build and run your project and take a look at your awesome new horizontal scroller:





Uh, wait. The horizontal scroller is in place, but where are the covers?

Ah, that’s right — you didn’t implement the code to download the covers yet. To do that, you’ll need to add a way to download images. Since all your access to services goes through
LibraryAPI
,
that’s where this new method would have to go. However, there are a few things to consider first:

AlbumView
shouldn’t work directly with
LibraryAPI
.
You don’t want to mix view logic with communication logic.

For the same reason,
LibraryAPI
shouldn’t know
about
AlbumView
.

LibraryAPI
needs to inform
AlbumView
once
the covers are downloaded since the
AlbumView
has
to display the covers.

Sounds like a conundrum? Don’t despair, you’ll learn how to do this using the Observer pattern
:]


The Observer Pattern

In the Observer pattern, one object notifies other objects of any state changes. The objects involved don’t need to know about one another – thus encouraging a decoupled design. This pattern’s most often used to notify interested objects when a property has
changed.

The usual implementation requires that an observer registers interest in the state of another object. When the state changes, all the observing objects are notified of the change. Apple’s Push Notification service is a global example of this.

If you want to stick to the MVC concept (hint: you do), you need to allow Model objects to communicate with View objects, but without direct references between them. And that’s where the Observer pattern comes in.

Cocoa implements the observer pattern in two familiar ways: Notifications and Key-Value
Observing (KVO)
.


Notifications

Not be be confused with Push or Local notifications, Notifications are based on a subscribe-and-publish model that allows an object (the publisher) to send messages to other objects (subscribers/listeners). The publisher never needs to know anything about the
subscribers.

Notifications are heavily used by Apple. For example, when the keyboard is shown/hidden the system sends a
UIKeyboardWillShowNotification
/
UIKeyboardWillHideNotification
,
respectively. When your app goes to the background, the system sends a
UIApplicationDidEnterBackgroundNotification
notification.

Note: Open up UIApplication.h,
at the end of the file you’ll see a list of over 20 notifications sent by the system.


How to Use Notifications

Go to AlbumView.m and insert the following code after
[self
addSubview:indicator];
in
initWithFrame:albumCover:
:

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

This line sends a notification through the
NSNotificationCenter
singleton.
The notification info contains the
UIImageView
to
populate and the URL of the cover image to be downloaded. That’s all the information you need to perform the cover download task.

Add the following line to
init
in LibraryAPI.m,
directly after
isOnline = NO
:

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

This is the other side of the equation: the observer. Every time an
AlbumView
class
posts a
BLDownloadImageNotification
notification,
since
LibraryAPI
has registered as an observer
for the same notification, the system notifies
LibraryAPI
.
And
LibraryAPI
executes downloadImage: in
response.

However, before you implement downloadImage: you must remember to unsubscribe from
this notification when your class is deallocated. If you do not properly unsubscribe from a notification your class registered for, a notification might be sent to a deallocated instance. This can result in application crashes.

Add the following method to LibraryAPI.m:

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

When this class is deallocated, it removes itself as an observer from all notifications it had registered for.

There’s one more thing to do. It would probably be a good idea to save the downloaded covers locally so the app won’t need to download the same covers over and over again.

Open PersistencyManager.h and add the following two method prototypes:

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

And the method implementations to PersistencyManager.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];
}

This code is pretty straightforward. The downloaded images will be saved in the Documents directory, and
getImage:
will
return
nil
if a matching file is not found in
the Documents directory.

Now add the following method to 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]];
});
});
}
}

Here’s a breakdown of the above code:

downloadImage
is executed via notifications and
so the method receives the notification object as a parameter. The
UIImageView
and
image URL are retrieved from the notification.

Retrieve the image from the
PersistencyManager
if
it’s been downloaded previously.

If the image hasn’t already been downloaded, then retrieve it using
HTTPClient
.

When the download is complete, display the image in the image view and use the
PersistencyManager
to
save it locally.

Again, you’re using the Facade pattern to hide the complexity of downloading an image from the other classes. The notification sender doesn’t care if the image came from the web or from the file system.

Build and run your app and check out the beautiful covers inside your
HorizontalScroller
:





Stop your app and run it again. Notice that there’s no delay in loading the covers because they’ve been saved locally. You can even disconnect from the Internet and your app will work flawlessly. However, there’s one odd bit here: the spinner never stops spinning!
What’s going on?

You started the spinner when downloading the image, but you haven’t implemented the logic to stop the spinner once the image is downloaded. You could send out a notification every time an image has been downloaded, but instead, you’ll do that using
the other Observer pattern, KVO.


Key-Value Observing (KVO)

In KVO, an object can ask to be notified of any changes to a specific property; either its own or that of another object. If you’re interested, you can read more about this on Apple’s
KVO Programming Guide.


How to Use the KVO Pattern

As mentioned above, the KVO mechanism allows an object to observe changes to a property. In your case, you can use KVO to observe changes to the
image
property
of the
UIImageView
that holds the image.

Open AlbumView.m and add the following code to
initWithFrame:albumCover:
,
just after the
[self addSubview:indicator];
line:

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

This adds
self
, which is the current class, as
an observer for the
image
property of
coverImage
.

You also need to unregister as an observer when you’re done. Still in AlbumView.m,
add the following code:

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

Finally, add this method:

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

You must implement this method in every class acting as an observer. The system executes this method every time the observed property changes. In the above code, you stop the spinner when the “image” property changes. This way, when an image is loaded, the
spinner will stop spinning.

Build and run your project. The spinner should disappear:





Note: Always remember to remove your observers when they’re deallocated, or else your app will crash when
the subject tries to send messages to these non-existent observers!

If you play around with your app a bit and terminate it, you’ll notice that the state of your app isn’t saved. The last album you viewed won’t be the default album when the app launches.

To correct this, you can make use of the next pattern on the list: Memento.


The Memento Pattern

The memento pattern captures and externalizes an object’s internal state. In other words, it saves your stuff somewhere. Later on, this externalized state can be restored without violating encapsulation; that is, private data remains private.


How to Use the Memento Pattern

Add the following two methods to 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
saves the current album index
to
NSUserDefaults
NSUserDefaults
is
a standard data store provided by iOS for saving application specific settings and data.

loadPreviousState
loads the previously saved index.
This isn’t quite the full implementation of the Memento pattern, but you’re getting there.

Now, Add the following line to
viewDidLoad
in ViewController.m before
the scroller initialization:

[self loadPreviousState];

That loads the previously saved state when the app starts. But where do you save the current state of the app for loading from? You’ll use Notifications to do this. iOS sends a
UIApplicationDidEnterBackgroundNotification
notification
when the app enters the background. You can use this notification to call
saveCurrentState
.
Isn’t that convenient?

Add the following line to the end of
viewDidLoad
:

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

Now, when the app is about to enter the background, the
ViewController
will
automatically save the current state by calling
saveCurrentState
.

Now, add the following code:

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

This ensures you remove the class as an observer when the
ViewController
is
deallocated.

Build and run your app. Navigate to one of the albums, send the app to the background usingCommand+Shift+H (if
you are on the simulator) and then shut down your app. Relaunch, and check that the previously selected album is centered:





It looks like the album data is correct, but the scroller isn’t centered on the correct album. What gives?

This is what the optional method
initialViewIndexForHorizontalScroller:
was
meant for! Since that method’s not implemented in the delegate,
ViewController
in
this case, the initial view is always set to the first view.

To fix that, add the following code to ViewController.m:

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

Now the
HorizontalScroller
first view is set to
whatever album is indicated by
currentAlbumIndex
.
This is a great way to make sure the app experience remains personal and resumable.

Run your app again. Scroll to an album as before, stop the app, then relaunch to make sure the problem is fixed:





If you look at
PersistencyManager
‘s
init
,
you’ll notice the album data is hardcoded and recreated every time
PersistencyManager
is
created. But tt’s better to create the list of albums once and store them in a file. How would you save the Album data to a file?

One option is to iterate through
Album
‘s properties,
save them to a plist file and then recreate the
Album
instances
when they’re needed. This isn’t the best option, as it requires you to write specific code depending on what data/properties are there in each class. For example, if you later created a Movie class with different properties, the saving and loading of that
data would require new code.

Additionally, you won’t be able to save the private variables for each class instance since they are not accessible to an external class. That’s exactly why Apple created the Archiving mechanism.


Archiving

One of Apple’s specialized implementations of the Memento pattern is Archiving. This converts an object into a stream that can be saved and later restored without exposing private properties to external classes. You can read more about this functionality in
Chapter 16 of the iOS 6 by Tutorials book. Or in Apple’s
Archives and Serializations Programming Guide.


How to Use Archiving

First, you need to declare that
Album
can be archived
by conforming to the
NSCoding
protocol. Open Album.hand
change the
@interface
line as follows:

@interface Album : NSObject <NSCoding>

Add the following two methods to 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;
}

You call
encodeWithCoder:
when you archive an
instance of this class. Conversely, you call
initWithCoder:
when
you unarchive an instance to create an instance of
Album
.
It’s simple, yet powerful.

Now that the
Album
class can be archived, add
the code that actually saves and loads the list of albums.

Add the following signature (or method prototype) to PersistencyManager.h:

- (void)saveAlbums;

This will be the method that’s called to save the albums.

Now, add the method implementation to PersistencyManager.m:

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

NSKeyedArchiver
archives the album array into
a file called albums.bin.

When you archive an object which contains other objects, the archiver automatically tries to recursively archive the child objects and any child objects of the children and so on. In this instance, the archival starts with
albums
,
which is an array of Album instances. Since
NSArray
and
Album
both
support the NSCopying interface, everything in the array is automatically archived.

Now replace
init
in PersistencyManager.m with
the following code:

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

In the new code,
NSKeyedUnarchiver
loads the album
data from the file, if it exists. If it doesn’t exist, it creates the album data and immediately saves it for the next launch of the app.

You’ll also want to save the album data every time the app goes into the background. This might not seem necessary now but what if you later add the option to change album data? Then you’d want this to ensure that all your changes are saved.

Add the following method signature to LibraryAPI.h:

- (void)saveAlbums;

Since the main application accesses all services via
LibraryAPI
,
this is how the application will let
PersitencyManager
know
that it needs to save album data.

Now add the method implementation to LibraryAPI.m:

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

This code simply passes on a call to
LibraryAPI
to
save the albums on to
PersistencyMangaer
.

Add the following code to the end of
saveCurrentState
in ViewController.m:

[[LibraryAPI sharedInstance] saveAlbums];

And the above code uses
LibraryAPI
to trigger
the saving of album data whenever the ViewController saves its state.

Build your app to check that everything compiles.

Unfortunately, there’s no easy way to check if the data persistency is correct though. You can check the simulator Documents folder for your app in Finder to see that the album data file is created but in order to see any other changes you’d have to add in
the ability to change album data.

But instead of changing data, what if you added an option to delete albums you no longer want in your library? Additionally, wouldn’t it be nice to have an undo option if you delete an album by mistake?

This provides a great opportunity to talk about the last pattern on the list: Command.


The Command Pattern

The Command design pattern encapsulates a request or action as an object. The encapsulated request is much more flexible than a raw request and can be passed between objects, stored for later, modified dynamically, or placed into a queue. Apple has implemented
this pattern using the Target-Action mechanism and Invocation.

You can read more about Target-Action in
Apple’s documentation but Invocation uses the NSInvocation class which contains a
target object, a method selector and some parameters. This object can be changed dynamically and executed when needed. It’s a perfect example of the Command pattern in action. It decouples the sending object from the receiving object or objects and can persist
a request or a chain of requests.


How to Use the Command Pattern

Before you get into the invocation of actions, you need to set up the framework for undoing actions. So you must define the
UIToolBar
and
the
NSMutableArray
needed for the undo stack.

Add the following code to the class extension in ViewController.m where you defined
all the other variables:

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

This creates a toolbar which will display the buttons for the new actions, as well as an array to act as the command queue.

Add the following code to the beginning of
viewDidLoad:
(right
before comment #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];

The above code creates a toolbar with two buttons and a flexible space between them. It also creates an empty undo stack. The undo button is disabled here because the undo stack starts off empty.

Also, note that the toolbar isn’t initiated with a frame, as the frame size set in
viewDidLoad
isn’t
final. So set the final frame via the following block of code once the view frame is finalized by adding the code toViewController.m:

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

You’ll add three method to ViewController.m for handling album management actions:
add, delete, and undo.

The first is the method for adding a new album:

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

Here you add the album, set it as the current album index, and reload the scroller.

Next comes the delete method:

- (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:¤tAlbumIndex atIndex:3];
[undoAction retainArguments];

// 3
[undoStack addObject:undoAction];

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

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

There are some new and exciting features in this code, so consider each commented section below:

Get the album to delete.

Define an object of type
NSMethodSignature
to
create the
NSInvocation
, which will be used to
reverse the delete action if the user later decides to undo a deletion. The
NSInvocation
needs
to know three things: The selector (what message to send), the target (who to send the message to) and the arguments of the message. In this example the message sent is delete’s opposite since when you undo a deletion, you need to add back the deleted album.

After the
undoAction
has been created you add
it to the
undoStack
. This action will be added
to the end of the array, just as in a normal stack.

Use
LibraryAPI
to delete the album from the data
structure and reload the scroller.

Since there’s an action in the undo stack, you need to enable the undo button.

Note: With
NSInvocation
,
you need to keep the following points in mind:

The arguments must be passed by pointer.

The arguments start at index 2; indices 0 and 1 are reserved for the target and the selector.

If there’s a chance that the arguments will be deallocated, then you should call
retainArguments
.

Finally, add the method for the undo action:

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

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

The undo operation “pops” the last object in the stack. This object is always of type NSInvocation and can be invoked by calling …
invoke
.
This invokes the command you created earlier when the album was deleted, and adds the deleted album back to the album list. Since you also deleted the last object in the stack when you “popped” it, you now check to see if the stack is empty. If it is, that
means there are no more actions to undo. So you disable the Undo button.

Build and run your app to test out your undo mechanism, delete an album (or two) and hit the Undo button to see it in action:





This is also a good place to test out whether changes to your album data is retained between sessions. Now, if you delete an album, send the app to the background, and then terminate the app, the next time you start the app the displayed album list should reflect
the deletion.


Where to go from here?

Here’s the source code for the finished project: BlueLibrary-final

There are two other design patterns that didn’t make their way into the app, but are important to mention:Abstract
Factory (aka Class Cluster) and Chain
of Responsibility (aka Responder Chain). Feel free to read up on these to expand your design pattern horizons.

In this tutorial you saw how to harness the power of iOS design patterns to perform complicated tasks in a very straightforward and loosely coupled manner. You’ve learned a lot of iOS design patterns and concepts: Singleton, MVC, Delegation, Protocols, Facade,
Observer, Memento, and Command.

Your final code is loosely coupled, reusable, and readable. If another developer looks at your code, they’ll instantly understand what’s going on and what each class does in your app.

The point isn’t to use a design pattern for every line of code you write. Instead, be aware of design patterns when you consider how to solve a particular problem, especially in the early stages of designing your app. They’ll make your life as a developer much
easier and your code a lot better!

copy from:http://www.raywenderlich.com/46988/ios-design-patterns
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: