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 3 of 4 of this article. Click here to view the first page.

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.

[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:

Forecast with Odd Heights

Ryan Nystrom

Contributors

Ryan Nystrom

Author

Over 300 content creators. Join our team.