Using Windows Azure AppFabric Access Control Service in an iPhone App to Integrate with Facebook

Intro

This is the third post in a series that shows how to use Windows Azure as a platform that provides key services for native mobile applications. In the first two parts of the series, I walked through the process of creating my SpeakEasy app, a fictitious iPhone app that keeps track of speakers and their events.

In this post, I will walk through how to use the Azure AppFabric Access Control Service (ACS). ACS is a service that provides federated authentication using claims. ACS provides support for different identity providers, including Facebook, Windows Live, Google, Yahoo!, and ADFS 2.0 (and other WS-Federation identity providers). In this scenario, I will be using Facebook as an identity provider. Rather than force the user to log in using their Facebook account right away, I will only ask the user to log in when they want to share the details of an event on their Facebook wall. When they attempt to share the event, they will first have to log in using their Facebook account and then, by using the token received from Facebook (which will be provided as a claim), I will show how to post a message on the user's wall.

As a reminder, this series includes:

Facebook Application Setup

To start with, we need to create a Facebook application:

  1. Go to http://www.facebook.com/developers and log in.
  2. Click the Create New App button.
  3. In the Create New App dialog, enter an appropriate name and the optional namespace. I have configured my application with the following settings:
    • App Name: SpeakEasy iPhone App from DeviantPoint
    • App Namespace: speakeasy-dp.
    • This is optional but if you want to use Facebook’s Open Graph to post different Actions and define custom Object Types, you’ll need a namespace (I presume to prevent naming collisions).

After you have agreed to the Facebook Platform Policies agreement and clicked the Continue button, you will be taken to your app’s settings page. On this page are two very important pieces of information: the App ID and App Secret. Make a copy of both values as this will be used later on in the ACS setup. The rest of the application configuration is as follows:

  • Contact Email: <my email>
  • Website Site URL: https://<ACS-Service-Namespace>.accesscontrol.windows.net.
    • This is the URL that Facebook will redirect to after the user has successfully logged in. While you’re debugging your application, it’s ok to configure this section and set the URL to your Azure ACS service namespace URL (details in following section). However, when you’ve published your app to the App Store, you’ll want to delete this section and then configure the Native iOS App section correctly to point to your app in the app store.

ACS Setup

Configuring a Service Namespace

  1. Log in to the Azure Management Portal and go to the Service Bus, Access Control & Caching section.
  2. Select Services > Access Control from the left pane and click on the New menu item from the ribbon. My Service Namespace configuration looks like this:

image

The end result of this will be an ACS URL for my namespace: https://speakeasy.accesscontrol.windows.net.

Once the Service Namespace has been created, you will be redirected to the Access Control Service management portal for your new namespace.

Configuring Facebook as an Identity Provider

The next step is configuring your Identity Providers (idP). In this example, we’ll only be using Facebook as our idP. However, you can configure multiple idPs to allow your users to log in using different idP services, like Facebook, Google, Yahoo, Windows Live, and ADFS 2.0.

  1. Click on the Identity Providers link under Trust Relationships in the left pane.
  2. Click on the Add link.
  3. Select Facebook Application from the custom identity provider section and click Next.
  4. Configure the following settings:
    • Display name: Facebook
    • Application ID: this is one of the values that you should have copied from the Facebook Application Setup. Enter that value here.
    • Application secret: this is one of the values that you should have copied from the Facebook Application Setup. Enter that value here.
    • Application permissions:email, publish_stream
      • The application permissions field is used to tell Facebook what permissions your app will need. The email is given by default. The publish_stream permission will allow our app to post on the user’s behalf on his/her newsfeed. Here is a list of all application permissions you can request.
  5. Click Save.

Configuring a Relying Party Application

The next step is to configure a relying party application. A relying party application is an application that trusts the identity provider and consumes the claims made by that identity provider about the user.

  1. Click on the Relying party applications under Trust Relationships in the left pane.
  2. Click on the Add link.
  3. The following are the configuration settings I used for my relying party application:
    • Name: speakeasy.deviantpoint.com
    • Mode: manual
    • Realm: uri:speakeasy.deviantpoint.com
    • Return URL:empty
      • Normally, this is where ACS will redirect your application to after the user has successfully logged in. Since we’re building a native iPhone app, this setting isn’t needed. However, it is sometimes still a good idea to set this to a web page that you’ve built that can take the incoming claims and do something with them. I like to set this to a page on my site where I have some code to read the incoming claims so that I am able to look at them in case I need to debug something.
    • Error URL: empty
    • Token Format:SWT
      • This setting is important. Originally, I had this set to SAML 2.0 but I realized after some debugging efforts that the Windows Azure Toolkit for iOS is expecting the format to be SWT (Simple Web Token).
    • Token Encryption Policy: None
    • Token Lifetime: 600 (secs)
    • Identity Providers: Facebook
    • Rule Groups: Create New Rule Group
    • Token Signing: Use Service Namespace Certificate
  4. Click Save.

Configuring Rule Groups

Rule Groups determine how incoming claims from the identity provider are mapped to output claims delivered to the relying party application.

  1. Click on Rule groups under Trust Relationships in the left pane.
  2. Click the Generate link and select the Facebook identity provider.

Clicking the Generate link will create a default set of rules for the identity provider. ACS is smart enough to tell a standard set of claims that most of the idPs provide. If the idP provided more claims than what was generated, then you can add those claims as well. Similarly, if there are claims that your relying party application doesn’t need, you can remove them as well.

Assuming you’ve set up the Facebook application and ACS (identity provider, relying party application, and rule groups) correctly, then you should be able to use the link to an ACS-hosted login page, found in the Application Integration section under Development in the left pane, to test the process. Note that unless you configured a Return URL for the Relying Party Application, you will get an ACS50011: The RP ReplyTo address is missing.’ error. It’s probably best to configure a Return URL at first for testing purposes to make sure your setup is correct.

Using ACS in the iPhone App

Modifying the Windows Azure iOS Toolkit

Now that the set up is complete, we can start to use ACS in the SpeakEasy app – except for one little detail: if you downloaded the iOS toolkit from the Master branch on Github, the toolkit currently filters out any claims that don’t start with a prefix of ‘http://schemas.xmlsoap.org/ws/2005/05/identity/claims/’. Fortunately, you have two options. The first option is to download the toolkit from the Develop branch. This branch removes the check for this prefix and just passes any claims through. The second option is to just modify the source code yourself. This is the option I took since I wasn’t sure exactly what other changes have been made to the Develop branch and I didn’t want to introduce any unknowns.

If you’re modifying the source code, find the file WACloudAccessToken.m. Around line 80, you will see the following code:

  1: 
  2: NSString* claimsPrefix = @"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/";
  3: 			
  4: for(NSString* part in [_securityToken componentsSeparatedByString:@"&"])
  5: {   
  6: 	NSRange split = [part rangeOfString:@"="];
  7: 	if(!split.length)
  8: 	{
  9: 		continue; // weird
 10: 	}
 11: 	
 12: 	NSString* key = [[part substringToIndex:split.location] URLDecode];
 13: 	NSString* value = [[part substringFromIndex:split.location + 1] URLDecode];
 14: 	
 15: 	if([key hasPrefix:claimsPrefix])
 16: 	{
 17: 		key = [key substringFromIndex:claimsPrefix.length];
 18: 		[claims setObject:value forKey:key];
 19: 	}
 20: }
 21: 
 22: _claims = [claims copy];

 

Change this code to comment out the offending lines:

  1: //NSString* claimsPrefix = @"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/";
  2: 
  3: for(NSString* part in [_securityToken componentsSeparatedByString:@"&"])
  4: {   
  5: 	NSRange split = [part rangeOfString:@"="];
  6: 	if(!split.length)
  7: 	{
  8: 		continue; // weird
  9: 	}
 10: 	
 11: 	NSString* key = [[part substringToIndex:split.location] URLDecode];
 12: 	NSString* value = [[part substringFromIndex:split.location + 1] URLDecode];
 13: 	
 14: // if([key hasPrefix:claimsPrefix])
 15: // {
 16: // key = [key substringFromIndex:claimsPrefix.length];
 17: 		[claims setObject:value forKey:key];
 18: // }
 19: }
 20: 
 21: _claims = [claims copy];

 

Rebuild the toolkit and re-add a reference to the SpeakEasy project.

UI Changes

Now that the Toolkit is ready, we want to add the ability for the user to post an event’s details to his Facebook newsfeed. The easiest place to start is to make changes to the user interface,

Storyboard Changes

  1. Open up the MainStoryboard.storyboard file and find the Event Details view.
  2. From the Object library, drag a Bar Button Item to the navigation bar.
  3. Select the Bar Button Item and change the Identifier to Action in the Attributes Inspector.
  4. Add a View Controller from the Object library to the Storyboard.
  5. Select the View Controller and change the Top Bar to Navigation Bar and the Bottom Bar to Tab Bar.
  6. Change the Navigation Item’s title to Post to Facebook.
  7. Add a Text View and a Round Rect Button from the Object library to the new view.
  8. Resize the Text View to be about 1/3 of the height of the view and almost the full width.
  9. Change the text of the button to Post!.
  10. CTRL+Drag from the Event Details View Controller to the Post to Facebook View Controller.
  11. Select Push for the Segue type.
  12. Set the segue identifier to PostToFacebookSegue.

The new part of the storyboard should now look like this:

image

PostToFacebookViewController

Now we need a custom view controller that will interact with the new Post To Facebook view.

  1. From the Project navigator, right-click the ViewControllers group and select New File.
  2. Select the UIViewController subclass template and click Next.
  3. Name the class PostToFacebookViewController as a subclass of UIViewController.
  4. Click Next a few times until the .h and .m files are created.
  5. Open the .h file and add #import statements for SEEvent.h and WACloudAccessToken.h.
  6. Add the following code to the interface definition:
  1: @property(nonatomic, retain) WACloudAccessToken *acsToken;
  2: @property(nonatomic, retain) SEEvent *event; 
  3: @property(nonatomic, retain) IBOutlet UITextView *messageTextView;
  4: 
  5: -(IBAction)postMessage:(id)sender;

 

The WACloudAccessToken is a class from the Windows Azure iOS Toolkit that represents an ACS access token.

The IBAction postMessage: is the action that will be called when the Post! button is pushed by the user. Synthesize the properties in the .m file and connect the two outlets to their interface counterparts in the storyboard. Add the postMessage: method to PostToFacebookViewController.m but just leave the implementation blank for now.

EventDetailsViewController

Now we need to add code to handle transitioning from the Event Details View to the Post To Facebook View. To do this, we need to modify the EventDetailsViewController.

  1. Open EventDetailsViewController.hand add the following field:
    • WACloudAccessControlClient *_acsClient;
  2. Add the following method:
    • -(IBAction)shareButtonPressed:(id)sender;
    This method is the event handler for when the user clicks on the Action button we added in the storyboard for this view.
  3. Open the EventDetailsViewController.m.
  4. Modify viewDidLoad so it looks like the following:
  1: - (void)viewDidLoad
  2: {
  3:     [super viewDidLoad];
  4:     
  5:     _acsClient = [WACloudAccessControlClient accessControlClientForNamespace:@"speakeasy" 
  6:                                                                        realm:@"uri:speakeasy.deviantpoint.com"];
  7: }

 

The above code is where we use the WACloudAccessControlClient class provided by the iOS Azure toolkit to create a client we can use to hit the ACS service. The namespace and realm passed in are the namespace/realm that was configured as part of the ACS setup.

Modify the prepareForSegue:sender: method so it looks like the following:

  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:     else if ([segue.identifier isEqualToString:@"PostToFacebookSegue"])
  9:     {
 10:         PostToFacebookViewController *postToFacebookViewController = segue.destinationViewController;
 11:         postToFacebookViewController.event = self.event;
 12:         postToFacebookViewController.acsToken = [WACloudAccessControlClient sharedToken];
 13:     }
 14: }

In the above code, we just add a new condition for the new segue we added. If the segue we’re trying to perform is the segue to move to the Facebook view, then we pass the event and the access token (which is available from the WACloudAccessControlClient after the user has successfully signed in) to that view.

Finally, to handle the Action button being clicked, add this method to the bottom of the file:

  1: -(IBAction)shareButtonPressed:(id)sender
  2: {  
  3:     [_acsClient showInViewController:self allowsClose:YES withCompletionHandler:^(BOOL authenticated) 
  4:     {   
  5:         if(authenticated) 
  6:         {
  7:             [self performSegueWithIdentifier:@"PostToFacebookSegue" sender:self];
  8:         }        
  9:     }];    
 10: }

 

This method calls the showInViewController:allowsClose:withCompletionHandler method on the WACloudAccessControlClient instance. When the method runs after the button is clicked, the facebook login screen will be shown to the client:

image image

After the user has successfully logged in, the completion handler code will run which basically checks to see if the user is logged in and if so, performs the segue to the Post To Facebook view.

Handling the segue to PostToFacebookViewController

In PostToFacebookViewController.m, add the following to viewDidLoad to add a default message to the Text View in the view:

  1: - (void)viewDidLoad
  2: {
  3:     [super viewDidLoad];
  4:     
  5:     messageTextView.text = [NSString stringWithFormat:@"I'll be attending %@ presented by %@ on %@ in %@",
  6:                             _event.eventName, _event.speakerName, _event.eventDateAsString, _event.eventLocation];
  7: }

 

This is a sample of what the view looks like when it is first loaded from the Event Details View:

image

Finally, to post the actual message to Facebook when the Post button is clicked, modify postMessage to look like the following:

  1: -(IBAction)postMessage:(id)sender
  2: {
  3:     NSString *accessToken = [acsToken.claims objectForKey:@"http://www.facebook.com/claims/AccessToken"];
  4:     NSURL *url = [[NSURL alloc] initWithString:[[NSString alloc] initWithFormat:@"https://graph.facebook.com/me/feed?access_token=%@&message=%@", 
  5:                                                 accessToken,  [messageTextView.text stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
  6:     
  7:     NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
  8:     [request setHTTPMethod:@"POST"];
  9:     
 10:     NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
 11:     
 12:     if (conn)
 13:     {        
 14:         [[self navigationController] popViewControllerAnimated:YES];
 15:     }
 16:     else
 17:     {
 18:         //todo: add error message
 19:     }
 20: }

 

In the first line of this method, I retrieve the Facebook access token that is returned to me as a claim from Facebook after a successful login. The claims property of the WACloudAccessToken is a collection of all the claims that were passed through from the idP to the relying party application.

The rest of the lines are just used to create a POST request to Facebook. Any requests made to Facebook needs to include the access token (passed in as the access_token parameter). The message parameter is the actual text that will be posted to the feed, which must be URL-escaped.

If the post is successful, then I just navigate back to the details view (I didn’t any any error-handling in this example). A successful post to Facebook looks like this:

image

Conclusion

That’s really all to using ACS from an iPhone app using the Windows Azure Toolkit for iOS. Like I mentioned earlier, you can use ACS for your app’s authorization and authentication needs with other, multiple idPs. This is an especially low barrier-of-entry for apps that need to use Active Directory by taking advantage of ADFS through ACS.

I hope you enjoyed this post. If you have any questions, ask them in the comments section and I’ll try to answer them as best as I can. For the next part of the series, I will cover using Azure to send push notifications.

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.

  

 

About the author

Bart X. Tubalinal is a Solutions Architect with over 10+ years experience in building enterprise applications. He also considers himself to be, pound for pound, one of the best developers there is.

Archives

Comments

Comment RSS