Using Azure Storage Services from an iPhone App - Part 2: Building the User Interface

Intro

This is part two of the first series of posts I’m writing on developing native mobile apps using the Windows Azure platform. In the first part, I covered the basic setup of my azure storage, I walked through the steps of building the iOS Windows Azure Toolkit and using it in a project, and I wrote the code to use the toolkit in my model classes for the SpeakEasy app, a fictitious app that keeps track of speakers and events.

This post covers building the UI for the app and, though not specifically about Azure, it does go through using the model classes I wrote from various view controllers. Since the rest of the series will be building off of this app, I thought it made sense to go over how the UI was built.

As a reminder, this series includes:

Events and Speakers View Controllers

As I said in the first part of this series, SpeakEasy is a Tabbed Application with two tabs – one for Events and the other for Speakers. Below is how each will look like:

image  image

The first thing we want to do is to build the view for the Events Table View Controller. This view is used to present a list of upcoming events and is the view that’s presented in the first tab of the application. To begin building this view, follow these steps:

  1. Open up the MainStoryboard.storyboard file and drag a Table View Controllerto the canvas.
  2. With the new Table View Controller selected, from the Editor Menu, select Embed In > Navigation Controller. This will add a Navigation Controllerto the app so that any view that is added after the navigation controller will be handled as part of the navigation stack.
  3. Now, with the Tab Bar Controller selected (the initial view controller for the app), Control + Click from the Tab Bar Controller and drag to the new Navigation Controller that was just added to the storyboard. From the popup menu, select ‘Relationship – viewControllers’.
  4. In the Events Table View Controller, set the Title of the Navigation Item to ‘Events’.
  5. In the Tab Bar Item on the Navigation Controller, set the title to ‘Events’ and the Image to ‘first.png’.

Building the view for Speakers Table View Controller is nearly identical as above except for making the title to ‘Speakers’ instead and using ‘second.png’ as the Image for the Tab Bar Item. After you’ve followed these steps, the storyboard should look like this:

image

 

Events Table View

The next step is to design the Events Table View. Since all the content in this view will be presented the same way, the Events Table View will use a single prototype cell. Configure the Table View to have Dynamic Prototypes for the Content and 1 Prototype Cell. Select the Prototype Cell and configure the following:

  • Style:Custom
  • Identifier:EventTableViewCell
  • Accessory:Disclosure Indicator
  • Row Height: 92

The custom style indicates that we’ll be using a style that is not one of the out of the box styles provided and the Identifier will be used to indicate in code what cell we are using to map values to (later). Finally, we have a Disclosure Indicator so that we can indicate to the user that there’s more information if they click on the cell.

Now from the Object Library, drag four Label objects to the Prototype Cells container in the Events View Controller. The four labels will be used to display the Event’s name, date, speaker and summary. The following are how I configured each label’s properties (only the changes I made from the default values are indicated):

imageName label

  • Font:System Bold 19.0
  • X, Y, Width, Height: 12, 7, 218, 21

Date label

  • Font:System 11.0
  • Text Color:Blue-ish
  • X, Y, Width, Height: 238, 7, 57, 21

Speaker label

  • Font:System Bold 19.0
  • Text Color:Blue-ish
  • X, Y, Width, Height: 13, 28, 189, 21

Summary label

  • Lines: 3
  • Font:System 14.0
  • X, Y, Width, Height: 12, 49, 283, 39

When you’re done, the Events Table View should look like the image on the right.

EventTableViewCell Class

Because we’ve created a prototype cell that isn’t using a standard style and added four labels that need to be reference-able in some way by the code, we need to create add a new class to the project that subclasses UITableViewCell. Right click the Views group from the Project Navigator and add a new UIViewController subclass file. Name the class EventTableViewCell and make it a subclass of UITableViewCell.

EventTableViewCell.h should look like this:

  1: #import <UIKit/UIKit.h> 
  2:  
  3: @interface EventTableViewCell : UITableViewCell 
  4:  
  5: @property(nonatomic, strong) IBOutlet UILabel *eventNameLabel; 
  6: @property(nonatomic, strong) IBOutlet UILabel *eventDescriptionLabel; 
  7: @property(nonatomic, strong) IBOutlet UILabel *eventDateLabel; 
  8: @property(nonatomic, strong) IBOutlet UILabel *eventSpeakerLabel; 
  9: @end

I create four properties, each as an IBOutlet that we can connect to the labels on the prototype cell we designed for the Events Table View.

The EventTableViewCell.m looks like this:

  1: #import "EventTableViewCell.h" 
  2:  
  3: @implementation EventTableViewCell 
  4: @synthesize eventNameLabel; 
  5: @synthesize eventDescriptionLabel; 
  6: @synthesize eventDateLabel; 
  7: @synthesize eventSpeakerLabel; 
  8:  
  9: - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier 
 10: { 
 11:     self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 
 12:     if (self) { 
 13:         // Initialization code 
 14:     } 
 15:     return self; 
 16: } 
 17:  
 18: - (void)setSelected:(BOOL)selected animated:(BOOL)animated 
 19: { 
 20:     [super setSelected:selected animated:animated]; 
 21: } 
 22:
 23: @end

The above doesn’t do anything except for synthesizing the properties we defined in the header and to override some of the default methods of the UITableViewCell.

To use this new class, go back to the storyboard, select the prototype cell and set its Class to EventTableViewCell. Then open the Assistant Editor to EventTableViewCell.h and connect each IBOutlet to its appropriate label:

image

 

EventsTableViewController Class

In order to populate the table using the model classes I created in the first part of this blog series, I need to create a subclass of UITableViewController that will take of the logic of retrieving SEEvent model instances and mapping them to the view. Right-click the ViewControllers group in the Project Navigator and add a new file. Using the UIViewController subclass template, create a new file called EventsTableViewController as a subclass of UITableViewController.

In the interface definition, add the following line: @property (nonatomic, retain) NSMutableArray *events; and synthesize this property in the class definition. This property will be used to store the SEEvent instances that is retrieved from Azure.

In the class implementation, change viewDidLoad to this:

  1: - (void)viewDidLoad 
  2: { 
  3:     [super viewDidLoad];     
  4:      
  5:     SEData *data = [SEData sharedManager]; 
  6:     [data fetchEventsWithCompletionHandler:^(NSMutableArray *theEvents, NSError *error) { 
  7:          
  8:         if(error) { 
  9:             UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" message:[error localizedDescription]  
 10:                                                            delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; 
 11:             [alert show]; 
 12:              
 13:         }  
 14:         else { 
 15:             self.events = theEvents; 
 16:          
 17:             [self.tableView reloadData]; 
 18:         } 
 19:     }]; 
 20: }

Basically, what this does is it uses our SEData singleton and uses the method fetchEventsWithCompletionHandler: to attempt to retrieve all of the EventEntity objects from Azure. If it fails, an alert message will be shown to the user. If the fetch succeeds, the events will be saved to this class’s events array and then a message will be sent to the table view to reload its data.

To handle the display of the table view’s data, the UITableViewDataSource protocol methods should be defined as below:

  1: - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 
  2: { 
  3:     return 1; 
  4: } 
  5:  
  6: - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 
  7: { 
  8:     return [events count]; 
  9: } 
 10:  
 11: - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
 12: { 
 13:     EventTableViewCell *cell = (EventTableViewCell *)[tableView  
 14:                                       dequeueReusableCellWithIdentifier:@"EventTableViewCell"]; 
 15:      
 16:     SEEvent *event = [self.events objectAtIndex:indexPath.row]; 
 17:     cell.eventNameLabel.text = event.eventName; 
 18:     cell.eventDescriptionLabel.text = event.eventDescription; 
 19:     cell.eventSpeakerLabel.text = event.speakerName; 
 20:      
 21:     cell.eventDateLabel.text = [event eventDateAsString]; 
 22:      
 23:     return cell; 
 24: }

There is only one section in the table view so I hard-code the return value of numberOfSectionsInTableView: to 1. The number of rows in our only section is based on the number of objects in the events array (lines 6-9).

In tableView:cellForRowAtIndexPath:, I get a reference to the my cell prototype that I created in the storyboard by using the ‘EventTableViewCell’ identifier (which is what I named it in the storyboard). That is then casted to a pointer to an instance of my custom EventTableViewCell with the four label IBOutlets. I then grab the correct event from the array and assign that event’s property values to the correct label in the cell.

Finally, in order to use this new table view controller, go back to the storyboard, select the Events Table View Controller and change its class to EventsTableViewController. I f you run this now, you should be able to see the list of your events in the Events tab.

Event Details View Controller

Before finishing up the Speakers View Controller, add a new Table View Controller to the storyboard. This new controller will be used to show the event details when a user clicks on one of the cells from the Events Table View. To make it easier to design this new controller’s view, make sure to use the Attributes Inspector to show the Navigation Bar as the Top Bar and the Tab Bar as the Bottom Bar and set the Navigation Item’s title to Event Details.

imageTo define the transition from the Events Table View controller to the Event Details View Controller, we need to create a segue. Control + Drag from the Events Table View’s prototype cell to the new Table View Controller and select Push as the segue type.

 

imageWith the segue selected, set the segue’s identifier to ‘EventDetailsSegue’ in the Attributes Inspector.

Now back in the Event Details View Controller, select the Table View and set the Contents to Static Cells and the number of Sections to 2. The Style should also be Grouped (see image on the right). We won’t be using prototype cells for this view.

Select the first Table View Section and change the number of rows to 4 and set the header to General Info. Select the second Table View Section and set the number of rows to 1 and the header to Presenter.

imageFor the single row in the second section, set its Accessory type to Disclosure Indicator. The Event Details Table View should now look like the image on the right.

 

EventDetailsViewController Class

Similar to how we needed to create a custom UITableViewController for the Events Table View, we need to do the same for the Event Details View. Right-click on the ViewControllers group in the Project Navigator and add a new file called EventDetailsViewController using the UIViewController subclass template and subclassing from the UITableViewController.

In the new view controller’s header file, add the following:

  1: #import <UIKit/UIKit.h> 
  2: #import "SEEvent.h" 
  3: #import "SESpeaker.h" 
  4: #import "SEData.h" 
  5:  
  6: typedef enum { 
  7:     EventDetailsViewControllerSectionGeneral = 0, 
  8:     EventDetailsViewControllerSectionPresenter = 1 
  9: } EventDetailsViewControllerSection; 
 10:  
 11: typedef enum { 
 12:     EventDetailsViewControllerGeneralRowEventName = 0, 
 13:     EventDetailsViewControllerGeneralRowEventDate = 1, 
 14:     EventDetailsViewControllerGeneralRowEventLocation = 2, 
 15:     EventDetailsViewControllerGeneralRowEventDescription = 3 
 16: } EventDetailsViewControllerGeneralRow; 
 17:  
 18: @interface EventDetailsViewController : UITableViewController <SESpeakerDelegate> 
 19:  
 20: @property(nonatomic, retain) SEEvent *event; 
 21: @property(nonatomic, retain) SESpeaker *presenter; 
 22:  
 23: @end 
 24: 

Lines 2-4 imports the classes that we’ll be using from our model in this view controller. We also define two enums, one to represent the sections we have in this view (lines 6-9) and one to represent the different rows we have in the first section of the table view (lines 11-16).

On line 18, I also indicate that this interface will conform to the SESpeakerDelegate protocol (which I defined in the model class SESpeaker), so that when the speaker’s image has been loaded, this new view controller class will be notified and can act appropriately.

Lines 20-21 just adds two properties where I’ll store the SEEvent instance and its associated SESpeaker instance that we’ll use to display data from. Make sure to synthesize both of these properties in the .m file.

The next step is to create our own setter method for this view controller for the event:

  1: -(void)setEvent:(SEEvent *)newEvent 
  2: { 
  3:     event = newEvent; 
  4:      
  5:     [self.tableView reloadData]; 
  6: }

The setter method just takes the new event, sets it to this class’s event property and then forces a reload of the Table View’s data.

Because we say that this class conforms to the SESpeakerDelegate protocol, we should add the implementation for speaker:didLoadImage:. The implementation is below:

  1: -(void)speaker:(SESpeaker *)speaker didLoadImage:(UIImage *)image 
  2: { 
  3:     NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:1]; 
  4:      
  5:     UITableViewCell *cell = [[self tableView] cellForRowAtIndexPath:indexPath]; 
  6:     cell.imageView.image = speaker.image; 
  7:     [cell setNeedsLayout]; 
  8: }

When this callback method runs, I get the image that’s passed and set it to the UIImageView’s image which is part of the UITableViewCell. Then I send a message to the cell to reset its layout.

The following code handles the required methods for the UITableViewSource protocol:

  1: - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 
  2: { 
  3:     return 2; 
  4: } 
  5:  
  6: - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 
  7: { 
  8:     EventDetailsViewControllerSection sec = section; 
  9:      
 10:     if(sec == EventDetailsViewControllerSectionGeneral) 
 11:         return 4; 
 12:     else 
 13:         return 1; 
 14: } 
 15:  
 16: - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
 17: {   
 18:     EventDetailsViewControllerSection section = indexPath.section; 
 19:     EventDetailsViewControllerGeneralRow row = indexPath.row; 
 20:      
 21:     NSString *cellIdentifier; 
 22:      
 23:     if (section == EventDetailsViewControllerSectionGeneral) 
 24:     { 
 25:         if (row != EventDetailsViewControllerGeneralRowEventDescription)  
 26:         { 
 27:             cellIdentifier = @"DefaultCell"; 
 28:         } 
 29:         else 
 30:         { 
 31:             cellIdentifier = @"DescriptionCell"; 
 32:         } 
 33:     } 
 34:     else 
 35:     { 
 36:         cellIdentifier = @"PresenterCell"; 
 37:     } 
 38:      
 39:     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; 
 40:     if (cell == nil) { 
 41:         cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; 
 42:          
 43:         if(section == EventDetailsViewControllerSectionPresenter) 
 44:         { 
 45:             cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 
 46:              
 47:         } 
 48:         else 
 49:         { 
 50:             if (row == EventDetailsViewControllerGeneralRowEventDescription) 
 51:             { 
 52:                 UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(15, 5, 290, 75)]; 
 53:                 textView.textColor = [UIColor blackColor]; 
 54:                 textView.editable = NO; 
 55:                 [cell addSubview:textView];             
 56:             } 
 57:         } 
 58:     } 
 59:      
 60:      
 61:     if (section == EventDetailsViewControllerSectionGeneral) 
 62:     { 
 63:         switch(indexPath.row) 
 64:         { 
 65:             case EventDetailsViewControllerGeneralRowEventName: 
 66:                 cell.textLabel.text = event.eventName; 
 67:                 break; 
 68:             case EventDetailsViewControllerGeneralRowEventDate: 
 69:                 cell.textLabel.text = [event eventDateAsString]; 
 70:                 break; 
 71:             case EventDetailsViewControllerGeneralRowEventLocation: 
 72:                 cell.textLabel.text = [event eventLocation]; 
 73:                 break; 
 74:             case EventDetailsViewControllerGeneralRowEventDescription: 
 75:                 ((UITextView *)[cell.subviews objectAtIndex:cell.subviews.count-1]).text = event.eventDescription; 
 76:                 break; 
 77:             default: 
 78:                 cell.textLabel.text = @""; 
 79:                 break; 
 80:         } 
 81:     } 
 82:     else 
 83:     { 
 84:         SEData *data = [SEData sharedManager]; 
 85:         [data fetchSpeakerWithRowKey:event.speakerKey withCompletionHandler:^(SESpeaker *speaker, NSError *error)  
 86:         { 
 87:             self.presenter = speaker; 
 88:             self.presenter.delegate = self; 
 89:              
 90:             if(error)  
 91:             { 
 92:                 cell.textLabel.text = event.speakerName;  
 93:             } 
 94:             else 
 95:             { 
 96:                 cell.textLabel.text = speaker.name;  
 97:                 cell.imageView.image = speaker.image; 
 98:                 [cell setNeedsLayout];     
 99:             } 
100:         }]; 
101:          
102:          
103:     } 
104:      
105:     return cell; 
106: }

numberOfSectionsInTableView: and tableView:numberOfRowsInSection: should be obvious. The first returns 2 sections like the view we designed in the storyboard and the second returns 4 rows if it’s the first section and 1 if it’s the second section (the presenter section).

The tableView:cellForRowAtIndexPath: requires some explanation. First, we determine what UITableViewCell to use based on the section and row. We have three types of cells: a default cell which we use for the Event’s name, date, and location. There is a description cell where I need to present a rather long summary of the event. And of course there’s the presenter cell, which will display a presenter’s picture, name, and title, as well as a disclosure indicator to indicate to the user that they can select the cell to see more information about the presenter (set in lines 43-47).

Because the event description text is so long, I wanted to present it inside of a UITextView because the UITextView supports scrolling, since it inherits from UIScrollView. After I’ve created and configured the UITextView, I just add it as a subview of the cell. The creation, configuration, and adding as a subview of the UITextView is all in lines 52-55.

Lines 61-81 just basically figures out the appropriate data from the Event to put in the cell, depending on what the current section and row are. If the current section/row is for the presenter, then I use the SEData singleton to send a fetch request for the speaker associated with the event. When the request has completed, I then just use the properties of the SESpeaker to set the cell’s controls properly. All of this is done in lines 84-100.

Going back to the event’s description, in order to accommodate for the length, we should increase the size of that row as well. In order to do that, we need to implement tableView:heightForRowAtIndexPath: from the UITableViewDelegate protocol. This is the implementation of that:

  1: - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath 
  2: { 
  3:     EventDetailsViewControllerSection section = indexPath.section; 
  4:     EventDetailsViewControllerGeneralRow row = indexPath.row; 
  5:      
  6:     if (section == EventDetailsViewControllerSectionGeneral && row == EventDetailsViewControllerGeneralRowEventDescription) 
  7:     { 
  8:         return 85; 
  9:     } 
 10:     else 
 11:     { 
 12:         return 45; 
 13:     } 
 14: }

I basically almost double the size of the description row as compared to the other rows. If there is an overflow of text, the UITextView can be scrolled.

So now that the code for this controller is done, I need to tell the EventsTableViewController that when a cell is selected in that controller, this new controller needs to be presented. To do that, I need to implement prepareForSegue:sender:. In EventsTableViewController.m, add the following:

  1: - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender 
  2: { 
  3:  if ([segue.identifier isEqualToString:@"EventDetailsSegue"]) 
  4:  { 
  5:             EventDetailsViewController *eventDetailsViewController = segue.destinationViewController;      
  6:             NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow]; 
  7:             SEEvent *event = [self.events objectAtIndex:indexPath.row]; 
  8:  
  9:             eventDetailsViewController.event = event; 
 10:  } 
 11: }

Basically, all this code does is, when the segue is EventDetailsSegue, get the event that was selected from the events array and set the event property of the EventDetailsViewController to the selected event. Then the rest of the segue handling that we configured will continue. If you run the code now, then you’ll see that when you select a row from the list of events, you will be taken to the details of the event.

image

 

Presenter Details View Controller

imageWhen the user clicks on the presenter row in the event details view, I want to take them to a view that contains more information about the speaker. In order to do that, add a View Controller to the storyboard and set its title to Presenter. There are four pieces of speaker information to present in this view: the speaker’s name, his/her title, bio, and image. For the name and title, two Label controls are needed. Add a Text View control for the bio and and Image View for the speaker’s photo. The name should be bold and the image should have a mode of Aspect Fit. Position and size the controls so it looks like the view on the right.

PresenterDetailsViewController Class

Create a new UIViewController class that the Presenter Details View Controller created above will use as its class. This time, just use UIViewController as the parent class. Add the following to the .h file and connect the IBOutlets to the corresponding control on the view on the storyboard:

  1: #import <UIKit/UIKit.h> 
  2: #import "SESpeaker.h" 
  3: @interface PresenterDetailsViewController : UIViewController 
  4:  
  5: @property(nonatomic, retain) SESpeaker *speaker; 
  6: @property(nonatomic, retain) IBOutlet UIImageView *image; 
  7: @property(nonatomic, retain) IBOutlet UILabel *speakerName; 
  8: @property(nonatomic, retain) IBOutlet UILabel *speakerTitle; 
  9: @property(nonatomic, retain) IBOutlet UITextView *speakerBio; 
 10:  
 11: @end

To handle the view lifecycle, in the class implementation, change viewDidLoad and viewDidUnload to the following:

  1: - (void)viewDidLoad 
  2: { 
  3:     [super viewDidLoad]; 
  4:     image.image = speaker.image; 
  5:     speakerName.text = speaker.name; 
  6:     speakerTitle.text = speaker.title; 
  7:     speakerBio.text = speaker.bio;     
  8: } 
  9:  
 10:  
 11: - (void)viewDidUnload 
 12: { 
 13:     [super viewDidUnload]; 
 14:      
 15:     image.image = nil; 
 16:     speakerName.text = nil; 
 17:     speakerTitle.text = nil; 
 18:     speakerBio = nil; 
 19:     speaker = nil;     
 20: }

Amazingly, that’s all you need for this view controller. Everything else is handled through control configurations and connections between the view controller’s IBOutlets and the controls in the view. Everything is handled … except for the transition from the event details view to the presenter view.

Event Details to Presenter Details Segue

In order for the user to be able to navigate from the event details to presenter details, we need to create a segue between the two. In the storyboard, make sure to have the entire UITableViewController of the Event Details selected and Control + Drag from that to the Presenter Details View. Do not create the segue from one of the cells like we did with the Events Table View. The segue identifier should be set to EventPresenterSegue and the segue style should be Push.

Now in EventDetailsViewController.m, add an #import PresenterDetailsViewController.h to the top of the file. We also need to implement tableView:didSelectRowAtIndexPath: from the UITableViewDelegate protocol:

  1: - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 
  2: {     
  3:     if (indexPath.row == 0 && indexPath.section == EventDetailsViewControllerSectionPresenter) { 
  4:         [self performSegueWithIdentifier:@"EventPresenterSegue" sender:self]; 
  5:     } 
  6: }

The code above checks to see if the cell that was tapped is the cell for the presenter and if it is, it sends a message to go ahead and start the EventPresenterSegue.

In order to send the correct SESpeaker instance from the Event Details view to the Presenter Details view, also add the following to EventDetailsViewController.m:

  1: - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender 
  2: { 
  3:  if ([segue.identifier isEqualToString:@"EventPresenterSegue"]) 
  4:  { 
  5:   PresenterDetailsViewController *presenterDetailsViewController = segue.destinationViewController;   
  6:   presenterDetailsViewController.speaker = [self presenter]; 
  7:  } 
  8: }

This code checks to see if the segue being performed is the EventPresenterSegue and, if it is, grabs a reference to the destination view controller which, in this case, should be a PresenterDetailsViewController. It then assigns its presenter property to the PresenterDetailsViewController’s speaker property. That’s all that’s needed to make the segue from the Event Details to the Presenter Details work.

image

 

Speakers Table View Controller

The Speakers Table View Controller, which we added in the very beginning of this post, can created similarly as the Events Table View Controller. It can also have a segue to the Presenter Details View Controller. You will still need a subclass of UITableViewController to handle the logic of presenting the speaker information to various controls on the view. You’ll only need one cell prototype as all the cells for each speaker should look the same. At the end, your full storyboard should look like this:

image

In the interest of brevity, I’m not going to go through all the steps of how to create the Speakers Table View. If you do want all of that code (and all of the code for parts 1 and 2 of this post) please tweet out a link to both posts and then ping me on Twitter to let me know that you did and I’ll go ahead and send you the projects.

Conclusion

For the first parts of this series, I took you through setting up Azure tables and blobs that were used to store information and images for speakers and events. I went through the build process for the Windows Azure iOS Toolkit, as well as using the toolkit to get data from Windows Azure. I then went through the steps on how to build a UI that presented this data.

For the next part in this series, I plan on adding some authentication and authorization to the SpeakEasy app that will force users to log in using one of the identity providers that Azure Access Control Services supports, as well as allowing users who log in through ADFS certain capabilities not available to the general public. Please keep an eye out for that post in the future.

Oh, and please don’t forget to tweet this so I can send you the code!

Using Azure Storage Services from an iPhone App - Part 1: Table and Blob setup, Azure iOS toolkit, and Model Classes

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

image

 

Table Data – Speakers

image

 

Table Data – Events

image

 

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.

  1. Add a new group called lib. Add the libwatoolkitios.afile and the toolkit’s headers folder to this group.
  2. Select the SpeakEasy project from the Project Navigator. This should bring up the project settings in the standard editor.
  3. In the standard editor, make sure you’ve got the SpeakEasy project selected and go to the Build Settingstab.
  4. Search for the Other Linker Flags build setting and add the following settings:
    • -ObjC
    • -all_load
  5. Now select the SpeakEasy target and select the Build Phasestab.
  6. 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:

image

 

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:

image

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.

  

 

Windows Azure as a Mobile Backend Platform

Today, I posted a video blog discussing why using Windows Azure as a mobile backend platform is an attractive option. I wanted to quickly summarize the contents of that video and also supplement it with some helpful links.

Summary

Backend platform selection is important because, unless you’re talking about the most trivial of apps, most mobile applications will have some need to connect to backend services and data. It’s important to select a platform that’s flexible, scalable to your app and users’ needs, and provides tools that make building and managing your applications easier.

Windows Azure and SQL Azure is a compelling PaaS for mobile for the following reasons:

Cost

Windows Azure has an attractive pay for what you use pricing model. You only pay for the bandwidth, storage, and compute processing that you consume.

For more details on pricing, see the following:

Scalability

Windows Azure is a highly-scalable platform. You can start small (for example, a single instance of a VM with a single 1GHz CPU and less than 1GB of memory or a 5GB SQL Azure database), and scale up and out as the need arises. This is an especially good strategy for mobile consumer apps (where app usage will probably not be as demanding initially) until you’ve determined that your running at or near maximum capacity with your resources.

Windows Azure also provides usage monitoring tools and management APIs so you can detect and react appropriately to predictable or unpredictable bursts in usage patterns by dynamically starting more instances. There are also third party tools available, like AzureWatch, that help you monitor and dynamically adjust instances based on demand.

High Availability

Windows Azure is also a platform that can provide your mobile application with highly-available back-end services. If your service or virtual machine is down, Azure will automatically try to restart it. If the service or virtual machine can’t be restarted due to hardware failure, then the platform will automatically create a new virtual machine on another physical server and deploy and run your service on it. And if you’ve got at least two instances of your service running, then Microsoft’s Service Level Agreement (SLA) guarantees a 99.95% uptime rate.

Development Platform Support

Azure supports using the .NET framework but if you're not accustomed to the .NET framework, Azure also supports Java, PHP, and Node.js if those are the development environments you're more familiar with.

Java SDK

PHP SDK

Node.js SDK

Platform Services

The Azure platform also provides the following services that are typically essential for mobile applications:

Data Storage

Several different data storage options are available:

  • Relational Databases with SQL Azure
  • Non-relational, semi-structured databases with Table Storage (NoSQL)
  • Blob Storage for storing large, unstructured binary files like images, audio, and video
  • Queues for reliable and persistent messaging between applications and services

Data Syncing

For applications with the need for offline capabilities that synchronize to a back-end data store when the application is back online, Microsoft Sync Framework, through OData, supports syncing to and from mobile devices.

Sync Framework Toolkit

Content Delivery Network (CDN)

Mobile applications targeted for global use can take advantage of Azure’s CDN to help build responsive applications. Your mobile application’s assets (images, videos, etc) can be hosted on edge servers around the world and devices can retrieve these assets from the server that is closest to their location.

Authentication and Authorization

If your application has a need for authentication and authorization, Windows Azure App Fabric also has Access Control Services (ACS) which can be used to set up and manage that for you. ACS has support for different identity providers, including Facebook, Active Directory Federation Services (ADFS), and others.

Note that in the video, I mentioned Twitter; however, this is incorrect. Right now ACS supports OAuth WRAP and OAuth 2.0 and Twitter only officially supports OAuth 1.0A. So Twitter is out, for now.

Native Toolkits

Finally, the Windows Azure Platform Team has done a nice job of providing toolkits for most of the major mobile platforms (none for Blackberry). Using these toolkits makes it easy to connect to and leverage the Azure platform services.

 

Conclusion

I hope you found the video helpful and the subsequent summary useful. I have hopes to turn it into a series with tutorials on how to use these various services and toolkits in the future. If that is something that is of interest to you, please let me know in the comments below!