Three20 Tutorial for iOS: How To Use the Three20 Photo Viewer

Ray Wenderlich
Create a full-featured photo viewer easily!

Create a full-featured photo viewer easily!

It’s a common requirement for apps to display images in a similar manner to the Photos app on the iPhone. One simple way to implement this is by using the TTPhotoViewController from the open source Three20 library.

In this Three20 tutorial, we will cover how you can use Three20 to make a simple app that displays a set of photos just like the Photos app does – complete with flicking between photos, pinching to zoom, captions, and full screen mode.

Best of all, because you’re using Three20, you get a lot of other great features built in such a built-in thumbnail viewer, being able to retrieve higher resolution images over the network, and caching the retrieved images locally!

If you haven’t used Three20 before, you may wish to check out the Introduction to Three20 tutorial first. However, this Three20 tutorial is fairly self-contained, so it isn’t required!

Adding Three20 To Your Project

Create a new project in XCode by going to File\New Project, select Window-based Application, and make sure to choose iPhone for the product. Click Choose, and name the Project “PhotoViewer.”

Next you need to get a copy of the three20 library. The best way to get the latest copy of three20 is to use the source control system “git.” If you do not already have this installed, you can download a Mac installer.

Once you have git installed, follow the excellent instructions on the three20 web page for instructions on how to pull down the latest code and add it to your project.

At the time of writing this post, there’s one additional step you need to take. The three20 projects are currently set to use iOS 3.0 as the Base SDK, but assuming you are using XCode 3.2.3, this SDK is no longer available. Therefore, you need to double click on each of the Three20 XCode projects in your Groups & Files list to open up the project, go to Project\Edit Project Settings in each Three20 project, and set the Base SDK to iPhone Device 4.0.

Once you’ve done that, add the following to the top of PhotoViewerAppDelegate.h: #import “Three20/Three20.h”

Then compile your project – if it works three20 has been successfully integrated and you can move onto the next step!

Creating a TTPhoto Class

To display a photo in the TTPhotoViewController, Three20 needs some information from our app. Specifically, it needs to know the size of the image, a URL for where to find the image, an optional caption for the image, and few other pieces of information.

To give Three20 this information, we need to create a class representing our photos that implements the TTPhoto protocol. The definition for the TTPhoto protocol looks something like this:

@protocol TTPhoto <NSObject, TTURLObject>
@property (nonatomic) CGSize size;
@property (nonatomic, copy) NSString* caption;
- (NSString*)URLForVersion:(TTPhotoVersion)version;
@property (nonatomic, assign) id<TTPhotoSource> photoSource;
@property (nonatomic) NSInteger index;
@end

So we need to make sure our class has properties for the size of the image and any caption we want to display. We need to create a method to return a URL for the image. And we’ll need properties for a photoSource and index, but we’ll talk about what those mean later.

A note about the method to retrieve a URL – note it takes a TTPhotoVersion parameter. This has values such as TTPhotoVersionSmall or TTPhotoVersionLarge, so we can return different URLs for images of different levels of quality. In fact that’s what we’ll do in this Three20 tutorial – we’ll include a couple small but low quality images embedded with the app, and a large but high quality image on a web server.

So let’s create our own class that implements this protocol. Go to File\New File, choose Cocoa Touch Class\Objective-C class, make sure Subclass of NSObject is selected, and click Next. Name the new file Photo.m (and make sure also create Photo.h is selected), and click Finish.

Replace Photo.h with the following:

#import <Foundation/Foundation.h>
#import <Three20/Three20.h>
 
@interface Photo : NSObject <TTPhoto> {
    NSString *_caption;
    NSString *_urlLarge;
    NSString *_urlSmall;
    NSString *_urlThumb;
    id <TTPhotoSource> _photoSource;
    CGSize _size;
    NSInteger _index;
}
 
@property (nonatomic, copy) NSString *caption;
@property (nonatomic, copy) NSString *urlLarge;
@property (nonatomic, copy) NSString *urlSmall;
@property (nonatomic, copy) NSString *urlThumb;
@property (nonatomic, assign) id <TTPhotoSource> photoSource;
@property (nonatomic) CGSize size;
@property (nonatomic) NSInteger index;
 
- (id)initWithCaption:(NSString *)caption urlLarge:(NSString *)urlLarge urlSmall:(NSString *)urlSmall urlThumb:(NSString *)urlThumb size:(CGSize)size;
 
@end

So basically we’re just creating a subclass of NSObject, but we implement the TTPhoto protocol. We create some properties for caption, photoSource, size, and index, since they’re required in the TTPhoto protocol, and some instance variables to back them up. We also create some instance variables to hold URLs to the three versions of the images we’re going to use for this Three20 tutorial – a “large” version, a “small” version, and a thumbnail version.

Now move over to Photo.m and replace it with the following:

#import "Photo.h"
 
@implementation Photo
@synthesize caption = _caption;
@synthesize urlLarge = _urlLarge;
@synthesize urlSmall = _urlSmall;
@synthesize urlThumb = _urlThumb;
@synthesize photoSource = _photoSource;
@synthesize size = _size;
@synthesize index = _index;
 
- (id)initWithCaption:(NSString *)caption urlLarge:(NSString *)urlLarge urlSmall:(NSString *)urlSmall urlThumb:(NSString *)urlThumb size:(CGSize)size {
    if ((self = [super init])) {
        self.caption = caption;
        self.urlLarge = urlLarge;
        self.urlSmall = urlSmall;
        self.urlThumb = urlThumb;
        self.size = size;
        self.index = NSIntegerMax;
        self.photoSource = nil;
    }
    return self;
}
 
- (void) dealloc {
    self.caption = nil;
    self.urlLarge = nil;
    self.urlSmall = nil;
    self.urlThumb = nil;    
    [super dealloc];
}
 
#pragma mark TTPhoto
 
- (NSString*)URLForVersion:(TTPhotoVersion)version {
    switch (version) {
        case TTPhotoVersionLarge:
            return _urlLarge;
        case TTPhotoVersionMedium:
            return _urlLarge;
        case TTPhotoVersionSmall:
            return _urlSmall;
        case TTPhotoVersionThumbnail:
            return _urlThumb;
        default:
            return nil;
    }
}
 
@end

We create a constructor for our Photo that we’ll use later, initializing everything to what’s passed in. Note we just initialize the index and photoSource to dummy values for now – they will be updated later after the constructor is called.

The only method of real interest in this class is URLForVersion: here we examine the version passed in, and return the appropriate URL based on the version. Note we are not including a “medium quality” image in this Three20 tutorial, so we just return the URL for the large image in this case.

By implementing this method, we’ll see that Three20 is smart enough to display the small image first (since it should load more quickly) and then follow up with retrieving the high quality image in the background and replacing the small image when its ready.

Creating a Photo Source

Our next step is to create an object that the TTPhotoViewController can use to figure out what photos it should display. In Three20, we do that by creating an object that implements the TTPhotoSource protocol. The definition for the TTPhotoSource protocol looks something like this:

@protocol TTPhotoSource <TTModel, TTURLObject>
@property (nonatomic, copy) NSString* title;
@property (nonatomic, readonly) NSInteger numberOfPhotos;
@property (nonatomic, readonly) NSInteger maxPhotoIndex;
- (id<TTPhoto>)photoAtIndex:(NSInteger)index;
@end

So we need to return a title for the set of photos (used in the thumbnail view of the Photo View Controller), the number of photos to display (and the max index), and finally a method to return a TTPhoto at a particular index.

TTPhotoSource is built so that you can retrieve the information on which photos to display over the network as well. However, for the purposes of this Three20 tutorial we’re take a simple route and just hard-code which images to display straight into our app – therefore we can bypass the remote downloading capability.

So let’s create an object that implements TTPhotoSource. Go to File\New File, choose Cocoa Touch Class\Objective-C class, make sure Subclass of NSObject is selected, and click Next. Name the new file PhotoSet.m (and make sure also create PhotoSet.h is selected), and click Finish.

Then replace PhotoSet.h with the following:

#import <Foundation/Foundation.h>
#import "Three20/Three20.h"
 
@interface PhotoSet : TTURLRequestModel <TTPhotoSource> {
    NSString *_title;
    NSArray *_photos;
}
 
@property (nonatomic, copy) NSString *title;
@property (nonatomic, retain) NSArray *photos;
 
+ (PhotoSet *)samplePhotoSet;
 
@end

Nothing too special here – we declare two properties / instance variables to store the title of our photo set and contain the list of photos. We also have a static method to create a sample photo set that we’ll use later on in the app.

Note that the class derives from TTURLRequestModel. This is because the TTPhotoSource protocol requires us to implement the TTModel and TTURLObject protocols as well, and deriving from this class makes taking care of that a lot easier.

Next go to PhotoSet.m and replace it with the following:

#import "PhotoSet.h"
#import "Photo.h"
 
@implementation PhotoSet
@synthesize title = _title;
@synthesize photos = _photos;
 
- (id) initWithTitle:(NSString *)title photos:(NSArray *)photos {
    if ((self = [super init])) {
        self.title = title;
        self.photos = photos;
        for(int i = 0; i < _photos.count; ++i) {
            Photo *photo = [_photos objectAtIndex:i];
            photo.photoSource = self;
            photo.index = i;
        }        
    }
    return self;
}
 
- (void) dealloc {
    self.title = nil;
    self.photos = nil;    
    [super dealloc];
}
 
#pragma mark TTModel
 
- (BOOL)isLoading { 
    return FALSE;
}
 
- (BOOL)isLoaded {
    return TRUE;
}
 
#pragma mark TTPhotoSource
 
- (NSInteger)numberOfPhotos {
    return _photos.count;
}
 
- (NSInteger)maxPhotoIndex {
    return _photos.count-1;
}
 
- (id<TTPhoto>)photoAtIndex:(NSInteger)photoIndex {
    if (photoIndex < _photos.count) {
        return [_photos objectAtIndex:photoIndex];
    } else {
        return nil;
    }
}
@end

First we create a constructor that takes a title and a list of photos. We squirrel those off, and then we go through each photo object and set its photo source to ourselves, and the index to the position of the photo in our list. If you recall, these are the two properties in TTPhoto that we said we’d mention later.

Next we have to implement a couple methods in TTModel that would be handy if we were retrieving the list of photos from a remote source. Since we’re hardcoding them in, we just return that we’ve already loaded them and go on our way!

Finally we have the most important methods: the required TTPhotoSource methods. However these are quite simple to implement – we just return the appropriate photo from the array.

Adding Some Photos

The next thing we need to do is actually add some photos to work with into the project! You can use some sample photos that I have gathered for this tutorial, or you can use your own. If you create your own, for each photo make a thumbnail versions 75×75, a small versions the size of the iPhone screen (in portrait or landscape), and a large version as large as you’d like (and upload the large version to a web server somewhere).

Once you’ve downloaded or created the images, drag them to the Resources folder in your project, make sure “Copy items into destination group’s folder (if needed)” is checked, and click add. Then add the following code to the bottom of PhotoSet.m (you’ll need to tweak this if you created your own images):

static PhotoSet *samplePhotoSet = nil;
 
+ (PhotoSet *) samplePhotoSet {
    @synchronized(self) {
        if (samplePhotoSet == nil) {
            Photo *mathNinja = [[[Photo alloc] initWithCaption:@"Math Ninja" 
                                                      urlLarge:@"http://www.raywenderlich.com/downloads/math_ninja_large.png" 
                                                      urlSmall:@"bundle://math_ninja_small.png" 
                                                      urlThumb:@"bundle://math_ninja_thumb.png" 
                                                          size:CGSizeMake(1024, 768)] autorelease];
            Photo *instantPoetry = [[[Photo alloc] initWithCaption:@"Instant Poetry" 
                                                          urlLarge:@"http://www.raywenderlich.com/downloads/instant_poetry_large.png" 
                                                          urlSmall:@"bundle://instant_poetry_small.png" 
                                                          urlThumb:@"bundle://instant_poetry_thumb.png" 
                                                              size:CGSizeMake(1024, 748)] autorelease];
            Photo *rpgCalc = [[[Photo alloc] initWithCaption:@"RPG Calc" 
                                                    urlLarge:@"http://www.raywenderlich.com/downloads/rpg_calc_large.png" 
                                                    urlSmall:@"bundle://rpg_calc_small.png" 
                                                    urlThumb:@"bundle://rpg_calc_thumb.png" 
                                                        size:CGSizeMake(640, 920)] autorelease];
            Photo *levelMeUp = [[[Photo alloc] initWithCaption:@"Level Me Up" 
                                                      urlLarge:@"http://www.raywenderlich.com/downloads/level_me_up_large.png" 
                                                      urlSmall:@"bundle://level_me_up_small.png" 
                                                      urlThumb:@"bundle://level_me_up_thumb.png" 
                                                          size:CGSizeMake(1024, 768)] autorelease];
            NSArray *photos = [NSArray arrayWithObjects:mathNinja, instantPoetry, rpgCalc, levelMeUp, nil];
            samplePhotoSet = [[self alloc] initWithTitle:@"My Apps" photos:photos];
        }
    }
    return samplePhotoSet;
}

This is just a helper method to create a photo set with four hard-coded images, using the constructors we created for the classes.

Creating the TTPhotoViewController

Believe it or not, that was the hardest part – everything from here on out is cake.

Let’s start by creating a TTPhotoViewController. Go to File\New File, choose Cocoa Touch Class\Objective-C class, make sure Subclass of NSObject is selected, and click Next. Name the new file PhotoViewController.m (and make sure also create PhotoViewController.h is selected), and click Finish.

Replace PhotoViewController.h with the following:

#import <Foundation/Foundation.h>
#import <Three20/Three20.h>
 
@class PhotoSet;
 
@interface PhotoViewController : TTPhotoViewController {
    PhotoSet *_photoSet;
}
 
@property (nonatomic, retain) PhotoSet *photoSet;
 
@end

Here we simply derive from TTPhotoViewController and keep track of the PhotoSet we wish to display.

Then replace TTPhotoViewController.m with the following:

#import "PhotoViewController.h"
#import "PhotoSet.h"
 
@implementation PhotoViewController
@synthesize photoSet = _photoSet;
 
- (void) viewDidLoad {
    self.photoSource = [PhotoSet samplePhotoSet];
}
 
- (void) dealloc {
    self.photoSet = nil;    
    [super dealloc];
}
 
@end

All we have to do here is set the photoSource property (part of TTPhotoViewController) to our class that implements TTPhotoSource in viewDidLoad. That’s it – yowza!

So let’s get this view controller displayed so we can admire all our cool photos!

Displaying the TTPhotoViewController

The last step is to replace PhotoViewerAppDelegate.m with the following:

#import "PhotoViewerAppDelegate.h"
#import "PhotoViewController.h"
 
@implementation PhotoViewerAppDelegate
@synthesize window;
 
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
 
    [[TTURLRequestQueue mainQueue] setMaxContentLength:0];
 
    TTNavigator *navigator = [TTNavigator navigator];
    navigator.window = window;
 
    TTURLMap *map = navigator.URLMap;
    [map from:@"tt://appPhotos" toSharedViewController:[PhotoViewController class]];
 
    [navigator openURLAction:[TTURLAction actionWithURLPath:@"tt://appPhotos"]];
 
    [window makeKeyAndVisible];
 
    return YES;
}
 
- (void)dealloc {
    [window release];
    [super dealloc];
}
 
@end

The real meat is in application:didFinishLaunchingWithOptions. The first thing we do here is set the maximum content length of the TTURLRequestQueue to 0, which means unlimited. If we forget to do this, Three20 will retrieve an error if we try to retrieve a file that is over a set limit. This is intended to warn you in case you didn’t mean to download something that big, but we don’t want those restrictions for the sake of this tutorial.

Next we start up a TTNavigator and a mapping from the tt://appPhotos URL to our PhotoViewController, and then navigate to that URL. If you’re unfamiliar how this works, you may wish to review the Introduction to Three20 tutorial.

And that’s it! Compile and run the sucker, and you should be able to navigate through your set of images with style! You’ll notice that the app shows the small images that ship with the app at first, then downloads the high res images in the background for you. Pretty cool, eh?

Photo Viewer Screenshot 1
Photo Viewer Screenshot 2
Photo Viewer Screenshot 3
Photo Viewer Screenshot 4

Where To Go From Here?

Here is a sample project with all of the code we’ve developed in the above Three20 tutorial.

The TTPhotoViewer is just one of the many great pieces of functionality available in Three20. A great way to get an overview of what’s available is to go through the TTCatalog project that comes with Three20.

Additionally, if you want to make a photo viewer but don’t want to use Three20, you could always write your own. The UIScrollView class actually contains much of the functionality to make this possible – see WWDC talk #104 for some great info on this.

If you have any comments or questions just drop a line below – and let me know how you’re using or planning to use Three20 in your apps!

Ray Wenderlich

Ray is part of a great team - the raywenderlich.com team, a group of over 100 developers and editors from across the world. He and the rest of the team are passionate both about making apps and teaching others the techniques to make them.

When Ray’s not programming, he’s probably playing video games, role playing games, or board games.

Other Items of Interest

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Come check out Alt U

Our Books

Our Team

Video Team

... 9 total!

Swift Team

... 15 total!

iOS Team

... 47 total!

Android Team

... 15 total!

OS X Team

... 12 total!

Apple Game Frameworks Team

... 15 total!

Unity Team

... 11 total!

Articles Team

... 8 total!