This post has moved. New location
Future updates to the series will only be posted to the new location.
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:
- Using Azure Storage Services from an iPhone App
- Part 1: Table and Blob Setup, iOS Toolkit, and Model Classes
- Part 2: Building the User Interface
- Adding Authentication and Authorization with Access Control Services
- Push Notifications with Queues and Workers
- Offline Synching using the Sync Framework (tentative)
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:
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:
- Open up the MainStoryboard.storyboard file and drag a Table View Controllerto the canvas.
- 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.
- 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’.
- In the Events Table View Controller, set the Title of the Navigation Item to ‘Events’.
- 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:
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):
Name 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:
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.
To 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.
With 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.
For 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.
Presenter Details View Controller
When 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.
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:
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!