Intro
A few weeks ago, I posted a video and a follow-up blog on why I think using Windows Azure as a middle-tier or backend platform for mobile is compelling. Remaining on the topic of Azure and mobile, I wanted to write a series of blog posts that show you how to use various Azure services to build a native mobile app. Through this series, I’m going to build an iPhone app called SpeakEasy. The app will keep track of speakers/presenters and various events that they speak at. It’s a simple application but hopefully one that will show you how you can leverage Windows Azure when it’s time for you to build your next great mobile app.
In the first part of the series, I’ll go through using the Windows Azure Table Storage and Blob Storage. The full series (as of this writing) will look like this:
Azure Tables and Blobs Setup
In order to get started, we’ll need some data. Since setting up the tables and blob containers isn’t the focus of this blog post, I am just going to post the code I used to populate the tables and blob container with minimal explanation.
SpeakerEntity.cs
The SpeakerEntity class is a simple class used to hold our speaker data. A speaker entity will be held in the speakers partition and will use a guid as its row identifier.
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using Microsoft.WindowsAzure.StorageClient;
6:
7: namespace SpeakEasyStorageSetup
8: {
9: public class SpeakerEntity : TableServiceEntity
10: {
11: public SpeakerEntity(string name, string title, string bio, string imageUrl)
12: {
13: this.PartitionKey = "speakers";
14: this.RowKey = Guid.NewGuid().ToString();
15:
16: this.Name = name;
17: this.Title = title;
18: this.Bio = bio;
19: this.ImageUrl = imageUrl;
20: }
21:
22: #region Properties
23: public string Name { get; set; }
24: public string Title { get; set; }
25: public string Bio { get; set; }
26: public string ImageUrl { get; set; }
27: #endregion
28: }
29: }
EventEntity.cs
The EventEntity class will be used to hold event data can will be in the events partition. Its row key will also be a guid.
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using Microsoft.WindowsAzure.StorageClient;
6:
7: namespace SpeakEasyStorageSetup
8: {
9: public class EventEntity : TableServiceEntity
10: {
11: public EventEntity(DateTime eventDate, string eventName,
12: string eventLocation, string eventDescription, SpeakerEntity speaker)
13: {
14: this.PartitionKey = "Events";
15: this.RowKey = Guid.NewGuid().ToString();
16:
17: this.EventDate = eventDate;
18: this.EventName = eventName;
19: this.EventLocation = eventLocation;
20: this.EventDescription = eventDescription;
21: this.SpeakerKey = speaker.RowKey;
22: this.SpeakerName = speaker.Name;
23: }
24:
25: #region Properties
26:
27: public DateTime EventDate { get; set; }
28: public string EventName { get; set; }
29: public string EventLocation { get; set; }
30: public string EventDescription { get; set; }
31: public string SpeakerKey { get; private set; }
32: public string SpeakerName { get; private set; }
33:
34: #endregion
35: }
36: }
37:
Program.cs
The code below takes care of creating the table and blob storage in my Azure account. First, the blob storage container is created and permissions are set to public access. The container speakers holds each speaker’s images (note that the images are embedded resources in my project). After the blob container is created and populated, I then create a table with two partitions, speakers and events. The speakers and events are then populated with some randomized data.
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Configuration;
6: using Microsoft.WindowsAzure;
7: using Microsoft.WindowsAzure.StorageClient;
8: using Microsoft.WindowsAzure.StorageClient.Tasks;
9: using System.IO;
10: using System.Reflection;
11:
12: namespace SpeakEasyStorageSetup
13: {
14: class Program
15: {
16: static string[] imgs = new string[] { "BTubalinal.jpg", "BJohnson.jpg", "TNielsen.jpg", "MMorse.jpg",
17: "MOmar.jpg", "DOrlova.jpg", "CJones.jpg", "EGardner.jpg" };
18:
19: static void Main(string[] args)
20: {
21: //get the storage account from a connection string
22: CloudStorageAccount storageAccount = CloudStorageAccount.Parse(
23: ConfigurationManager.ConnectionStrings["StorageConnectionString"].ConnectionString);
24:
25: //create the blob storage
26: var blobClient = storageAccount.CreateCloudBlobClient();
27: string containerName = "speakers";
28: var blobContainer = blobClient.GetContainerReference(containerName);
29:
30: if (blobContainer.CreateIfNotExist())
31: {
32: blobContainer.SetPermissions( new BlobContainerPermissions()
33: {
34: PublicAccess = BlobContainerPublicAccessType.Container
35: });
36:
37: Console.WriteLine(" Blob container created. Now populating ...");
38: PopulateBlob(blobContainer);
39: }
40:
41:
42: //create the table 'events'
43: var tableClient = storageAccount.CreateCloudTableClient();
44: string eventsTable = "events";
45: if (tableClient.CreateTableIfNotExist(eventsTable))
46: {
47: Console.WriteLine(" Table created. Now populating...");
48: PopulateTables(tableClient, blobContainer.Uri.ToString());
49: }
50:
51: Console.WriteLine(" Completed.");
52: Console.ReadLine();
53:
54: }
55:
56: static void PopulateBlob(CloudBlobContainer blobContainer)
57: {
58: Assembly assm = Assembly.GetExecutingAssembly();
59:
60: for (int i = 0; i < imgs.Length; i++)
61: {
62: string imgName = imgs[i];
63: using (Stream imgStream = assm.GetManifestResourceStream(
64: string.Format("SpeakEasyStorageSetup.images.{0}", imgName)))
65: {
66: var blob = blobContainer.GetBlobReference(imgName);
67: blob.UploadFromStream(imgStream);
68: }
69: }
70: }
71:
72: static void PopulateTables(CloudTableClient tableClient, string imgBaseUrl)
73: {
74: string[] names = new string[] { "Bart Tubalinal", "Bert Johnson", "Travis Nielsen",
75: "Matt Morse", "Mo Omar", "Darya Orlova", "Callie Jones", "Ela Gardner" };
76: string[] titles = new string[] { "Solutions Architect", "Solutions Architect", "Principal Consultant",
77: "Practice Manager", "Principal Consultant", "Consultant", "Consultant", "Consultant" };
78:
79: TableServiceContext serviceCtx = tableClient.GetDataServiceContext();
80:
81: string defaultDescription = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit.
82: In commodo fermentum adipiscing. Maecenas id dolor velit, sit amet commodo ipsum. Cras lacus dui, bibendum
83: et commodo at, scelerisque ac enim. Mauris nec purus ante. Vestibulum ante ipsum primis in faucibus orci luctus
84: et ultrices posuere cubilia Curae; Praesent ut lobortis lorem. Nullam tempor, erat dignissim sodales blandit,
85: turpis velit fringilla augue, posuere ultricies augue augue quis quam. Quisque sed felis augue. Nam sit amet
86: enim tortor. Aenean ut fringilla leo. Phasellus tincidunt turpis ut diam dictum id ullamcorper nunc faucibus.
87: Nunc semper dapibus orci sit amet suscipit. Maecenas viverra est volutpat lectus lacinia suscipit. Nullam
88: sapien elit, fermentum et pretium vitae, sodales quis eros. Ut et nulla urna. Nunc congue viverra nisi, eget
89: porta est lobortis non. Cras in turpis urna, sed porta ipsum. In suscipit purus a nibh facilisis eget
90: pharetra odio rhoncus. Morbi faucibus blandit mattis.";
91:
92: List<SpeakerEntity> speakers = new List<SpeakerEntity>();
93: for (int i = 0; i < names.Length; i++)
94: {
95: SpeakerEntity speaker = new SpeakerEntity(names[i], titles[i],
96: string.Format("{0} is a {1} in the {2}", names[i], titles[i], defaultDescription),
97: string.Format("{0}/{1}", imgBaseUrl, imgs[i]));
98: speakers.Add(speaker);
99:
100: serviceCtx.AddObject(" events", speaker);
101: }
102:
103: serviceCtx.SaveChangesWithRetries(System.Data.Services.Client.SaveChangesOptions.Batch);
104:
105: string[] eventNames = new string[] { "Mobile Mondays", "iOS Meetup", "Windows Phone Meetup",
106: "Android Meetup", "HTML5 Meetup", "SharePoint Saturdays" };
107: string[] eventLocations = new string[] { "Chicago, IL", "Milwaukee, WI", "Madison, WI",
108: "Boston, MA", "Washington, DC", "Los Angeles, CA", "Las Vegas, NV", "New York City, NY" };
109:
110: List<EventEntity> events = new List<EventEntity>();
111: Random random = new Random();
112:
113: for (int i = 1; i <= 100; i++)
114: {
115: string eventName = eventNames[random.Next(0, eventNames.Length - 1)];
116: string eventLocation = eventLocations[random.Next(0, eventLocations.Length - 1)];
117: SpeakerEntity eventSpeaker = speakers[random.Next(0, speakers.Count - 1)];
118: DateTime date = CalcDate(random, DateTime.Now,
new DateTime(2012, 12, 31));
119:
120: if (!events.Exists(delegate(EventEntity e)
121: {
122: return (e.EventName == eventName &&
123: e.EventLocation == e.EventLocation &&
124: e.SpeakerKey == eventSpeaker.RowKey &&
125: e.EventDate == date);
126: }))
127: {
128: EventEntity e = new EventEntity(date, eventName, eventLocation,
129: defaultDescription, eventSpeaker);
130:
131: events.Add(e);
132: serviceCtx.AddObject(" events", e);
133: }
134: }
135:
136: serviceCtx.SaveChangesWithRetries(System.Data.Services.Client.SaveChangesOptions.Batch);
137:
138: }
139:
140: static DateTime CalcDate(Random random, DateTime minDate, DateTime maxDate)
141: {
142: TimeSpan timeSpan = maxDate - minDate;
143: TimeSpan randomSpan = new TimeSpan((long)(timeSpan.Ticks * random.NextDouble()));
144: return minDate + randomSpan;
145:
146: }
147: }
148: }
149:
After this code runs, I can now see that I have the following blobs and table data:
Blob Data

Table Data – Speakers

Table Data – Events

Windows Azure iOS Toolkit Prep
The next thing to do is to download and build the Windows Azure iOS Toolkit from GitHub. Despite what the Readme says, it doesn’t seem to have the binaries so you have to build yourself.
When you’re building the toolkit, make sure you set to build to the correct device you’ll be using the toolkit from, otherwise when you go to use the toolkit in the project, you’ll end up with linking errors. For example, if you’re testing on the simulator, make sure to build the toolkit with that selected as the target. After you’ve successfully built the toolkit, find the libwatoolkitios.a file and save it off somewhere where you can reference it later.
SpeakEasy iPhone App
Project Settings
Now with all of the prep work out of the way, it’s time to start building the app. Open XCode and create a new project. SpeakEasy will have two tabs, one for events and one for speakers so select Tabbed Application for the project type. The following options should be set for your project:
- Product Name:SpeakEasy
- Company Identifier:com.yourcompanyname (mine is set to com.pointbridge – I wrote this code prior to our acquisition)
- Class Prefix:leave as default
- Device Family:iPhone
- Use Storyboard:checked
- Use Automatic Reference Counting:checked
- Include Unit Tests: unchecked
Click through the rest of the New Project wizard. The next step is to set up the structure of your project. In the Project Navigator view, you should see the SpeakEasy project with three Groups: SpeakEasy, Frameworks and Products. Here are some basic steps to set up the project.
- Add a new group called lib. Add the libwatoolkitios.afile and the toolkit’s headers folder to this group.
- Select the SpeakEasy project from the Project Navigator. This should bring up the project settings in the standard editor.
- In the standard editor, make sure you’ve got the SpeakEasy project selected and go to the Build Settingstab.
- Search for the Other Linker Flags build setting and add the following settings:
- Now select the SpeakEasy target and select the Build Phasestab.
- Open up the section Link Binary with Libraries and make sure libwatoolkitios.a is there. Then add a link to libxml2.2.dylib (or higher).
Your project and settings should now look a lot like this:

Setting up the Project Groups and Files
Open the SpeakEasy group in the Project Navigator and add the following three groups:
- Model – the classes we’ll add to this group are going to be used to retrieve the data from Azure and model our events and speakers
- ViewControllers – various view controllers we’ll create to present the model to our views
- Views – for our custom view classes
Open up the file MainStoryboard.storyboard and delete both the First and Second View Controllers. Also delete the following files from the Project Navigator:
- FirstViewController.h
- FirstViewController.m
- SecondViewController.h
- SecondViewController.m
Finally, under the Supporting Files group, add a new plist file called SpeakEasy-AzureSettings.plist. This will hold our Azure settings. To this file, add two new keys, AccessKey and StorageAccount (both type String), and set these values to your appropriate Azure account settings.
Your project structure should now look like this:

The next step is to build our model. We basically need three classes: a class to model our speaker entities, a class to model our event entities, and a data access class that takes care of pulling the data from Azure. All of these classes should be in the Model group of the project.
Model Files
SESpeaker
This class is what models our speaker entity. The interface and class looks like this:
1: //SESpeaker.h
2:
3: #import <Foundation/Foundation.h>
4: #import "WATableEntity.h"
5:
6: @class SESpeaker;
7:
8: @protocol SESpeakerDelegate <NSObject>
9:
10: @optional
11:
12: -(void) speaker:(SESpeaker *)speaker didLoadImage:(UIImage *)image;
13:
14: @end
15:
16: @interface SESpeaker : NSObject {
17: @private
18: NSString *_partitionKey;
19: NSString *_rowKey;
20: NSDate *_timestamp;
21: }
22: @property (retain) id delegate;
23: @property(readonly) NSString *partitionKey;
24: @property(readonly) NSString *rowKey;
25: @property(readonly) NSDate *timestamp;
26: @property(nonatomic, retain) NSString *name;
27: @property(nonatomic, retain) NSString *title;
28: @property(nonatomic, retain) NSString *bio;
29: @property(nonatomic, retain) NSURL *imageUrl;
30: @property(nonatomic, retain) UIImage *image;
31:
32: -(id) initWithEntity:(WATableEntity *) entity;
33:
34: @end
35:
36: //SESpeaker.m
37:
38: #import "SESpeaker.h"
39: #import "SEData.h"
40:
41: @implementation SESpeaker
42: @synthesize delegate;
43: @synthesize partitionKey = _partitionKey;
44: @synthesize rowKey = _rowKey;
45: @synthesize timestamp = _timestamp;
46: @synthesize name;
47: @synthesize title;
48: @synthesize bio;
49: @synthesize imageUrl;
50: @synthesize image;
51:
52: -(id) initWithEntity:(WATableEntity *)entity
53: {
54: if(self = [super init])
55: {
56: _partitionKey = entity.partitionKey;
57: _rowKey = entity.rowKey;
58: _timestamp = entity.timeStamp;
59: name = [entity objectForKey:@" Name"];
60: title = [entity objectForKey:@" Title"];
61: bio = [entity objectForKey:@" Bio"];
62: imageUrl = [NSURL URLWithString:[entity objectForKey:@"
ImageUrl"]];
63:
64: SEData *data = [SEData sharedManager];
65: [data fetchBlobDataFromURL:imageUrl withCompletionHandler:^(NSData *imageData, NSError *error) {
66: if (!error)
67: {
68: image = [UIImage imageWithData:imageData];
69: [[self delegate] speaker:self didLoadImage:image];
70: }
71: }];
72: }
73:
74: return self;
75: }
76:
77: -(void) dealloc
78: {
79: delegate = nil;
80: _partitionKey = nil;
81: _rowKey = nil;
82: _timestamp = nil;
83: name = nil;
84: title = nil;
85: bio = nil;
86: imageUrl = nil;
87: image = nil;
88: }
89:
90: @end
91:
The interface basically defines a set of properties that I’ll populate with the values returned for a speaker entity from our Azure table storage. This population occurs in initWithEntity:. The WATableEntity class from the iOS toolkit is for working with entities retrieved from Azure table storage. It has a few basic properties that all entities in a table have (partition and row keys and timestamp). To access your own entity properties (the ones we created with our SpeakerEntity class in the setup and population console app), we send an objectForKey: message to the entity with the name of the property we’re retrieving and then assign its value to a property of the SESpeaker class.
For the speaker’s image, notice that I have properties for both the image url and the actual image. The image url is what I stored with the speaker entity in the table. When I initialize an SESpeaker, I use this image url to grab the image via my SEData data access class (lines 64-71). That class has a helper method for retrieving blob data from my Azure blob container. After the image is retrieved, I then send a message that the image is ready to any delegate implementing the SESpeakerDelegate protocol assigned to this entity instance.
SEEvent
The SEEvent interface and class is used to model the EventEntity. The h/m files look like this:
1: //SEEvent.h
2: #import <Foundation/Foundation.h>
3: #import "WATableEntity.h"
4:
5: @interface SEEvent : NSObject {
6: @private
7: NSString *_partitionKey;
8: NSString *_rowKey;
9: NSDate *_timeStamp;
10:
11: }
12:
13: @property(readonly) NSString *partitionKey;
14: @property(readonly) NSString *rowKey;
15: @property(readonly) NSDate *timeStamp;
16: @property(nonatomic, retain) NSString *eventName;
17: @property(nonatomic, retain) NSString *eventLocation;
18: @property(nonatomic, retain) NSString *eventDescription;
19: @property(nonatomic, retain) NSDate *eventDate;
20: @property(nonatomic, retain) NSString *speakerKey;
21: @property(nonatomic, retain) NSString *speakerName;
22:
23: -(id) initWithEntity:(WATableEntity *) entity;
24: -(NSString*) eventDateAsString;
25:
26: @end
27:
28:
29: //SEEvent.m
30: #import "SEEvent.h"
31:
32: @implementation SEEvent
33: @synthesize partitionKey = _partitionKey;
34: @synthesize rowKey = _rowKey;
35: @synthesize timeStamp = _timeStamp;
36: @synthesize eventName;
37: @synthesize eventLocation;
38: @synthesize eventDescription;
39: @synthesize eventDate;
40: @synthesize speakerKey;
41: @synthesize speakerName;
42:
43: -(id) initWithEntity:(WATableEntity *) entity
44: {
45: if(self = [super init])
46: {
47: _partitionKey = entity.partitionKey;
48: _rowKey = entity.rowKey;
49: _timeStamp = entity.timeStamp;
50: eventName = [entity objectForKey:@" EventName"];
51: eventLocation = [entity objectForKey:@" EventLocation"];
52: eventDescription = [entity objectForKey:@" EventDescription"];
53: speakerKey = [entity objectForKey:@" SpeakerKey"];
54: speakerName = [entity objectForKey:@" SpeakerName"];
55:
56: //date comes back as a string; convert to an NSDate
57: NSString* eventDateString = [entity objectForKey:@"
EventDate"];
58: if(eventDateString)
59: {
60: NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
61: [dateFormat setDateFormat:@" yyyy-MM-dd'T'HH:mm:ss.SSSSSSS'Z'"];
62: eventDate = [dateFormat dateFromString:eventDateString];
63: }
64: }
65:
66: return self;
67: }
68:
69: - (void)dealloc
70: {
71: _partitionKey = nil;
72: _rowKey = nil;
73: _timeStamp =nil;
74: eventName = nil;
75: eventLocation = nil;
76: eventDescription = nil;
77: eventDate = nil;
78: speakerKey = nil;
79: speakerName = nil;
80: }
81:
82: -(NSString*) eventDateAsString
83: {
84: NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
85: [dateFormatter setDateStyle:NSDateFormatterShortStyle];
86: return [dateFormatter stringFromDate:eventDate];
87: }
88:
89: @end
90:
This class functions the same way as SESpeaker, except that it doesn’t do any loading of any blobs. It still uses a WATableEntity to initialize itself with an entity from the Azure table storage (in this case, an EventEntity object).
SEData
Finally, we have SEData. SEData is basically my data access class used to retrieve items from the Azure storage services. Below is the interface definition (I will go through the implementation separately):
1: #import <Foundation/Foundation.h>
2: #import "WAAuthenticationCredential.h"
3: #import "WACloudStorageClient.h"
4: #import "WATableFetchRequest.h"
5: #import "WABlobContainer.h"
6: #import "WABlob.h"
7: #import "SEEvent.h"
8: #import "SESpeaker.h"
9:
10: #define kEventsStorageTable @"events"
11:
12: @interface SEData : NSObject{
13: @private
14: WAAuthenticationCredential *_credential;
15: WACloudStorageClient *_storageClient;
16:
17: NSMutableArray *_events;
18: NSMutableArray *_speakers;
19: }
20:
21: -(void)fetchEventsWithCompletionHandler:(void (^)(NSMutableArray *events, NSError *error))block;
22: -(void)fetchSpeakersWithCompletionHandler:(void (^)(NSMutableArray *speakers, NSError *error))block;
23: -(void)fetchSpeakerWithRowKey:(NSString *)rowKey withCompletionHandler:(void (^)(SESpeaker *speaker, NSError *error))block;
24: -(void)fetchBlobDataFromURL:(NSURL *)imageUrl withCompletionHandler:(void (^)(NSData *blobData, NSError *error))block;
25:
26: +(SEData*)sharedManager;
27:
28: @end
A WAAuthenticationCredential are the credentials that are used to access Azure and the WACloudStorageClient is primarily a façade that allows you to invoke operations on and return data from Azure storage. The interface also keeps an array of events and speakers. There are four methods for the interface to retrieve all events, all speakers, a single speaker, and a blob. The class also has a static method sharedManager that returns an instance of the SEData class. This is part of implementing this class as a Singleton.
Below is the full implementation:
1: #import "SEData.h"
2:
3: @interface SEData (hidden)
4: -(void) privateInit;
5: -(void) fetchEntitiesFromTable:(NSString *)table WithFilter:(NSString *)filter withCompletionHandler:(void (^)(NSArray *, NSError *))block;
6: @end
7:
8: @implementation SEData
9:
10: static SEData *sharedDataManager = nil;
11:
12: #pragma mark - Singleton Implementation
13:
14: +(SEData*)sharedManager
15: {
16: if(sharedDataManager == nil){
17: sharedDataManager = [[super allocWithZone:NULL] init];
18: [sharedDataManager privateInit];
19: }
20:
21: return sharedDataManager;
22: }
23:
24: + (id)allocWithZone:(NSZone *)zone
25: {
26: return [self sharedManager];
27: }
28:
29: - (id)copyWithZone:(NSZone *)zone
30: {
31: return self;
32: }
33:
34: #pragma mark - Public Methods
35:
36:
37: -(void)fetchEventsWithCompletionHandler:(void (^)(NSMutableArray *events, NSError *error))block;
38: {
39: if(!_events)
40: {
41: [self fetchEntitiesFromTable:kEventsStorageTable WithFilter:@"
PartitionKey eq 'Events'" withCompletionHandler:^(NSArray *entities, NSError *error) {
42: if(error)
43: {
44: block(nil, error);
45: }
46: else
47: {
48: _events = [[NSMutableArray alloc] initWithCapacity:entities.count];
49:
50: for (WATableEntity *entity in entities)
51: {
52: SEEvent *event = [[SEEvent alloc] initWithEntity:entity];
53:
54: [_events addObject:event];
55: }
56:
57: NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"
eventDate" ascending:YES];
58: NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
59: _events = [NSMutableArray arrayWithArray:[_events sortedArrayUsingDescriptors:sortDescriptors]];
60:
61: block(_events, nil);
62:
63: }
64: }];
65: }
66: else
67: {
68: block(_events, nil);
69: }
70: }
71:
72: -(void)fetchSpeakersWithCompletionHandler:(void (^)(NSMutableArray *speakers, NSError *error))block
73: {
74: if(!_speakers)
75: {
76: [self fetchEntitiesFromTable:kEventsStorageTable WithFilter:@"
PartitionKey eq 'speakers'" withCompletionHandler:^(NSArray *entities, NSError *error) {
77: if(error)
78: {
79: block(nil, error);
80: }
81: else
82: {
83: _speakers = [[NSMutableArray alloc] initWithCapacity:entities.count];
84:
85: for (WATableEntity *entity in entities)
86: {
87: SESpeaker *speaker = [[SESpeaker alloc] initWithEntity:entity];
88:
89: [_speakers addObject:speaker];
90: }
91: block(_speakers, nil);
92:
93: }
94: }];
95:
96: }
97: else
98: {
99: block(_speakers, nil);
100: }
101:
102: }
103:
104: -(void) fetchSpeakerWithRowKey:(NSString *)rowKey withCompletionHandler:(void (^)(SESpeaker *speaker, NSError *error))block;
105: {
106: if(!_speakers)
107: {
108: [self fetchEntitiesFromTable:kEventsStorageTable
109: WithFilter:[NSString stringWithFormat:@"
PartitionKey eq 'speakers' and RowKey eq '%@'", rowKey]
110: withCompletionHandler:^(NSArray *entities, NSError *error) {
111: if(error)
112: {
113: block(nil, error);
114: }
115: else
116: {
117: SESpeaker *speaker = [[SESpeaker alloc] initWithEntity:[entities objectAtIndex:0]];
118: block(speaker, nil);
119: }
120: }];
121:
122: }
123: else
124: {
125: NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
126: SESpeaker *speaker = (SESpeaker*)evaluatedObject;
127: return ([speaker.rowKey isEqualToString:rowKey]);
128: }];
129:
130: NSArray *results = [_speakers filteredArrayUsingPredicate:predicate];
131:
132: if (results) {
133: block([results objectAtIndex:0], nil);
134: }
135: else {
136: //todo: load from speakers
137: block(nil, nil);
138: }
139: }
140: }
141:
142: -(void) fetchBlobDataFromURL:(NSURL *)imageUrl withCompletionHandler:(void (^)(NSData *blobData, NSError *error))block
143: {
144: WABlobContainer *container = [[WABlobContainer alloc] initContainerWithName:@"
speakers"];
145:
146: NSString *imageUrlString = [imageUrl absoluteString];
147: NSString *filename = [imageUrlString substringFromIndex:[imageUrlString rangeOfString:@"
/" options:NSBackwardsSearch].location + 1];
148:
149: WABlob *blob = [[WABlob alloc] initBlobWithName:filename URL:imageUrlString container:container];
150:
151: [_storageClient fetchBlobData:blob withCompletionHandler:^(NSData *data, NSError *error) {
152: block(data, error);
153: }];
154: }
155:
156: #pragma mark - Private Methods
157: -(void) privateInit
158: {
159: NSString *path = [[NSBundle mainBundle] pathForResource:@"
SpeakEasy-AzureSettings" ofType:@"plist"];
160:
161: NSDictionary *settings = [[NSDictionary alloc] initWithContentsOfFile:path];
162:
163: _credential = [WAAuthenticationCredential
164: credentialWithAzureServiceAccount:[settings objectForKey:@"
StorageAccount"]
165: accessKey:[settings objectForKey:@"AccessKey"]];
166:
167: _storageClient = [WACloudStorageClient storageClientWithCredential:_credential];
168: }
169:
170:
171: -(void) fetchEntitiesFromTable:(NSString *)table WithFilter:(NSString *)filter withCompletionHandler:(void (^)(NSArray *, NSError *))block
172: {
173: WATableFetchRequest* fetchRequest = [WATableFetchRequest fetchRequestForTable:table];
174: fetchRequest.filter = filter;
175: [_storageClient fetchEntities:fetchRequest withCompletionHandler:^(NSArray *entities, NSError *error)
176: {
177: block(entities, error);
178: }];
179:
180: }
181:
182: @end
183:
Lines 3-5 just adds a few hidden (not private) methods using a category (hidden) to my SEData class that should only be called internally. privateInit, implemented in lines 157-168, sets up my _credential and _storageClient variables which will be used in other methods to grab the data from Azure. It uses the information I stored in the SpeakEasy-AzureSettings.plist file to create the credentials.
fetchEntitiesFromTable:withFilter:withCompletionHandler: uses a WATableFetchRequest to retrieve entities from table storage. The filter can be used to limit the entities retrieved and has the same filter syntax you’d use when using the $filter parameter in OData. Finally, when the entities have been retrieved (or if an error has occurred), the completion handler block that is passed is called.
SEData is implemented as a singleton in lines 14-32 as per the Apple guideline.
Lines 142-154 is the implementation of fetchBlobDataFromURL:withCompletionHandler:. This method creates a WABlobContainer instance for the speakers container, then creates an instance of a WABlob using the filename and blob url in that container. It then uses the _storageClient to make a request to retrieve the binary data for this blob.
fetchEventsWithCompletionHandler:, on lines 37-70, takes care of retrieving the EventEntity objects from Azure. It uses the filter PartionKey eq ‘Events’ to make sure we’re only grabbing the EventEntity objects from our table and not any of the SpeakerEntity objects. If the fetch request I successful, I iterate through the WATableEntity array that’s returned and create my own array of strongly-typed SEEvent objects. Once I’m done with that, I sort the events by date and then send the array back to the completion handler block.
fetchSpeakersWithCompletionHandler:, on lines 72-102, functions identically as the method for events except we filter only for the SpeakerEntity objects to create an array of SESpeaker objects and that there’s no sorting.
Finally, fetchSpeakerWithRowKey:WithCompletionHandler: retrieves an individual SESpeaker object. If we’ve already got a list of all the speakers, then it uses that a predicate to find the correct object within that array. If the speaker array hasn’t yet been loaded, then the code goes back to Azure and finds the correct speaker using a filter.
Conclusion
The model code above is technically all the code that directly interacts with Windows Azure. In Part 2, I cover creating the user interface for the iPhone app. As a sneak peak, that will look like below, so if you want to know how I built it, please continue with the series.
