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.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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:

  1. Format the URL from a CLLocationCoordinate2D object using its latitude and longitude.
  2. 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.
  3. Use MTLJSONAdapter to convert the JSON into an WXCondition object, using the MTLJSONSerializing protocol you created for WXCondition.

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:

  1. Use -fetchJSONFromURL again and map the JSON as appropriate. Note how much code you’re saving by reusing this call!
  2. Build an RACSequence from the “list” key of the JSON. RACSequences let you perform ReactiveCocoa operations on lists.
  3. Map the new list of objects. This calls -map: on each object in the list, returning a list of new objects.
  4. Use MTLJSONAdapter again to convert the JSON into a WXCondition object.
  5. Using -map on RACSequence returns another RACSequence, so use this convenience method to get the data as an NSArray.

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.

Labels and Views

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:

  1. 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.
  2. Use instancetype instead of WXManager so subclasses will return the appropriate type.
  3. These properties will store your data. Since WXManager is a singleton, these properties will be accessible anywhere. Set the public properties to readonly as only the manager should ever change these values privately.
  4. 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:

  1. 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.
  2. 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:

  1. Creates a location manager and sets it’s delegate to self.
  2. Creates the WXClient object for the manager. This handles all networking and data parsing, following our separation of concerns best practice.
  3. 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.
  4. In order to continue down the method chain, currentLocation must not be nil.
  5. -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.
  6. Deliver the signal to subscribers on the main thread.
  7. 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.

Ryan Nystrom

Contributors

Ryan Nystrom

Author

Over 300 content creators. Join our team.