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.
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.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:
- 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.
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:
- Always ignore the first location update because it is almost always cached.
- Once you have a location with the proper accuracy, stop further updates.
- Setting the
currentLocation
key triggers the RACObservable you set earlier in theinit
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:
- Observes the
currentCondition
key on the WXManager singleton. - Delivers any changes on the main thread since you’re updating the UI.
- 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. - 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! :]
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?
Okay, pick your jaw up off the floor. It’s time to move on.
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:
- The RAC(…) macro helps keep syntax clean. The returned value from the signal is assigned to the
text
key of thehiloLabel
object. - 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. - Reduce the values from your combined signals into a single value; note that the parameter order matches the order of your signals.
- 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:
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:
- The first section is for the hourly forecast. Use the six latest hourly forecasts and add one more cell for the header.
- The next section is for daily forecasts. Use the six latest daily forecasts and add one more cell for the header.
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:
- The first row of each section is the header cell.
- Get the hourly weather and configure the cell using custom configure methods.
- 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:
- Configures and adds text to the cell used as the section header. You’ll reuse this for daily and hourly forecast sections.
- Formats the cell for an hourly forecast.
- 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.
Build and run your app once more; scroll the table views and you’ll see all the forecast data populate, as below:
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:
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:
- 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.
- Divide the offset by the height with a maximum of 1 so that your offset is capped at 100%.
- 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:
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.