iOS 7 Best Practices; A Weather App Case Study: Part 2/2
Learn various iOS 7 best practices in this 2-part tutorial series; you’ll master the theory and then practice by making a functional, beautiful weather app. By Ryan Nystrom.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
iOS 7 Best Practices; A Weather App Case Study: Part 2/2
30 mins
- Getting Started
- Working with ReactiveCocoa
- Building the Signals
- Fetching Current Conditions
- Fetching the Hourly Forecast
- Fetching the Daily Forecast
- Managing & Storing Your Data
- Finding Your Location
- Retrieve the Weather Data
- Wiring the Interface
- ReactiveCocoa Bindings
- Displaying Data in the Table View
- Adding Polish to Your App
- Where To Go From Here?
Finding Your Location
Next you’ll add the code that triggers weather fetching when a location is found.
Add the following code to the implementation in WXManager.m:
- (void)findCurrentLocation {
self.isFirstUpdate = YES;
[self.locationManager startUpdatingLocation];
}
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
// 1
if (self.isFirstUpdate) {
self.isFirstUpdate = NO;
return;
}
CLLocation *location = [locations lastObject];
// 2
if (location.horizontalAccuracy > 0) {
// 3
self.currentLocation = location;
[self.locationManager stopUpdatingLocation];
}
}
The methods above are fairly straightforward:
- Always ignore the first location update because it is almost always cached.
- Once you have a location with the proper accuracy, stop further updates.
- Setting the
currentLocation
key triggers the RACObservable you set earlier in theinit
implementation.
Retrieve the Weather Data
Finally, it’s time to add the three fetch methods which call methods on the client and save values on the manager. All three of these methods are bundled up and subscribed to by the RACObservable create in the init method added earlier. You’ll return the same signals that the client returns, which can also be subscribed to.
All of the property assignments are happening in side-effects with -doNext:
.
Add the following code to WXManager.m:
- (RACSignal *)updateCurrentConditions {
return [[self.client fetchCurrentConditionsForLocation:self.currentLocation.coordinate] doNext:^(WXCondition *condition) {
self.currentCondition = condition;
}];
}
- (RACSignal *)updateHourlyForecast {
return [[self.client fetchHourlyForecastForLocation:self.currentLocation.coordinate] doNext:^(NSArray *conditions) {
self.hourlyForecast = conditions;
}];
}
- (RACSignal *)updateDailyForecast {
return [[self.client fetchDailyForecastForLocation:self.currentLocation.coordinate] doNext:^(NSArray *conditions) {
self.dailyForecast = conditions;
}];
}
It looks like everything is wired up and ready to go. But wait! The app doesn’t actually tell the manager to do anything yet.
Open up WXController.m and import the manager at the top of the file, like so:
#import "WXManager.h"
Add the following to the end of -viewDidLoad
:
[[WXManager sharedManager] findCurrentLocation];
This simply asks the manager class to begin finding the current location of the device.
Build and run your app; you’ll be prompted for permission to use location services. You still won’t see any UI updates, but check the console log and you’ll see something like the following:
2013-11-05 08:38:48.886 WeatherTutorial[17097:70b] Fetching: http://api.openweathermap.org/data/2.5/weather?lat=37.785834&lon=-122.406417&units=imperial
2013-11-05 08:38:48.886 WeatherTutorial[17097:70b] Fetching: http://api.openweathermap.org/data/2.5/forecast/daily?lat=37.785834&lon=-122.406417&units=imperial&cnt=7
2013-11-05 08:38:48.886 WeatherTutorial[17097:70b] Fetching: http://api.openweathermap.org/data/2.5/forecast?lat=37.785834&lon=-122.406417&units=imperial&cnt=12
It looks a little obtuse, but that output means all of your code is working and the network requests are firing properly.
Wiring the Interface
It’s finally time to display all that data you’re fetching, mapping, and storing. You’ll use ReactiveCocoa to observe changes on the WXManager
singleton and update the interface when new data arrives.
Still in WXController.m, go to the bottom of -viewDidLoad
, and add the following code just above [[WXManager sharedManager] findCurrentLocation];
line:
// 1
[[RACObserve([WXManager sharedManager], currentCondition)
// 2
deliverOn:RACScheduler.mainThreadScheduler]
subscribeNext:^(WXCondition *newCondition) {
// 3
temperatureLabel.text = [NSString stringWithFormat:@"%.0f°",newCondition.temperature.floatValue];
conditionsLabel.text = [newCondition.condition capitalizedString];
cityLabel.text = [newCondition.locationName capitalizedString];
// 4
iconView.image = [UIImage imageNamed:[newCondition imageName]];
}];
Here’s what the above code accomplishes:
- Observes the
currentCondition
key on the WXManager singleton. - Delivers any changes on the main thread since you’re updating the UI.
- Updates the text labels with weather data; you’re using
newCondition
for the text and not the singleton. The subscriber parameter is guaranteed to be the new value. - Uses the mapped image file name to create an image and sets it as the icon for the view.
Build and run your app; you’ll see the the current temperature, current conditions, and an icon representing the current conditions. All of the data is real-time, so your values likely won’t match the ones below. However, if your location is San Francisco, it always seems to be about 65 degrees. Lucky San Franciscans! :]
ReactiveCocoa Bindings
ReactiveCocoa brings its own form of Cocoa Bindings to iOS.
Don’t know what bindings are? In a nutshell, they’re a technology which provides a means of keeping model and view values synchronized without you having to write a lot of “glue code.” They allow you to establish a mediated connection between a view and a piece of data, “binding” them such that a change in one is reflected in the other.
It’s a pretty powerful concept, isn’t it?
Okay, pick your jaw up off the floor. It’s time to move on.
Add the following code below the code you added in the previous step:
// 1
RAC(hiloLabel, text) = [[RACSignal combineLatest:@[
// 2
RACObserve([WXManager sharedManager], currentCondition.tempHigh),
RACObserve([WXManager sharedManager], currentCondition.tempLow)]
// 3
reduce:^(NSNumber *hi, NSNumber *low) {
return [NSString stringWithFormat:@"%.0f° / %.0f°",hi.floatValue,low.floatValue];
}]
// 4
deliverOn:RACScheduler.mainThreadScheduler];
The code above binds high and low temperature values to the hiloLabel
‘s text property. Here’s a detailed look at how you accomplish this:
- The RAC(…) macro helps keep syntax clean. The returned value from the signal is assigned to the
text
key of thehiloLabel
object. - Observe the high and low temperatures of the
currentCondition
key. Combine the signals and use the latest values for both. The signal fires when either key changes. - Reduce the values from your combined signals into a single value; note that the parameter order matches the order of your signals.
- Again, since you’re working on the UI, deliver everything on the main thread.
Build and run your app; you should see the high/low label in the bottom left update along with the rest of the UI like so:
Displaying Data in the Table View
Now that you’ve fetched all your data, you can display it neatly in the table view. You’ll display the six latest hourly and daily forecasts in a paged table view with header cells as appropriate. The app will appear to have three pages: one for current conditions, one for the hourly forecast, and one for the daily forecasts.
Before you can add cells to the table view, you’ll need to initialize and configure some date formatters.
Go to the private interface at the top of WXController.m and add the following two properties:
@property (nonatomic, strong) NSDateFormatter *hourlyFormatter;
@property (nonatomic, strong) NSDateFormatter *dailyFormatter;
As date formatters are expensive to create, we’ll instantiate them in our init
method and store references to them using these properties.
Still in the same file, add the following code directly under the @implementation
statement:
- (id)init {
if (self = [super init]) {
_hourlyFormatter = [[NSDateFormatter alloc] init];
_hourlyFormatter.dateFormat = @"h a";
_dailyFormatter = [[NSDateFormatter alloc] init];
_dailyFormatter.dateFormat = @"EEEE";
}
return self;
}
You might wonder why you’re initializing these date formatters in -init
and not -viewDidLoad
like everything else. Good question!
-viewDidLoad
can actually be called several times in the lifecycle of a view controller. NSDateFormatter
objects are expensive to initialize, but by placing them in -init
you’ll ensure they’re initialized only once by your view controller.
Find tableView:numberOfRowsInSection:
in WXController.m and replace the TODO
and return
lines with the following:
// 1
if (section == 0) {
return MIN([[WXManager sharedManager].hourlyForecast count], 6) + 1;
}
// 2
return MIN([[WXManager sharedManager].dailyForecast count], 6) + 1;
A relatively short code block, but here’s what it does:
- The first section is for the hourly forecast. Use the six latest hourly forecasts and add one more cell for the header.
- The next section is for daily forecasts. Use the six latest daily forecasts and add one more cell for the header.
Find tableView:cellForRowAtIndexPath:
in WXController.m and replace the TODO
section with the following:
if (indexPath.section == 0) {
// 1
if (indexPath.row == 0) {
[self configureHeaderCell:cell title:@"Hourly Forecast"];
}
else {
// 2
WXCondition *weather = [WXManager sharedManager].hourlyForecast[indexPath.row - 1];
[self configureHourlyCell:cell weather:weather];
}
}
else if (indexPath.section == 1) {
// 1
if (indexPath.row == 0) {
[self configureHeaderCell:cell title:@"Daily Forecast"];
}
else {
// 3
WXCondition *weather = [WXManager sharedManager].dailyForecast[indexPath.row - 1];
[self configureDailyCell:cell weather:weather];
}
}
Again, this code is fairly straightforward:
- The first row of each section is the header cell.
- Get the hourly weather and configure the cell using custom configure methods.
- Get the daily weather and configure the cell using another custom configure method.
Finally, add the following three methods to WXController.m:
// 1
- (void)configureHeaderCell:(UITableViewCell *)cell title:(NSString *)title {
cell.textLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:18];
cell.textLabel.text = title;
cell.detailTextLabel.text = @"";
cell.imageView.image = nil;
}
// 2
- (void)configureHourlyCell:(UITableViewCell *)cell weather:(WXCondition *)weather {
cell.textLabel.font = [UIFont fontWithName:@"HelveticaNeue-Light" size:18];
cell.detailTextLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:18];
cell.textLabel.text = [self.hourlyFormatter stringFromDate:weather.date];
cell.detailTextLabel.text = [NSString stringWithFormat:@"%.0f°",weather.temperature.floatValue];
cell.imageView.image = [UIImage imageNamed:[weather imageName]];
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
}
// 3
- (void)configureDailyCell:(UITableViewCell *)cell weather:(WXCondition *)weather {
cell.textLabel.font = [UIFont fontWithName:@"HelveticaNeue-Light" size:18];
cell.detailTextLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:18];
cell.textLabel.text = [self.dailyFormatter stringFromDate:weather.date];
cell.detailTextLabel.text = [NSString stringWithFormat:@"%.0f° / %.0f°",
weather.tempHigh.floatValue,
weather.tempLow.floatValue];
cell.imageView.image = [UIImage imageNamed:[weather imageName]];
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
}
Here’s what the above three methods do:
- Configures and adds text to the cell used as the section header. You’ll reuse this for daily and hourly forecast sections.
- Formats the cell for an hourly forecast.
- Formats the cell for a daily forecast.
Build and run your app; try to scroll your table view and…wait a minute. Nothing is showing up! What gives?
If you’ve used UITableView
in the past, you’ve probably run into this very problem before. The table isn’t reloading!
To fix this you need to add another ReactiveCocoa observable on the hourly and daily forecast properties of the manager.
As a self-test, try to write this reusing some of the observables in -viewDidLoad
. If you get stuck, the solution is below.
[spoiler title=”Solution”]
Add the following to your other ReactiveCocoa observables in -viewDidLoad
of WXController.m:
[[RACObserve([WXManager sharedManager], hourlyForecast)
deliverOn:RACScheduler.mainThreadScheduler]
subscribeNext:^(NSArray *newForecast) {
[self.tableView reloadData];
}];
[[RACObserve([WXManager sharedManager], dailyForecast)
deliverOn:RACScheduler.mainThreadScheduler]
subscribeNext:^(NSArray *newForecast) {
[self.tableView reloadData];
}];
[/spoiler]
Build and run your app once more; scroll the table views and you’ll see all the forecast data populate, as below: