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?
In Part 1 of this tutorial series looking at iOS 7 best practices, you set up your project with Cocoapods, added views to the controller and laid them out, and finally built models to reflect the weather data you will be fetching.
In the second and final part of this tutorial series, you’ll fill out the rest of the app to fetch data from the weather API and then wire up the UI. You’ll be exposed to the world of Functional Programming with ReactiveCocoa and rely on it heavily for data fetching and UI updating events.
Getting Started
You have two choices for the starting point of this tutorial: you can either use your completed project from Part 1 of this tutorial, or you can download the completed project here.
You created the weather model for your app in the previous tutorial — now you need to fetch some data for your app using the OpenWeatherMap API. You’ll abstract the data fetching, parsing, and storing with two classes: WXClient
and WXManager
. You’re going to create the client first, then the manager.
WXClient
‘s sole responsibility is to create API requests and parse them; someone else can worry about what to do with the data and how to store it. The design pattern of dividing different types of work between classes is called separation of concerns. This makes your code much easier to understand, extend, and maintain.
Working with ReactiveCocoa
Ensure you’re using the SimpleWeather.xcworkspace file, open WXClient.h and add the following imports:
@import CoreLocation;
#import <ReactiveCocoa/ReactiveCocoa/ReactiveCocoa.h>
@import
directive before; it was introduced with Xcode 5 and is viewed by Apple as a modern, more efficient replacement to #import
. There’s a great tutorial that covers the new features of Objective-C in What’s New in Objective-C and Foundation in iOS 7.
Add the following four public methods to the interface declaration in WXClient.h:
@import Foundation;
- (RACSignal *)fetchJSONFromURL:(NSURL *)url;
- (RACSignal *)fetchCurrentConditionsForLocation:(CLLocationCoordinate2D)coordinate;
- (RACSignal *)fetchHourlyForecastForLocation:(CLLocationCoordinate2D)coordinate;
- (RACSignal *)fetchDailyForecastForLocation:(CLLocationCoordinate2D)coordinate;
Yes, there might be one tiny thing that you don’t recognize in the code above:
Now seems like a really good time to introduce ReactiveCocoa!
ReactiveCocoa (RAC) is an Objective-C framework for Functional Reactive Programming that provides APIs for composing and transforming streams of values. Instead of focusing on writing serial code — code that executes in an orderly sequence — your code can react to nondeterministic events.
Github provides a great overview of the benefits of ReactiveCocoa, namely :
- The ability to compose operations on future data.
- An approach to minimize state and mutability.
- A declarative way to define behaviors and the relationships between properties.
- A unified, high-level interface for asynchronous operations.
- A lovely API built on top of KVO.
For example, you can observe changes on the username
property of an object like so:
[RACAble(self.username) subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
The subscribeNext
block will be called whenever the value of self.username
changes. The new value is passed to the block.
You can also combine signals and reduce their values into a single composite value. The following sample is taken from the Github page on ReactiveCocoa:
[[RACSignal
combineLatest:@[ RACAble(self.password), RACAble(self.passwordConfirmation) ]
reduce:^(NSString *currentPassword, NSString *currentConfirmPassword) {
return [NSNumber numberWithBool:[currentConfirmPassword isEqualToString:currentPassword]];
}]
subscribeNext:^(NSNumber *passwordsMatch) {
self.createEnabled = [passwordsMatch boolValue];
}];
The RACSignal
object captures present and future values. Signals can be chained, combined, and reacted to by observers. A signal won’t actually perform any work until it is subscribed to.
That means calling [mySignal fetchCurrentConditionsForLocation:someLocation];
won’t do anything but create and return a signal. You’ll see how to subscribe and react later on.
Open WXClient.m and add the following imports:
#import "WXCondition.h"
#import "WXDailyForecast.h"
Under the import section, add the following private interface:
@interface WXClient ()
@property (nonatomic, strong) NSURLSession *session;
@end
This interface has a single property that manages the URL session for your API requests.
Add the following init
method between @implementation
and @end
:
- (id)init {
if (self = [super init]) {
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
_session = [NSURLSession sessionWithConfiguration:config];
}
return self;
}
This simply creates the session for you with defaultSessionConfiguration
.
NSURLSession
before, check out our NSURLSession tutorial to learn more.Building the Signals
You’ll need a master method to build a signal to fetch data from a URL. You already know that three methods are required for fetching the current conditions, the hourly forecast and the daily forecast.
But instead of writing three separate methods, you can follow the DRY (Don’t Repeat Yourself) software design philosophy to make your code easy to maintain.
Some of the following ReactiveCocoa parts may look rather unfamiliar at first. Don’t worry, you’ll go through it piece by piece.
Add the following method to WXClient.m:
- (RACSignal *)fetchJSONFromURL:(NSURL *)url {
NSLog(@"Fetching: %@",url.absoluteString);
// 1
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 2
NSURLSessionDataTask *dataTask = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// TODO: Handle retrieved data
}];
// 3
[dataTask resume];
// 4
return [RACDisposable disposableWithBlock:^{
[dataTask cancel];
}];
}] doError:^(NSError *error) {
// 5
NSLog(@"%@",error);
}];
}
Going through the commented sections one by one, you’ll see that the code does the following:
- Returns the signal. Remember that this will not execute until this signal is subscribed to.
-fetchJSONFromURL:
creates an object for other methods and objects to use; this behavior is sometimes called the factory pattern. - Creates an NSURLSessionDataTask (also new to iOS 7) to fetch data from the URL. You’ll add the data parsing later.
- Starts the the network request once someone subscribes to the signal.
- Creates and returns an
RACDisposable
object which handles any cleanup when the signal when it is destroyed. - Adds a “side effect” to log any errors that occur. Side effects don’t subscribe to the signal; rather, they return the signal to which they’re attached for method chaining. You’re simply adding a side effect that logs on error.
Find the // TODO: Handle retrieved data
line in -fetchJSONFromURL:
and replace it with the following:
if (! error) {
NSError *jsonError = nil;
id json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
if (! jsonError) {
// 1
[subscriber sendNext:json];
}
else {
// 2
[subscriber sendError:jsonError];
}
}
else {
// 2
[subscriber sendError:error];
}
// 3
[subscriber sendCompleted];
Here’s what’s happening in the code above in each numbered section:
- When JSON data exists and there are no errors, send the subscriber the JSON serialized as either an array or dictionary.
- If there is an error in either case, notify the subscriber.
- Whether the request passed or failed, let the subscriber know that the request has completed.
The -fetchJSONFromURL:
method is a little lengthy, but it makes your specific API request methods quite simple in the end.