In the last article of this series, I will show you how to create the user interface of our weather application. Thanks to the work of Chris Carey, a regular contributor of Vectortuts+, we have a beautiful design that we can work with!
Introduction
For the design of our weather application, I collaborated with Chris Carey, a regular contributor of Vectortuts+. Chris crafted a beautiful design that we can use to create the application's user interface. After Chris handed me the design as a vector illustration, I sliced the artwork and prepared it for use in our project. You can find the sliced artwork in the source files of this article.
Due to the limited customizability of UIKit, we will have to make a few compromises when implementing Chris' design. The final result, however, will very much look like the design that Chris created for us. Customizing UIKit's UISwitch
class, for example, is very limited and we will therefore use a different approach to implement the temperature setting. Chris used two custom fonts for his design, Maven Pro and Mission Gothic. Even though iOS supports custom fonts, we will only use fonts available on iOS.
1. Notifications
Step 1: Creating String Constants
Whenever the weather view controller receives weather data from the Forecast API, its own view and the right view need to be updated. This means that we need to notify the forecast view controller and send it the weather data. There are several ways to notify the forecast view controller of such an event. Posting a notification using NSNotificationCenter
is the simplest solution and provides us with the most flexibility. We can use the same solution for updating the user interface after the user has toggled the temperature setting. By sending a notification, every object that is interested in this event can register itself as an observer. I have updated MTConstants.h/.m to create a string constant for each notification.
extern NSString * const MTRainWeatherDataDidChangeChangeNotification; extern NSString * const MTRainTemperatureUnitDidChangeNotification;
NSString * const MTRainWeatherDataDidChangeChangeNotification = @"com.mobileTuts.MTRainWeatherDataDidChangeChangeNotification"; NSString * const MTRainTemperatureUnitDidChangeNotification = @"com.mobileTuts.MTRainTemperatureUnitDidChangeNotification";
Step 2: Weather View Controller
After receiving a response from the Forecast API, we need to store it so that we can use it later to update the view. Create two private properties in MTWeatherViewController.m, (1) response
(of type NSDictionary
) that will hold the response of the Forecast API and (2) forecast
(of type NSArray
) that will hold a subset of the response, the weather data for the next hours.
#import "MTWeatherViewController.h" #import "MTForecastClient.h" @interface MTWeatherViewController () <CLLocationManagerDelegate> { BOOL _locationFound; } @property (strong, nonatomic) NSDictionary *location; @property (strong, nonatomic) NSDictionary *response; @property (strong, nonatomic) NSArray *forecast; @property (strong, nonatomic) CLLocationManager *locationManager; @end
To receive notifications, we need to update initWithNibName:bundle:
as shown below (MTWeatherViewController.m). In weatherDataDidChangeChange:
, we store the response of the Forecast API in response
and forecast
and we update the view controller's view. In temperatureUnitDidChange
, we only need to update the view to reflect the changed setting.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { // Initialize Location Manager self.locationManager = [[CLLocationManager alloc] init]; // Configure Location Manager [self.locationManager setDelegate:self]; [self.locationManager setDesiredAccuracy:kCLLocationAccuracyKilometer]; // Add Observer NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; [nc addObserver:self selector:@selector(reachabilityStatusDidChange:) name:MTRainReachabilityStatusDidChangeNotification object:nil]; [nc addObserver:self selector:@selector(weatherDataDidChangeChange:) name:MTRainWeatherDataDidChangeChangeNotification object:nil]; [nc addObserver:self selector:@selector(temperatureUnitDidChange:) name:MTRainTemperatureUnitDidChangeNotification object:nil]; } return self; }
- (void)weatherDataDidChangeChange:(NSNotification *)notification { // Update Response & Forecast [self setResponse:[notification userInfo]]; [self setForecast:self.response[@"hourly"][@"data"]]; // Update View [self updateView]; }
- (void)temperatureUnitDidChange:(NSNotification *)notification { // Update View [self updateView]; }
Step 3: Forecast View Controller
The steps are almost identical in the MTForecastViewController
class. We update initWithNibName:bundle:
as shown below, create two properties (response
and forecast
), and implement weatherDataDidChangeChange:
and temperatureUnitDidChange:
. The difference is the weather data stored in forecast
. We will implement updateView
a bit later in this tutorial, but it is good practice to create a stub implementation to get rid of any compiler warnings.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { // Add Observer NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(weatherDataDidChangeChange:) name:MTRainWeatherDataDidChangeChangeNotification object:nil]; [nc addObserver:self selector:@selector(temperatureUnitDidChange:) name:MTRainTemperatureUnitDidChangeNotification object:nil]; } return self; }
#import "MTForecastViewController.h" @interface MTForecastViewController () @property (strong, nonatomic) NSDictionary *response; @property (strong, nonatomic) NSArray *forecast; @end
- (void)weatherDataDidChangeChange:(NSNotification *)notification { // Update Response & Forecast [self setResponse:[notification userInfo]]; [self setForecast:self.response[@"daily"][@"data"]]; // Update View [self updateView]; }
- (void)temperatureUnitDidChange:(NSNotification *)notification { // Update View [self updateView]; }
- (void)updateView { }
2.User Interface Center View
Step 1: Outlets and Actions
Even though the center view contains a lot of information, it isn't that difficult to implement. Let's start by creating a number of outlets and one new action. Update MTWeatherViewController.h as shown below. Revisit Chris's design to better understand the location and purpose of each user interface element. The difference with Chris's design is that we will replace the calendar icon in the top right with the refresh button that we created in the previous tutorial. The weather data for the next hours will be presented in a collection view, which implies that the MTWeatherViewController
class needs to conform to the UICollectionViewDataSource
, UICollectionViewDelegate
, and UICollectionViewDelegateFlowLayout
protocols.
#import <UIKit/UIKit.h> #import "MTLocationsViewController.h" @interface MTWeatherViewController : UIViewController <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, MTLocationsViewControllerDelegate> @property (weak, nonatomic) IBOutlet UIButton *buttonLocation; @property (weak, nonatomic) IBOutlet UIButton *buttonRefresh; @property (weak, nonatomic) IBOutlet UILabel *labelDate; @property (weak, nonatomic) IBOutlet UILabel *labelTemp; @property (weak, nonatomic) IBOutlet UILabel *labelTime; @property (weak, nonatomic) IBOutlet UILabel *labelWind; @property (weak, nonatomic) IBOutlet UILabel *labelRain; @property (weak, nonatomic) IBOutlet UILabel *labelLocation; @property (weak, nonatomic) IBOutlet UICollectionView *collectionView; @end
We also need to add an action for the location button in the top left. Open MTWeatherViewController.m and add a new action named openLeftView:
. In openLeftView:
, we tell the view controller's view deck controller to toggle the left view. Remember that it is also possible to swipe from left to right to open the left view.
- (IBAction)openLeftView:(id)sender { [self.viewDeckController toggleLeftViewAnimated:YES]; }
Step 2: Creating the User Interface
Open MTWeatherViewController.xib and create the user interface as shown in figure 2. While creating the user interface, it is important to verify that the user interface displays correctly on both the 3.5" screen and the iPhone 5's 4" screen. You can test this by selecting the view controller's view and changing the Size attribute in the Attributes Inspector. To accomplish the desired result, you need to tweak the autolayout constraints of the user interface elements. The goal is to make the weather data stick to the top of the view, while the collection view is glued to the bottom. The icons next to the time, wind, and rain labels are instances of UIImageView
.
Configure the labels and buttons as shown in figure 2. This includes properly aligning the text of the labels, setting the types of both buttons to Custom, making the File's Owner the collection view's dataSource
and delegate
, and setting the scroll direction of the collection view flow layout to horizontal. I am a fan of Gill Sans so that is the font I chose to use for this project. Before switching back to the implementation file of the weather view controller, connect the outlets and action that we created earlier. In addition to the labels and buttons, I've also added an image view to the view controller's view to display the background image.
As I mentioned in the introduction, you can find the application's artwork in the source files of this tutorial. Create a folder named Artwork in your Xcode project and drag the artwork in this folder.
Step 3: Populating the User Interface
We currently log the response of the Forecast API to Xcode's Console. To start using the weather data, we need to update the fetchWeatherData
method as shown below. In the completion block of requestWeatherForCoordinate:completion:
, we hide the progress HUD and send a notification on the main thread. We make use of the dispatch_async
function and pass the queue of the main thread as the first argument. The notification's userInfo
dictionary is the response of the request.
- (void)fetchWeatherData { if ([[MTForecastClient sharedClient] networkReachabilityStatus] == AFNetworkReachabilityStatusNotReachable) return; // Show Progress HUD [SVProgressHUD showWithMaskType:SVProgressHUDMaskTypeGradient]; // Query Forecast API double lat = [[_location objectForKey:MTLocationKeyLatitude] doubleValue]; double lng = [[_location objectForKey:MTLocationKeyLongitude] doubleValue]; [[MTForecastClient sharedClient] requestWeatherForCoordinate:CLLocationCoordinate2DMake(lat, lng) completion:^(BOOL success, NSDictionary *response) { // Dismiss Progress HUD [SVProgressHUD dismiss]; if (response && [response isKindOfClass:[NSDictionary class]]) { dispatch_async(dispatch_get_main_queue(), ^{ // Post Notification on Main Thread NSNotification *notification = [NSNotification notificationWithName:MTRainWeatherDataDidChangeChangeNotification object:nil userInfo:response]; [[NSNotificationCenter defaultCenter] postNotification:notification]; }); } }]; }
The weather and forecast view controllers are both observers of MTRainWeatherDataDidChangeChangeNotification
notifications. The weather view controller invokes weatherDataDidChangeChange:
, which in turn invokes updateView
. In updateView
, we invoke updateCurrentWeather
and update the collection view by sending it a message of reloadData
.
- (void)updateView { // Update Location Label [self.labelLocation setText:[self.location objectForKey:MTLocationKeyCity]]; // Update Current Weather [self updateCurrentWeather]; // Reload Collection View [self.collectionView reloadData]; }
Before we implement updateCurrentWeather
, I want to take a small detour. We will be displaying temperature values in various places of the application and because we support both Fahrenheit and Celsius this can become cumbersome. It is therefore useful to create a class that centralizes this logic so that we don't need to clutter our code base with if statements and temperature conversions.
Step 4: Creating the Settings Class
Before we create the class that handles temperature conversions, we need to be able to store the current temperature setting in the user defaults database. Revisit MTConstants.h/.m and declare a string constant with a name of MTRainUserDefaultsTemperatureUnit
.
extern NSString * const MTRainUserDefaultsTemperatureUnit;
NSString * const MTRainUserDefaultsTemperatureUnit = @"temperatureUnit";
To make working with settings easier, I often create a category on NSUserDefaults
that allows me to quickly and elegantly access the application's settings. Let me show you what I mean. Create a new Objective-C category (figure 3), name the category Helpers, and make it a category on NSUserDefaults
(figure 4). In NSUserDefaults+Helpers.h, we declare three class methods as shown below.
#import <Foundation/Foundation.h> @interface NSUserDefaults (Helpers) #pragma mark - #pragma mark Temperature + (BOOL)isDefaultCelcius; + (void)setDefaultToCelcius; + (void)setDefaultToFahrenheit; @end
Even though these methods aren't magical, they are very useful. The first method, isDefaultCelcius
, tells us if the temperature unit is set to Celsius or not. The other two methods make it very easy to switch between Fahrenheit and Celsius. Not only do we update the user defaults database, we also post a notification that informs observers of the change.
+ (BOOL)isDefaultCelcius { return [[NSUserDefaults standardUserDefaults] integerForKey:MTRainUserDefaultsTemperatureUnit] == 1; } + (void)setDefaultToCelcius { // Update User Defaults NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; [ud setInteger:1 forKey:MTRainUserDefaultsTemperatureUnit]; [ud synchronize]; // Post Notification [[NSNotificationCenter defaultCenter] postNotificationName:MTRainTemperatureUnitDidChangeNotification object:nil]; } + (void)setDefaultToFahrenheit { // Update User Defaults NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; [ud setInteger:0 forKey:MTRainUserDefaultsTemperatureUnit]; [ud synchronize]; // Post Notification [[NSNotificationCenter defaultCenter] postNotificationName:MTRainTemperatureUnitDidChangeNotification object:nil]; }
It is time to create the settings class that I wrote about earlier. The solution is surprisingly easy. Create a new Objective-C class, name it MTSettings
, and make it a subclass of NSObject
(figure 5). In MTSettings.h, we declare one class method, formatTemperature:
. In MTSettings.m, we import the header of the category we created a moment ago and implement formatTemperature:
as shown below. The method accepts an instance of NSNumber
, converts it to a float, and returns a formatted string based on the temperature setting.
#import <Foundation/Foundation.h> @interface MTSettings : NSObject #pragma mark - #pragma mark Convenience Methods + (NSString *)formatTemperature:(NSNumber *)temperature; @end
#import "NSUserDefaults+Helpers.h"
+ (NSString *)formatTemperature:(NSNumber *)temperature { float value = [temperature floatValue]; if ([NSUserDefaults isDefaultCelcius]) { value = (value - 32.0) * (5.0 / 9.0); } return [NSString stringWithFormat:@"%.0f°", value]; }
Before we continue, add an import statement for the MTSettings
class to the project's precompiled header file so that we can use it throughout the project.
#import <Availability.h> #ifndef __IPHONE_3_0 #warning "This project uses features only available in iOS SDK 3.0 and later." #endif #ifdef __OBJC__ #import <UIKit/UIKit.h> #import <Foundation/Foundation.h> #import <QuartzCore/QuartzCore.h> #import <CoreLocation/CoreLocation.h> #import <MobileCoreServices/MobileCoreServices.h> #import <SystemConfiguration/SystemConfiguration.h> #import "AFNetworking.h" #import "SVProgressHUD.h" #import "IIViewDeckController.h" #import "MTSettings.h" #import "MTConstants.h" #endif
It is now time to implement updateCurrentWeather
in the MTWeatherViewController
class. The data for the current weather is a subset of the response we received from the Forecast API. The implementation of updateCurrentWeather
is pretty straightforward. The only caveat to watch out for is the precipitation probability. If this value is equal to 0
, the key precipProbability
is not included in the response. That is the reason that we first check for the existence of the key precipProbability
in the response dictionary.
- (void)updateCurrentWeather { // Weather Data NSDictionary *data = [self.response objectForKey:@"currently"]; // Update Date Label NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; [dateFormatter setDateFormat:@"EEEE, MMM d"]; NSDate *date = [NSDate dateWithTimeIntervalSince1970:[data[@"time"] doubleValue]]; [self.labelDate setText:[dateFormatter stringFromDate:date]]; // Update Temperature Label [self.labelTemp setText:[MTSettings formatTemperature:data[@"temperature"]]]; // Update Time Label NSDateFormatter *timeFormatter = [[NSDateFormatter alloc] init]; [timeFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; [timeFormatter setDateFormat:@"ha"]; [self.labelTime setText:[timeFormatter stringFromDate:[NSDate date]]]; // Update Wind Label [self.labelWind setText:[NSString stringWithFormat:@"%.0fMP", [data[@"windSpeed"] floatValue]]]; // Update Rain Label float rainProbability = 0.0; if (data[@"precipProbability"]) { rainProbability = [data[@"precipProbability"] floatValue] * 100.0; } [self.labelRain setText:[NSString stringWithFormat:@"%.0f%%", rainProbability]]; }
Step 5: Populating the Collection View
To populate the collection view, we first need to create a UICollectionViewCell
subclass. Create a new Objective-C class, name it MTHourCell
, and make it a subclass of UICollectionViewCell
(figure 6). Creating custom table or collection view cells can be laborious and painstaking. If you want to learn more about creating custom table and collection view cells, then I suggest you take a look at a tutorial I wrote a few weeks ago.
In the interface of MTHourCell
, we declare four properties of type UILabel
. We don't do much magic in MTHourcell.m as you can see below. To better understand the initWithFrame:
method, revisit the design I showed you at the beginning of this article. I won't discuss the implementation of initWithFrame:
in detail, but I do want to point out that I use a preprocessor define for the text color of the labels. I added the preprocess define to MTConstants.h to make it available to the entire project (see below).
#import <UIKit/UIKit.h> @interface MTHourCell : UICollectionViewCell @property (strong, nonatomic) UILabel *labelTime; @property (strong, nonatomic) UILabel *labelTemp; @property (strong, nonatomic) UILabel *labelWind; @property (strong, nonatomic) UILabel *labelRain; @end
#import "MTHourCell.h" #define kMTLabelBottomWidth 40.0 #define kMTLabelBottomHeight 40.0 @interface MTHourCell () @end @implementation MTHourCell - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // Helpers CGSize size = self.contentView.frame.size; // Initialize Label Time self.labelTime = [[UILabel alloc] initWithFrame:CGRectMake(30.0, 0.0, 50.0, 40.0)]; // Configure Label Time [self.labelTime setBackgroundColor:[UIColor clearColor]]; [self.labelTime setTextColor:[UIColor whiteColor]]; [self.labelTime setFont:[UIFont fontWithName:@"GillSans-Light" size:18.0]]; [self.labelTime setAutoresizingMask:(UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleBottomMargin)]; [self.contentView addSubview:self.labelTime]; // Initialize Label Temp self.labelTemp = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 46.0, 80.0, 44.0)]; // Configure Label Temp [self.labelTemp setBackgroundColor:[UIColor clearColor]]; [self.labelTemp setTextAlignment:NSTextAlignmentCenter]; [self.labelTemp setTextColor:kMTColorGray]; [self.labelTemp setFont:[UIFont fontWithName:@"GillSans-Bold" size:40.0]]; [self.labelTemp setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin)]; [self.contentView addSubview:self.labelTemp]; // Initialize Label Wind self.labelWind = [[UILabel alloc] initWithFrame:CGRectMake(0.0, size.height - kMTLabelBottomHeight, kMTLabelBottomWidth, kMTLabelBottomHeight)]; // Configure Label Wind [self.labelWind setBackgroundColor:[UIColor clearColor]]; [self.labelWind setTextAlignment:NSTextAlignmentCenter]; [self.labelWind setTextColor:kMTColorGray]; [self.labelWind setFont:[UIFont fontWithName:@"GillSans-Light" size:16.0]]; [self.labelWind setAutoresizingMask:(UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin)]; [self.contentView addSubview:self.labelWind]; // Initialize Label Rain self.labelRain = [[UILabel alloc] initWithFrame:CGRectMake(size.width - kMTLabelBottomWidth, size.height - kMTLabelBottomHeight, kMTLabelBottomWidth, kMTLabelBottomHeight)]; // Configure Label Rain [self.labelRain setBackgroundColor:[UIColor clearColor]]; [self.labelRain setTextAlignment:NSTextAlignmentCenter]; [self.labelRain setTextColor:kMTColorGray]; [self.labelRain setFont:[UIFont fontWithName:@"GillSans-Light" size:16.0]]; [self.labelRain setAutoresizingMask:(UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin)]; [self.contentView addSubview:self.labelRain]; // Background View UIImage *backgroundImage = [[UIImage imageNamed:@"background-hour-cell"] resizableImageWithCapInsets:UIEdgeInsetsMake(40.0, 10.0, 10.0, 10.0)]; UIImageView *backgroundView = [[UIImageView alloc] initWithFrame:CGRectMake(0.0, 0.0, size.width, size.height)]; [backgroundView setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight)]; [backgroundView setImage:backgroundImage]; [self setBackgroundView:backgroundView]; } return self; } @end
#define kMTColorGray [UIColor colorWithRed:0.737 green:0.737 blue:0.737 alpha:1.0] #define kMTColorGreen [UIColor colorWithRed:0.325 green:0.573 blue:0.388 alpha:1.0] #define kMTColorOrange [UIColor colorWithRed:1.000 green:0.306 blue:0.373 alpha:1.0]
As you can see below, implementing the UICollectionViewDataSource
, UICollectionViewDelegate
, and UICollectionViewDelegateFlowLayout
protocols is very similar to implementing the UITableViewDataSource
and UITableViewDelegate
protocols.
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { return self.forecast ? 1 : 0; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return [self.forecast count]; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { MTHourCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:HourCell forIndexPath:indexPath]; // Fetch Data NSDictionary *data = [self.forecast objectAtIndex:indexPath.row]; // Initialize Date Formatter NSDateFormatter *timeFormatter = [[NSDateFormatter alloc] init]; [timeFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; [timeFormatter setDateFormat:@"ha"]; NSDate *date = [NSDate dateWithTimeIntervalSince1970:[data[@"time"] doubleValue]]; // Configure Cell [cell.labelTime setText:[timeFormatter stringFromDate:date]]; [cell.labelTemp setText:[MTSettings formatTemperature:data[@"temperature"]]]; [cell.labelWind setText:[NSString stringWithFormat:@"%.0fMP", [data[@"windSpeed"] floatValue]]]; float rainProbability = 0.0; if (data[@"precipProbability"]) { rainProbability = [data[@"precipProbability"] floatValue] * 100.0; } [cell.labelRain setText:[NSString stringWithFormat:@"%.0f%%", rainProbability]]; return cell; }
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { return CGSizeMake(80.0, 120.0); } - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { return UIEdgeInsetsMake(0.0, 10.0, 0.0, 10.0); }
To make all this work, we need to (1) import the header file of MTHourCell
, (2) declare a static string constant that serves as the cell reuse identifier, and (3) tell the collection view to use the MTHourCell
class to instantiate new cells. In viewDidLoad
, we also set the collection view's background color to transparant.
#import "MTHourCell.h"
static NSString *HourCell = @"HourCell";
- (void)viewDidLoad { [super viewDidLoad]; // Load Location self.location = [[NSUserDefaults standardUserDefaults] objectForKey:MTRainUserDefaultsLocation]; if (!self.location) { [self.locationManager startUpdatingLocation]; } // Configure Collection View [self.collectionView setBackgroundColor:[UIColor clearColor]]; [self.collectionView registerClass:[MTHourCell class] forCellWithReuseIdentifier:HourCell]; }
3.User Interface Right View
Step 1: Outlets
Even though the right view displays a lot of data, the implementation of the MTForecastViewController
class isn't that complex. We start by creating an outlet for the table view in MTForecastViewController.h and conform the class to the UITableViewDataSource
and UITableViewDelegate
protocols.
#import <UIKit/UIKit.h> @interface MTForecastViewController : UIViewController <UITableViewDataSource, UITableViewDelegate> @property (weak, nonatomic) IBOutlet UITableView *tableView; @end
Step 2: Creating the User Interface
Creating the user interface is as simple as adding a table view to the view controller's view, connecting the outlet we created a moment ago, and setting the File's Owner as the table view's dataSource
and delegate
(figure 7).
Step 3: Populating the Table View
Before we populate the table view with data, we need to create a UITableViewCell
subclass. Create a new Objective-C class, name it MTDayCell
, and make it a subclass of UITableViewCell
(figure 8). Open MTDayCell.h and declare five outlets of type UILabel
. As with the MTHourCell
class, the implementation of MTDayCell
isn't too difficult as you can see below.
#import <UIKit/UIKit.h> @interface MTDayCell : UITableViewCell @property (strong, nonatomic) UILabel *labelDay; @property (strong, nonatomic) UILabel *labelDate; @property (strong, nonatomic) UILabel *labelTemp; @property (strong, nonatomic) UILabel *labelWind; @property (strong, nonatomic) UILabel *labelRain; @end
#import "MTDayCell.h" #define kMTCalendarWidth 44.0 #define kMTCalendarHeight 80.0 #define kMTCalendarMarginLeft 60.0 #define kMTLabelRightWidth 30.0 #define kMTLabelRightHeight 14.0 @interface MTDayCell () @property (strong, nonatomic) UIImageView *imageViewCalendar; @end @implementation MTDayCell #pragma mark - #pragma mark Initialization - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { // Helpers CGSize size = self.contentView.frame.size; // Configure Table View Cell [self setSelectionStyle:UITableViewCellSelectionStyleNone]; // Initialize Image View Clock self.imageViewCalendar = [[UIImageView alloc] initWithFrame:CGRectMake(kMTCalendarMarginLeft, 0.0, kMTCalendarWidth, kMTCalendarHeight)]; // Configure Image View Clock [self.imageViewCalendar setContentMode:UIViewContentModeCenter]; [self.imageViewCalendar setImage:[UIImage imageNamed:@"background-calendar-day-cell"]]; [self.imageViewCalendar setAutoresizingMask:(UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin)]; [self.contentView addSubview:self.imageViewCalendar]; // Initialize Label Day self.labelDay = [[UILabel alloc] initWithFrame:CGRectMake(kMTCalendarMarginLeft, 10.0, kMTCalendarWidth, 20.0)]; // Configure Label Day [self.labelDay setTextColor:[UIColor whiteColor]]; [self.labelDay setTextAlignment:NSTextAlignmentCenter]; [self.labelDay setBackgroundColor:[UIColor clearColor]]; [self.labelDay setFont:[UIFont fontWithName:@"GillSans" size:14.0]]; [self.labelDay setAutoresizingMask:(UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin)]; [self.contentView addSubview:self.labelDay]; // Initialize Label Date self.labelDate = [[UILabel alloc] initWithFrame:CGRectMake(kMTCalendarMarginLeft, 20.0, kMTCalendarWidth, 60.0)]; // Configure Label Date [self.labelDate setTextColor:kMTColorGray]; [self.labelDate setTextAlignment:NSTextAlignmentCenter]; [self.labelDate setBackgroundColor:[UIColor clearColor]]; [self.labelDate setFont:[UIFont fontWithName:@"GillSans" size:24.0]]; [self.labelDate setAutoresizingMask:(UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin)]; [self.contentView addSubview:self.labelDate]; // Initialize Label Wind self.labelWind = [[UILabel alloc] initWithFrame:CGRectMake(size.width - kMTLabelRightWidth, (size.height / 2.0) - kMTLabelRightHeight, kMTLabelRightWidth, kMTLabelRightHeight)]; // Configure Label Wind [self.labelWind setTextColor:kMTColorGray]; [self.labelWind setTextAlignment:NSTextAlignmentCenter]; [self.labelWind setBackgroundColor:[UIColor clearColor]]; [self.labelWind setFont:[UIFont fontWithName:@"GillSans" size:12.0]]; [self.labelWind setAutoresizingMask:(UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin)]; [self.contentView addSubview:self.labelWind]; // Initialize Label Rain self.labelRain = [[UILabel alloc] initWithFrame:CGRectMake(size.width - kMTLabelRightWidth, (size.height / 2.0), kMTLabelRightWidth, kMTLabelRightHeight)]; // Configure Label Rain [self.labelRain setTextColor:kMTColorGray]; [self.labelRain setTextAlignment:NSTextAlignmentCenter]; [self.labelRain setBackgroundColor:[UIColor clearColor]]; [self.labelRain setFont:[UIFont fontWithName:@"GillSans" size:12.0]]; [self.labelRain setAutoresizingMask:(UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin)]; [self.contentView addSubview:self.labelRain]; // Initialize Label Temp self.labelTemp = [[UILabel alloc] initWithFrame:CGRectMake(kMTCalendarWidth + kMTCalendarMarginLeft + 12.0, 0.0, size.width - kMTCalendarWidth - kMTCalendarMarginLeft - kMTLabelRightWidth - 12.0, size.height)]; // Configure Label Temp [self.labelTemp setTextColor:kMTColorGray]; [self.labelTemp setTextAlignment:NSTextAlignmentCenter]; [self.labelTemp setBackgroundColor:[UIColor clearColor]]; [self.labelTemp setFont:[UIFont fontWithName:@"GillSans-Bold" size:40.0]]; [self.labelTemp setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight)]; [self.contentView addSubview:self.labelTemp]; } return self; } @end
The implementation of the table view data source protocol is very similar to that of the collection view data source protocol that we saw earlier. We also implement one method of the table view delegate protocol, tableView:heightForRowAtIndexPath:
, to set the row height to 80.0
.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return self.forecast ? 1 : 0; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.forecast count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MTDayCell *cell = [tableView dequeueReusableCellWithIdentifier:DayCell forIndexPath:indexPath]; // Fetch Data NSDictionary *data = [self.forecast objectAtIndex:indexPath.row]; // Initialize Date Formatter NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; NSDate *date = [NSDate dateWithTimeIntervalSince1970:[data[@"time"] doubleValue]]; // Configure Cell [dateFormatter setDateFormat:@"EEE"]; [cell.labelDay setText:[dateFormatter stringFromDate:date]]; [dateFormatter setDateFormat:@"d"]; [cell.labelDate setText:[dateFormatter stringFromDate:date]]; float tempMin = [data[@"temperatureMin"] floatValue]; float tempMax = [data[@"temperatureMax"] floatValue]; [cell.labelTemp setText:[NSString stringWithFormat:@"%.0f°/%.0f°", tempMin, tempMax]]; [cell.labelWind setText:[NSString stringWithFormat:@"%.0f", [data[@"windSpeed"] floatValue]]]; float rainProbability = 0.0; if (data[@"precipProbability"]) { rainProbability = [data[@"precipProbability"] floatValue] * 100.0; } [cell.labelRain setText:[NSString stringWithFormat:@"%.0f", rainProbability]]; return cell; }
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return 80.0; }
To make all this work, we need to (1) import the header file of the MTDayCell
class, (2) declare a static string constant for the cell reuse identifier, and (3) register the MTDayCell
as the class for that cell reuse identifier in viewDidLoad
. In updateView
, we reload the table view.
#import "MTDayCell.h"
static NSString *DayCell = @"DayCell";
- (void)viewDidLoad { [super viewDidLoad]; // Configure Table View [self.tableView registerClass:[MTDayCell class] forCellReuseIdentifier:DayCell]; }
- (void)updateView { // Reload Table View [self.tableView reloadData]; }
4.User Interface Right View
Step 1: Updating the User Interface
It is clear that we need to make some significant changes to the locations view controller to implement Chris's design. The table view of the locations view controller will include two sections instead of one. The top section will display the stored locations while the bottom section is reserved for the temperature setting. Start by opening MTLocationsViewController.xib and remove the navigation bar that we added in the previous article (figure 9). This means that we can also delete the outlet for the edit button in MTLocationsViewController.h and the editLocations
action in MTLocationsViewController.m.
Step 2: Creating the Location Cell
The cells that display the locations have a delete button on the left. To make this work, we need to create a custom table view cell. Create another UITableViewCell
subclass and name it MTLocationCell
(figure 10). Open MTLocationCell.h and create two properties, (1) buttonDelete
(UIButton
) and (2) labelLocation
(UILabel
). As you can see, the implementation of MTLocationCell
is less complex than those of MTHourCell
and MTDayCell
.
#import <UIKit/UIKit.h> @interface MTLocationCell : UITableViewCell @property (strong, nonatomic) UIButton *buttonDelete; @property (strong, nonatomic) UILabel *labelLocation; @end
#import "MTLocationCell.h" #define kMTButtonDeleteWidth 44.0 #define kMTLabelLocationMarginLeft 44.0 @implementation MTLocationCell #pragma mark - #pragma mark Initialization - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { // Helpers CGSize size = self.contentView.frame.size; // Initialize Delete Button self.buttonDelete = [UIButton buttonWithType:UIButtonTypeCustom]; // Configure Delete Button [self.buttonDelete setFrame:CGRectMake(0.0, 0.0, kMTButtonDeleteWidth, size.height)]; [self.buttonDelete setImage:[UIImage imageNamed:@"button-delete-location-cell"] forState:UIControlStateNormal]; [self.buttonDelete setImage:[UIImage imageNamed:@"button-delete-location-cell"] forState:UIControlStateSelected]; [self.buttonDelete setImage:[UIImage imageNamed:@"button-delete-location-cell"] forState:UIControlStateDisabled]; [self.buttonDelete setImage:[UIImage imageNamed:@"button-delete-location-cell"] forState:UIControlStateHighlighted]; [self.buttonDelete setAutoresizingMask:(UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleRightMargin)]; [self.contentView addSubview:self.buttonDelete]; // Initialize Location Label self.labelLocation = [[UILabel alloc] initWithFrame:CGRectMake(kMTLabelLocationMarginLeft, 0.0, size.width - kMTLabelLocationMarginLeft, size.height)]; // Configure Text Label [self.labelLocation setTextColor:kMTColorGray]; [self.labelLocation setBackgroundColor:[UIColor clearColor]]; [self.labelLocation setFont:[UIFont fontWithName:@"GillSans" size:20.0]]; [self.labelLocation setAutoresizingMask:(UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleLeftMargin)]; [self.contentView addSubview:self.labelLocation]; } return self; } @end
Step 3: Updating the Table View Data Source Protocol
To implement the design, we need to update the UITableViewDataSource
and UITableViewDelegate
protocols. Start by importing the header file of the MTLocationCell
class and the category on NSUserDefaults
that we created earlier. The table view will contain three types of cells and we need to declare a reuse identifier for each type (see below).
#import "MTLocationsViewController.h" #import "MTLocationCell.h" #import "NSUserDefaults+Helpers.h" @interface MTLocationsViewController () @property (strong, nonatomic) NSMutableArray *locations; @end
static NSString *AddLocationCell = @"AddLocationCell"; static NSString *LocationCell = @"LocationCell"; static NSString *SettingsCell = @"SettingsCell";
In setupView
, we configure the table view by (1) setting its separatorStyle
property to UITableViewCellSeparatorStyleNone
and (2) registering a class for each reuse identifier.
- (void)setupView { // Setup Table View [self.tableView setSeparatorStyle:UITableViewCellSeparatorStyleNone]; // Register Class for Cell Reuse [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:AddLocationCell]; [self.tableView registerClass:[MTLocationCell class] forCellReuseIdentifier:LocationCell]; [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:SettingsCell]; }
The UITableViewDataSource
protocol changes significantly and the implementations of the various methods can seem daunting at first. Most of the complexity, however, is due to nested if
statements.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 2; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (section == 0) { return [self.locations count] + 1; } return 2; }
We also implement tableView:titleForHeaderInSection:
and tableView:viewForHeaderInSection:
to replace the default section headers with a custom design that matches the application's design. When implementing tableView:viewForHeaderInSection:
, it is important to also implement tableView:heightForHeaderInSection:
.
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { switch (section) { case 0: { return NSLocalizedString(@"Locations", nil); break; } default: { return NSLocalizedString(@"Temperature", nil); break; } } return nil; } - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { // Header Text NSString *text = [self tableView:tableView titleForHeaderInSection:section]; // Helpers CGRect labelFrame = CGRectMake(12.0, 0.0, tableView.bounds.size.width, 44.0); // Initialize Label UILabel *label = [[UILabel alloc] initWithFrame:labelFrame]; // Configure Label [label setText:text]; [label setTextColor:kMTColorOrange]; [label setFont:[UIFont fontWithName:@"GillSans" size:20.0]]; [label setBackgroundColor:[UIColor clearColor]]; // Initialize View UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, tableView.bounds.size.width, 34.0)]; [backgroundView setBackgroundColor:[UIColor clearColor]]; [backgroundView addSubview:label]; return backgroundView; } - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { return 40.0; }
We make use of tableView:heightForFooterInSection:
to create some whitespace between the top and bottom sections. This means that we also need to implement tableView:viewForFooterInSection:
.
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section { // Initialize View UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, tableView.bounds.size.width, 34.0)]; [backgroundView setBackgroundColor:[UIColor whiteColor]]; return backgroundView; } - (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section { if (section == 0) { return 40.0; } return 0.0; }
The implementation of tableView:cellForRowAtIndexPath:
is a bit more complex due to the three cell types in the table view.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = nil; if (indexPath.section == 0) { if (indexPath.row == 0) { cell = [tableView dequeueReusableCellWithIdentifier:AddLocationCell forIndexPath:indexPath]; } else { cell = [tableView dequeueReusableCellWithIdentifier:LocationCell forIndexPath:indexPath]; } } else { cell = [tableView dequeueReusableCellWithIdentifier:SettingsCell forIndexPath:indexPath]; } // Configure Cell [self configureCell:cell atIndexPath:indexPath]; return cell; }
In configureCell:atIndexPath:
, we configure each cell. As I wrote earlier, the complexity is primarily due to nested if
statements. Tapping the delete button in a location cell sends a message of deleteLocation:
to the locations view controller. We will implement deleteLocation:
shortly.
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath { // Helpers UIFont *fontLight = [UIFont fontWithName:@"GillSans-Light" size:18.0]; UIFont *fontRegular = [UIFont fontWithName:@"GillSans" size:18.0]; // Background View Image UIImage *backgroundImage = [[UIImage imageNamed:@"background-location-cell"] resizableImageWithCapInsets:UIEdgeInsetsMake(10.0, 0.0, 0.0, 10.0)]; // Configure Table View Cell [cell.textLabel setFont:fontLight]; [cell.textLabel setTextColor:kMTColorGray]; [cell.textLabel setBackgroundColor:[UIColor clearColor]]; [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; if (indexPath.section == 0) { if (indexPath.row == 0) { [cell.textLabel setText:@"Add Current Location"]; [cell.imageView setContentMode:UIViewContentModeCenter]; [cell.imageView setImage:[UIImage imageNamed:@"icon-add-location"]]; // Background View Image backgroundImage = [[UIImage imageNamed:@"background-add-location-cell"] resizableImageWithCapInsets:UIEdgeInsetsMake(10.0, 0.0, 0.0, 10.0)]; } else { // Fetch Location NSDictionary *location = [self.locations objectAtIndex:(indexPath.row - 1)]; // Configure Cell [[(MTLocationCell *)cell buttonDelete] addTarget:self action:@selector(deleteLocation:) forControlEvents:UIControlEventTouchUpInside]; [[(MTLocationCell *)cell labelLocation] setText:[NSString stringWithFormat:@"%@, %@", location[MTLocationKeyCity], location[MTLocationKeyCountry]]]; } } else { if (indexPath.row == 0) { [cell.textLabel setText:NSLocalizedString(@"Fahrenheit", nil)]; if ([NSUserDefaults isDefaultCelcius]) { [cell.textLabel setFont:fontLight]; [cell.textLabel setTextColor:kMTColorGray]; } else { [cell.textLabel setFont:fontRegular]; [cell.textLabel setTextColor:kMTColorGreen]; } } else { [cell.textLabel setText:NSLocalizedString(@"Celsius", nil)]; if ([NSUserDefaults isDefaultCelcius]) { [cell.textLabel setFont:fontRegular]; [cell.textLabel setTextColor:kMTColorGreen]; } else { [cell.textLabel setFont:fontLight]; [cell.textLabel setTextColor:kMTColorGray]; } } } if (backgroundImage) { // Background View UIImageView *backgroundView = [[UIImageView alloc] initWithFrame:CGRectMake(0.0, 0.0, cell.frame.size.width, cell.frame.size.height)]; [backgroundView setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight)]; [backgroundView setImage:backgroundImage]; [cell setBackgroundView:backgroundView]; } }
Because locations can now be deleted by tapping the delete button in the location cells, the rows themselves no longer need to be editable. This means that the implementation of tableView:canEditRowAtIndexPath:
can be reduced to returning NO
and the implementation of tableView:commitEditingStyle:forRowAtIndexPath:
can be removed altogether.
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return NO; }
Step 4: Updating the Table View Delegate Protocol
In tableView:didSelectRowAtIndexPath:
, we add a bit more complexity due to the inclusion of the second section containing the temperature setting. Thanks to our category on NSUserDefaults
, the implementation is simple and concise.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; if (indexPath.section == 0) { if (indexPath.row == 0) { // Notify Delegate [self.delegate controllerShouldAddCurrentLocation:self]; } else { // Fetch Location NSDictionary *location = [self.locations objectAtIndex:(indexPath.row - 1)]; // Notify Delegate [self.delegate controller:self didSelectLocation:location]; } } else { if (indexPath.row == 0 && [NSUserDefaults isDefaultCelcius]) { [NSUserDefaults setDefaultToFahrenheit]; } else if (![NSUserDefaults isDefaultCelcius]) { [NSUserDefaults setDefaultToCelcius]; } // Update Section [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationNone]; } // Show Center View Controller [self.viewDeckController closeLeftViewAnimated:YES]; }
The rows in Chris's design are slightly taller than the default height of 44 points. To put this detail into practice, we implement tableView:heightForRowAtIndexPath:
as shown below.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return 50.0; }
Step 5: Deleting Locations (Revisited)
The last thing that we need to do is implement the deleteLocation:
method that is invoked when the delete button is tapped in a location cell. The implementation is more verbose than you might expect. Inferring the row number from the cell to which the delete button belongs isn't as trivial as it should be. However, once we have the index path of the cell to which the button belongs, we only need to update the array of locations, update the user defaults database, and update the table view.
- (void)deleteLocation:(id)sender { UITableViewCell *cell = (UITableViewCell *)[[(UIButton *)sender superview] superview]; NSIndexPath *indexPath = [self.tableView indexPathForCell:cell]; // Update Locations [self.locations removeObjectAtIndex:(indexPath.row - 1)]; // Update User Defaults [[NSUserDefaults standardUserDefaults] setObject:self.locations forKey:MTRainUserDefaultsLocations]; // Update Table View [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationTop]; }
5. Finishing Touch
To finish our project, we need to replace the default launch images with the ones provided by Chris Carey. The launch images are also included in the source files of this article.
Build and run the application to see the final result in action. Even though we have spent quite some time creating and designing the application, there are still some rough edges. It would also be nice to implement a caching mechanism so that we can show cached data to the user as long as a request to the Forecast API hasn't returned. These are a few examples of refinements that we can add to our application.
Conclusion
Creating the user interface was quite a bit of work, but the code involved wasn't all that complicated. I hope this tutorial has shown you what a great design can do for an application. Consumer applications in particular really benefit from an appealing, fresh design like the one we used in this tutorial.
Comments