Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

iOS 7 Best Practices; A Weather App Case Study: Part 2/2

$
0
0
Finished Weather App

Finished Weather App

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>
Note: You may not have seen the @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:

RACSignal??

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.

Note: If you haven’t encountered 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:

  1. 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.
  2. Creates an NSURLSessionDataTask (also new to iOS 7) to fetch data from the URL. You’ll add the data parsing later.
  3. Starts the the network request once someone subscribes to the signal.
  4. Creates and returns an RACDisposable object which handles any cleanup when the signal when it is destroyed.
  5. 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.
Note: If you feel like you need a little more background, check out this post by Ash Furrow to gain a better understanding of the core concepts of ReactiveCocoa.

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:

  1. When JSON data exists and there are no errors, send the subscriber the JSON serialized as either an array or dictionary.
  2. If there is an error in either case, notify the subscriber.
  3. 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.

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.h:

- (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.

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:

  1. Always ignore the first location update because it is almost always cached.
  2. Once you have a location with the proper accuracy, stop further updates.
  3. Setting the currentLocation key triggers the RACObservable you set earlier in the init 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:

  1. Observes the currentCondition key on the WXManager singleton.
  2. Delivers any changes on the main thread since you’re updating the UI.
  3. 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.
  4. 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! :]

Wiring up the UI

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?

Rainbow Vom

Okay, pick your jaw up off the floor. It’s time to move on.

Note: For more examples of powerful bindings, check out the ReactiveCocoa Readme.

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:

  1. The RAC(…) macro helps keep syntax clean. The returned value from the signal is assigned to the text key of the hiloLabel object.
  2. 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.
  3. Reduce the values from your combined signals into a single value; note that the parameter order matches the order of your signals.
  4. 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:

UI Wiring with Bindings

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:

  1. The first section is for the hourly forecast. Use the six latest hourly forecasts and add one more cell for the header.
  2. The next section is for daily forecasts. Use the six latest daily forecasts and add one more cell for the header.
Note: You’re using table cells for headers here instead of the built-in section headers which have sticky-scrolling behavior. The table view is set up with paging enabled and sticky-scrolling behavior would look odd in this context.

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:

  1. The first row of each section is the header cell.
  2. Get the hourly weather and configure the cell using custom configure methods.
  3. 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:

  1. Configures and adds text to the cell used as the section header. You’ll reuse this for daily and hourly forecast sections.
  2. Formats the cell for an hourly forecast.
  3. 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.

Solution Inside: Solution SelectShow>

Build and run your app once more; scroll the table views and you’ll see all the forecast data populate, as below:

Forecast with Odd Heights

Adding Polish to Your App

The pages for the hourly and daily forecasts aren’t taking up the whole screen. Fortunately, this turns out to be a real easy fix. Earlier in the tutorial you captured the screen height in -viewDidLoad.

Find the table view delegate method -tableView:heightForRowAtIndexPath: in WXController.m and replace the TODO and return lines with the following:

NSInteger cellCount = [self tableView:tableView numberOfRowsInSection:indexPath.section];
return self.screenHeight / (CGFloat)cellCount;

This divides the screen height by the number of cells in each section so the total height of all cells equals the height of the screen.

Build and run your app; the table view now fills the entire screen as shown in the screenshot below:

Forecast with Full Height

The last thing to do is incorporate the blur I mentioned at the beginning of Part 1 of this tutorial. The blur should fill in dynamically as you scroll past the first page of forecast.

Add the following scroll delegate near the bottom of WXController.m:

#pragma mark - UIScrollViewDelegate
 
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    // 1
    CGFloat height = scrollView.bounds.size.height;
    CGFloat position = MAX(scrollView.contentOffset.y, 0.0);
    // 2
    CGFloat percent = MIN(position / height, 1.0);
    // 3
    self.blurredImageView.alpha = percent;
}

This method is pretty straightforward:

  1. Get the height of the scroll view and the content offset. Cap the offset at 0 so attempting to scroll past the start of the table won’t affect blurring.
  2. Divide the offset by the height with a maximum of 1 so that your offset is capped at 100%.
  3. Assign the resulting value to the blur image’s alpha property to change how much of the blurred image you’ll see as you scroll.

Build and run your app; scroll your table view and check out the awesome blur effect:

Finished Product

Where To Go From Here?

You’ve accomplished a lot in this tutorial: you created a project using CocoaPods, built a view structure completely in code, created data models and managers, and wired it all together using Functional Programming!

You can download a finished copy of the project here.

There are lots of cool places you could take this app. A neat start would be to use the Flickr API to find background images based on the device’s location.

As well, your app only deals with temperature and conditions; what other weather information could you integrate into your app?

Thanks for following along! If you’ve got any questions or comments, please share them below!

iOS 7 Best Practices; A Weather App Case Study: Part 2/2 is a post from: Ray Wenderlich

The post iOS 7 Best Practices; A Weather App Case Study: Part 2/2 appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>