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?
Fetching Current Conditions
Still working in WXClient.m, add the following method:
- (RACSignal *)fetchCurrentConditionsForLocation:(CLLocationCoordinate2D)coordinate {
// 1
NSString *urlString = [NSString stringWithFormat:@"http://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&units=imperial",coordinate.latitude, coordinate.longitude];
NSURL *url = [NSURL URLWithString:urlString];
// 2
return [[self fetchJSONFromURL:url] map:^(NSDictionary *json) {
// 3
return [MTLJSONAdapter modelOfClass:[WXCondition class] fromJSONDictionary:json error:nil];
}];
}
Taking each comment in turn:
- Format the URL from a
CLLocationCoordinate2D
object using its latitude and longitude. - Use the method you just built to create the signal. Since the returned value is a signal, you can call other ReactiveCocoa methods on it. Here you map the returned value — an instance of NSDictionary — into a different value.
- Use
MTLJSONAdapter
to convert the JSON into anWXCondition
object, using theMTLJSONSerializing
protocol you created forWXCondition
.
Fetching the Hourly Forecast
Now add the following method to WXClient.m, which fetches the hourly forecast for a given set of coordinates:
- (RACSignal *)fetchHourlyForecastForLocation:(CLLocationCoordinate2D)coordinate {
NSString *urlString = [NSString stringWithFormat:@"http://api.openweathermap.org/data/2.5/forecast?lat=%f&lon=%f&units=imperial&cnt=12",coordinate.latitude, coordinate.longitude];
NSURL *url = [NSURL URLWithString:urlString];
// 1
return [[self fetchJSONFromURL:url] map:^(NSDictionary *json) {
// 2
RACSequence *list = [json[@"list"] rac_sequence];
// 3
return [[list map:^(NSDictionary *item) {
// 4
return [MTLJSONAdapter modelOfClass:[WXCondition class] fromJSONDictionary:item error:nil];
// 5
}] array];
}];
}
It’s a fairly short method, but there’s a lot going on:
- Use
-fetchJSONFromURL
again and map the JSON as appropriate. Note how much code you’re saving by reusing this call! - Build an
RACSequence
from the “list” key of the JSON.RACSequence
s let you perform ReactiveCocoa operations on lists. - Map the new list of objects. This calls
-map:
on each object in the list, returning a list of new objects. - Use
MTLJSONAdapter
again to convert the JSON into aWXCondition
object. - Using
-map
onRACSequence
returns anotherRACSequence
, so use this convenience method to get the data as anNSArray
.
Fetching the Daily Forecast
Finally, add the following method to WXClient.m:
- (RACSignal *)fetchDailyForecastForLocation:(CLLocationCoordinate2D)coordinate {
NSString *urlString = [NSString stringWithFormat:@"http://api.openweathermap.org/data/2.5/forecast/daily?lat=%f&lon=%f&units=imperial&cnt=7",coordinate.latitude, coordinate.longitude];
NSURL *url = [NSURL URLWithString:urlString];
// Use the generic fetch method and map results to convert into an array of Mantle objects
return [[self fetchJSONFromURL:url] map:^(NSDictionary *json) {
// Build a sequence from the list of raw JSON
RACSequence *list = [json[@"list"] rac_sequence];
// Use a function to map results from JSON to Mantle objects
return [[list map:^(NSDictionary *item) {
return [MTLJSONAdapter modelOfClass:[WXDailyForecast class] fromJSONDictionary:item error:nil];
}] array];
}];
}
Does this look familiar? Yup — this method is exactly the same as -fetchHourlyForecastForLocation:
, except it uses WXDailyForecast
instead of WXCondition
and fetches the daily forecast.
Build and run your app; you won’t see anything new at this time, but it’s a good spot to catch your breath and ensure there aren’t any errors or warnings.
Managing & Storing Your Data
It’s time to flesh out WXManager
, the class that brings everything together. This class implements some key functions of your app:
- It follows the singleton design pattern.
- It attempts to find the device’s location.
- After finding the location, it fetches the appropriate weather data.
Open WXManager.h and replace the contents with the following code:
@import Foundation;
@import CoreLocation;
#import <ReactiveCocoa/ReactiveCocoa/ReactiveCocoa.h>
// 1
#import "WXCondition.h"
@interface WXManager : NSObject
<CLLocationManagerDelegate>
// 2
+ (instancetype)sharedManager;
// 3
@property (nonatomic, strong, readonly) CLLocation *currentLocation;
@property (nonatomic, strong, readonly) WXCondition *currentCondition;
@property (nonatomic, strong, readonly) NSArray *hourlyForecast;
@property (nonatomic, strong, readonly) NSArray *dailyForecast;
// 4
- (void)findCurrentLocation;
@end
There’s nothing earth-shattering here, but here’s a few points to note from the commented sections above:
- Note that you’re not importing WXDailyForecast.h; you’ll always use
WXCondition
as the forecast class.WXDailyForecast
only exists to help Mantle transform JSON to Objective-C. - Use
instancetype
instead ofWXManager
so subclasses will return the appropriate type. - These properties will store your data. Since
WXManager
is a singleton, these properties will be accessible anywhere. Set the public properties toreadonly
as only the manager should ever change these values privately. - This method starts or refreshes the entire location and weather finding process.
Now open WXManager.m and add the following imports to the top of the file:
#import "WXClient.h"
#import <TSMessages/TSMessage.h>
Right beneath the imports, paste in the private interface as follows:
@interface WXManager ()
// 1
@property (nonatomic, strong, readwrite) WXCondition *currentCondition;
@property (nonatomic, strong, readwrite) CLLocation *currentLocation;
@property (nonatomic, strong, readwrite) NSArray *hourlyForecast;
@property (nonatomic, strong, readwrite) NSArray *dailyForecast;
// 2
@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, assign) BOOL isFirstUpdate;
@property (nonatomic, strong) WXClient *client;
@end
Here’s the deets on the properties above:
- Declare the same properties you added in the public interface, but this time declare them as
readwrite
so you can change the values behind the scenes. - Declare a few other private properties for location finding and data fetching.
Add the following generic singleton constructor between @implementation
and @end
:
+ (instancetype)sharedManager {
static id _sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedManager = [[self alloc] init];
});
return _sharedManager;
}
Next, you need to set up your properties and observables.
Add the following method to WXManager.m:
- (id)init {
if (self = [super init]) {
// 1
_locationManager = [[CLLocationManager alloc] init];
_locationManager.delegate = self;
// 2
_client = [[WXClient alloc] init];
// 3
[[[[RACObserve(self, currentLocation)
// 4
ignore:nil]
// 5
// Flatten and subscribe to all 3 signals when currentLocation updates
flattenMap:^(CLLocation *newLocation) {
return [RACSignal merge:@[
[self updateCurrentConditions],
[self updateDailyForecast],
[self updateHourlyForecast]
]];
// 6
}] deliverOn:RACScheduler.mainThreadScheduler]
// 7
subscribeError:^(NSError *error) {
[TSMessage showNotificationWithTitle:@"Error"
subtitle:@"There was a problem fetching the latest weather."
type:TSMessageNotificationTypeError];
}];
}
return self;
}
You’re using more ReactiveCocoa methods to observe and react to value changes. Here’s what the method above does:
- Creates a location manager and sets it’s delegate to
self
. - Creates the
WXClient
object for the manager. This handles all networking and data parsing, following our separation of concerns best practice. - The manager observes the
currentLocation
key on itself using a ReactiveCocoa macro which returns a signal. This is similar to Key-Value Observing but is far more powerful. - In order to continue down the method chain,
currentLocation
must not benil
. -
-flattenMap:
is very similar to-map:
, but instead of mapping each value, it flattens the values and returns one object containing all three signals. In this way, you can consider all three processes as a single unit of work. - Deliver the signal to subscribers on the main thread.
- It’s not good practice to interact with the UI from inside your model, but for demonstration purposes you’ll display a banner whenever an error occurs.
Next up, in order to display an accurate weather forecast, we need to determine the location of the device.