Professional Documents
Culture Documents
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 youve probably heard the term, but do you know what it means? While most developers probably agree that design patterns are very important, there arent many articles on the subject and we developers
sometimes dont pay too much attention to design patterns while writing code.
Design patterns are reusable solutions to common problems in software design. Theyre templates designed to help
you write code thats 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 youre new to design patterns, then I have good news for you! First, youre already using tons of iOS design patterns
thanks to the way Cocoa is built and the best practices youre 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, youll 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, youll 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.
Dont be misled into thinking that this is an article about theory; youll 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.
Theres 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
NSObject. Click Next and then Create.
Open Album.h and add the following properties and method prototype between @interface and @end
@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 theres no need to change them after the Album object is created.
The method is the object initializer. When you create a new album, youll 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
@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;
Theres 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 theres an instance variable named coverImage
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 classs internal functionality. This convention is extremely important if youre 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 de-
sign pattern! :]
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
UIViews 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? Thats right: ViewController
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 cant 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 doesnt rely on a specific implementation of the Model, then it can be reused with a different model to
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. Youll 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.
Note: Apple uses this approach a lot. For example: [NSUserDefaults standardUserDefaults]
standardUserDefaults],
[UIApplication sharedApplication]
sharedApplication], [UIScreen mainScreen]
mainScreen], [NSFileManager defaultManager] all return a Singleton object.
Youre likely wondering why you care if theres 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, theres 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: its 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.
The above image shows a Logger class with a single property (which is the single instance), and two methods:
sharedInstance and init
init.
The first time a client sends the sharedInstance message, the property instance isnt yet initialized, so you
create a new instance of the class and return a reference to it.
The next time you call sharedInstance
sharedInstance, instance is immediately returned without any initialization. This logic
promises that only one instance exists at all times.
Youll implement this pattern by creating a singleton class to manage all the album data.
Youll notice theres a group called API in the project; this is where youll 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
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;
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
sistencyManager, and make it a subclass of NSObject
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 arrays
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"],
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
youre 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 wont have to change the code
that uses your API as it wont change.
Note: Usually, a singleton exists for the lifetime of the app. You shouldnt keep too many strong pointers in the
singleton to other objects, since they wont be released until the app is closed.
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 youll 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
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 doesnt actually work with a real server and is only here to demonstrate the usage of the facade pattern, so isOnline will always be NO
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:
addAlbum:atIndex:. The class first updates the data locally, and then if theres 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 doesnt know and doesnt 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. Dont be stingy with defensive code and dont assume that all the
clients will necessarily use your classes the same way the Facade uses them.
Build and run your app. Youll see an incredibly exciting empty black screen like this:
Youll need something to display the album data on screen which is a perfect use for your next design pattern: the
Decorator.
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. Its
slightly different from the classic definition of a decorator, because a Category doesnt hold an instance of the class it
extends.
Note: Besides extending your own classes, you can also add methods to any of Cocoas own classes!
Where will the album titles come from? Album is a Model object, so it doesnt care how you present the data. Youll
need some external code to add this functionality to the Album class, but without modifying the class directly.
Youll create a category that is an extension of Album
Album; it will define a new method that returns a data structure which
can be used easily with UITableView
UITableViews.
The data structure will look like the following:
Note: Did you notice the name of the new file? Album+TableRepresentation means youre extending the
Album class. This convention is important, because its easier to read and it prevents a collision with other categories you or someone else might create.
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 undefined as to which
method implementation is used at runtime. This is less likely to be an issue if youre using categories with your
own classes, but can cause serious problems when using categories to add methods to standard Cocoa or Cocoa Touch classes.
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
UITableView, one of the methods you must implement is
tableView:numberOfRowsInSection:
tableView:numberOfRowsInSection:.
You cant expect the UITableView to know how many rows you want to have in each section, as this is applicationspecific. 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.
Heres a pseudo-explanation of whats going on when you create a new UITableView
UITableView:
The UITableView object does its job of displaying a table view. However, eventually it will need some information
that it doesnt have. Then, it turns to its delegates and sends a message asking for additional information. In Objective-Cs implementation of the delegate pattern, a class can declare optional and required methods through a protocol. Youll 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 wont be
able to achieve this by subclassing.
Note: This is an important pattern. Apple uses this approach in most of the UIKit classes: UITableView
UITableView, UITextView
TextView, UITextField
UITextField, UIWebView
UIWebView, UIAlert
UIAlert, UIActionSheet
UIActionSheet, UICollectionView
UICollectionView, UIPickerView
erView, UIGestureRecognizer
UIGestureRecognizer, UIScrollView
UIScrollView. The list goes on and on.
#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 methods 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];
}
Heres a breakdown of the above code:
1. Change the background color to a nice navy blue color.
2. Get a list of all the albums via the API. You dont use PersistencyManager directly!
3. This is where you create the UITableView
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];
}
else
{
}
currentAlbumData = nil;
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
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; youll experience a crash with the following exception displayed in the debug console:
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. Well implement this when we discuss the next design pattern.
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
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. Its good practice to conform to the NSObject proto-
col 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
HorizontalScroller. Youll 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 its not implemented then the HorizontalScroller will default to the first index.
Next, youll 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
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
HorizontalScrollerDelegate,
giving you some type safety.
The reload method is modelled after reloadData in UITableView
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:
1. Define constants to make it easy to modify the layout at design time. The views dimensions inside the scroller
will be 100 x 100 with a 10 point margin from its enclosing rectangle.
2. 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.
3. 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
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:
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;
[obj removeFromSuperview];
- (void)didMoveToSuperview
{
[self reload];
}
The didMoveToSuperview message is sent to a view when its 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 youre viewing is always centered inside the scroll view. To do this, youll 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 hasnt come to a complete stop yet. When the scroll action ends,
the the system calls scrollViewDidEndDecelerating
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 youve just written; youll see theres not
one single mention of the Album or AlbumView classes. Thats 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, its 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; youll 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: Its 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 Xcodes jump bar, youll see a
separator and a bold title for the directive. This helps you organize your code for easy navigation in Xcode.
[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
[self showDataForAlbumAtIndex:0];
mAtIndex: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
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
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, thats right you didnt implement the code to download the covers yet. To do that, youll need to add a way to
download images. Since all your access to services goes through LibraryAPI
LibraryAPI, thats where this new method would
have to go. However, there are a few things to consider first:
1. AlbumView shouldnt work directly with LibraryAPI
LibraryAPI. You dont want to mix view logic with communication
logic.
2. For the same reason, LibraryAPI shouldnt know about AlbumView
AlbumView.
3. LibraryAPI needs to inform AlbumView once the covers are downloaded since the AlbumView has to display the covers.
Sounds like a conundrum? Dont despair, youll learn how to do this using the Observer pattern :]
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
UIKeyboardWillShowNotification/UIKeyboardWillHideNotification
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 youll see a list of over 20 notifications sent by the system.
When this class is deallocated, it removes itself as an observer from all notifications it had registered for.
Theres one more thing to do. It would probably be a good idea to save the downloaded covers locally so the app
wont 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]];
});
});
}
}
Heres a breakdown of the above code:
1. 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.
2. Retrieve the image from the PersistencyManager if its been downloaded previously.
3. If the image hasnt already been downloaded, then retrieve it using HTTPClient
HTTPClient.
4. When the download is complete, display the image in the image view and use the PersistencyManager to
save it locally.
Again, youre using the Facade pattern to hide the complexity of downloading an image from the other classes. The
notification sender doesnt 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
HorizontalScroller:
Stop your app and run it again. Notice that theres no delay in loading the covers because theyve been saved locally.
You can even disconnect from the Internet and your app will work flawlessly. However, theres one odd bit here: the
spinner never stops spinning! Whats going on?
You started the spinner when downloading the image, but you havent 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, youll do that using the other Observer pattern, KVO.
}
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 theyre 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, youll notice that the state of your app isnt saved. The last album you viewed wont be the default album when the app launches.
To correct this, you can make use of the next pattern on the list: Memento.
- (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 isnt quite the full implementation of the Memento
pattern, but youre 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? Youll use Notifications to do this. iOS sends a UIApplicationDidEnterBackgroundNotification notification when the app enters the background. You can use this notification to call saveCurrentState
rentState. Isnt that convenient?
Add the following line to the end of viewDidLoad
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
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 using Command+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 isnt centered on the correct album. What gives?
This is what the optional method initialViewIndexForHorizontalScroller: was meant for! Since that
methods 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
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:
PersistencyManager is created. But tts 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
Albums properties, save them to a plist file and then recreate the Album instances
when theyre needed. This isnt 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 wont be able to save the private variables for each class instance since they are not accessible to an
external class. Thats exactly why Apple created the Archiving mechanism.
Archiving
One of Apples 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 Apples Archives and Serializations
Programming Guide.
- (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
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 doesnt exist, it creates
the album data and immediately saves it for the next launch of the app.
Youll 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 youd 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
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
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, theres 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
youd 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, wouldnt 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 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 isnt initiated with a frame, as the frame size set in viewDidLoad isnt final. So set the final frame via the following block of code once the view frame is finalized by adding the code to ViewController.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);
}
Youll 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:
1. Get the album to delete.
2. Define an object of type NSMethodSignature to create the NSInvocation
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 deletes opposite since when you undo a deletion, you need to add
back the deleted album.
3. After the undoAction has been created you add it to the undoStack
undoStack. This action will be added to the end of
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
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.
Eli Ganem
Eli is an independent iOS developer located in Israel (soon relocating to San Francisco!). He
has been developing iOS apps and working as an Instructor for over 2 years. You can see
his portfolio of apps here: Moving App.