In March of this year, the company behind Dark Sky and Forecast.io introduced Forecast. Forecast is a simple weather API that provides both short- and longterm weather data. In this series, I will show you how to create a beautiful weather application for iOS powered by Forecast.
Introduction
Earlier this year, the Dark Sky Company introduced Forecast, a simple yet powerful weather API that provides short and longterm weather predictions. In this series we will create an iOS application that is powered by the Forecast API. Forecast is free for up to a thousand API calls per day, so feel free to sign up for a developer account and follow along with me.
Even though a number of open source wrappers for the Forecast API are available, in this series we will use the AFNetworking library to query the Forecast API. In the first part of this series, we will create the project's foundation and implement a basic user interface. Even though our application is simple in scope, it will support multiple locations and that is what we will focus on in this article.
1. Project Setup
Step 1: Creating the Project
Fire up Xcode and create a new project based on the Empty Application template (figure 1). Name the application Rain and enable Automatic Reference Counting (figure 2).
Step 2: Adding Libraries
For this project, we will be using three open source libraries, SVProgressHUD created by Sam Vermette, AFNetworking created by Mattt Thompson, and ViewDeck created by Tom Adriaenssen.
Since I am an avid fan of CocoaPods, I will be using it to install and manage the libraries of our project. If you are not familiar with CocoaPods, then I recommend visiting the website of CocoaPods or reading my introduction to CocoaPods. You can also manually add each library to your project if you prefer to not use CocoaPods.
Close your project, browse to the project's root, and create a file named Podfile. Open Podfile in your favorite text editor and replace its contents with the snippet below. In the project's pod file, we specify the platform, deployment target, and the pods we want to include in the project.
platform :ios, '6.0' pod 'ViewDeck', '~> 2.2.11' pod 'AFNetworking', '~> 1.2.1' pod 'SVProgressHUD', '~> 0.9.0'
Open the Terminal application, browse to the project's root, and install the libraries by executing pod install
. In addition to installing the three pods that we specified in the project's pod file, CocoaPods has already created an Xcode workspace for us. Open the new workspace by executing the open Rain.xcworkspace
command from the command line.
Step 3: Adding Dependencies
Before we can continue, we need to link our project against a handful of frameworks. The AFNetworking library depends on the Mobile Core Services and System Configuration frameworks and the ViewDeck library makes use of the Quartz Core framework. Select the project from the Project Navigator on the left, select the Rain target in the list of targets, open the Build Phases tab at the top, and expand the Link Binary With Libraries drawer. Click the plus button to link your project against the aforementioned frameworks. We will be using the Core Location framework a bit later in this article so now is good time to link your project against that framework as well (figure 3).
Step 4: Edit the Precompiled Header File
Before we start implementing the basic structure of our weather application, it is a good idea to edit the precompiled header file of our project. Add an import statement for each of the frameworks we added to our project a moment ago and do the same for the AFNetworking, SVProgressHUD, and ViewDeck libraries.
#import <Availability.h> #ifndef __IPHONE_3_0 #warning "This project uses features only available in iOS SDK 3.0 and later." #endif #ifdef __OBJC__ #import <UIKit/UIKit.h> #import <Foundation/Foundation.h> #import <QuartzCore/QuartzCore.h> #import <CoreLocation/CoreLocation.h> #import <MobileCoreServices/MobileCoreServices.h> #import <SystemConfiguration/SystemConfiguration.h> #import "AFNetworking.h" #import "SVProgressHUD.h" #import "IIViewDeckController.h" #endif
2. Laying the Foundation
The concept and structure of the application is simple. The application manages three views, (1) a view in the center showing the current weather for a particular location, a view on the right showing the weather for the next few days, and a view on the left with a list of locations. The basic structure will make more sense once we've implemented it. To create this structure, we make use of the terrific ViewDeck library, created and maintained by Tom Adriaenssen. The ViewDeck library is one of the most powerful implementations of the sliding view design pattern originally introduced in Facebook for iOS.
Step 1: View Controllers
Before we put the ViewDeck library to use, we need to create the view controller classes that will manage the three views I mentioned in the previous paragraph. Create three UIViewController
subclasses named MTWeatherViewController
, MTForecastViewController
, and MTLocationsViewController
, respectively (figure 4). Don't forget to create a user interface or XIB file for each class (figure 4).
Step 2: Creating a View Deck Controller
Open MTAppDelegate.m and import the header file the three UIViewController
subclasses. Add a class extension and create a property of type IIViewDeckController
and name it viewDeckController
. The reason for this will become clear in a moment.
#import "MTAppDelegate.h" #import "MTWeatherViewController.h" #import "MTForecastViewController.h" #import "MTLocationsViewController.h" @interface MTAppDelegate () @property (strong, nonatomic) IIViewDeckController *viewDeckController; @end
In application:didFinishLaunchingWithOptions:
, we start by creating an instance of each of the three UIViewController
subclasses. We then initialize an instance of the IIViewDeckController
class and pass the view controller objects as arguments to initWithCenterViewController:leftViewController:rightViewController:
. As the initializer indicates, the view deck controller that we create manages a center, left, and right view controller. The rest of the implementation of application:didFinishLaunchingWithOptions:
should be familiar to you. We initialize the application window and set the view deck controller as its root view controller.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Initialize View Controllers MTLocationsViewController *leftViewController = [[MTLocationsViewController alloc] initWithNibName:@"MTLocationsViewController" bundle:nil]; MTForecastViewController *rightViewController = [[MTForecastViewController alloc] initWithNibName:@"MTForecastViewController" bundle:nil]; MTWeatherViewController *centerViewController = [[MTWeatherViewController alloc] initWithNibName:@"MTWeatherViewController" bundle:nil]; // Initialize View Deck Controller self.viewDeckController = [[IIViewDeckController alloc] initWithCenterViewController:centerViewController leftViewController:leftViewController rightViewController:rightViewController]; // Initialize Window self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Configure Window [self.window setRootViewController:self.viewDeckController]; [self.window makeKeyAndVisible]; return YES; }
Build and run the application in the iOS Simulator or on a physical device to see the ViewDeck library in action. Even though the views of the view controllers are empty, the fundamental application structure is ready.
3. Adding Locations
As I mentioned in the introduction, in this tutorial we will also add the ability to add locations to the list of locations managed by the application. The MTLocationsViewController
class is in charge of managing the list of locations and presenting them in a table view. The user can add the current location to the list of locations by tapping the table view's first row, which will be labeled "Add Current Location". Adding a new location with the Core Location framework is a responsibility of the MTWeatherViewController
class as we will see in a few minutes.
This leaves us with a problem. How is the weather view controller notified when the user has tapped the first row of the locations view controller? Notification center? Delegation is a better choice in this context. Whenever I encounter the need for a one-way communication between two objects, I tend to choose for delegation in favor of notifications. A delegate protocol is much easier to wrap your head around and extending it is as simple as declaring another method.
Another option would be to pass a reference to the weather view controller to the locations view controller, but I don't like this type of tight coupling. Tight coupling makes code less reusable and it results in unnecessarily complex object hierarchies when the code base grows over time. Delegation is the right choice for this problem.
Step 1: Declaring the Delegate Protocol
Open MTLocationsViewController.h and update the header file as shown below. We create a property for the view controller's delegate and we declare the MTLocationsViewControllerDelegate
protocol. The protocol defines two methods, (1) controllerShouldAddCurrentLocation:
, which is invoked when the first row in the view controller's table view is tapped, and (2) controller:didSelectLocation:
, which is invoked when the user select a location from the list of locations.
#import <UIKit/UIKit.h> @protocol MTLocationsViewControllerDelegate; @interface MTLocationsViewController : UIViewController @property (weak, nonatomic) id<MTLocationsViewControllerDelegate> delegate; @end @protocol MTLocationsViewControllerDelegate <NSObject> - (void)controllerShouldAddCurrentLocation:(MTLocationsViewController *)controller; - (void)controller:(MTLocationsViewController *)controller didSelectLocation:(NSDictionary *)location; @end
Step 2: Adding the Table View
Revisit MTLocationsViewController.h one more time. Create an outlet for the view controller's table view and make sure to conform the MTLocationsViewController
class to the UITableViewDataSource
and UITableViewDelegate
protocols.
#import <UIKit/UIKit.h> @protocol MTLocationsViewControllerDelegate; @interface MTLocationsViewController : UIViewController <UITableViewDataSource, UITableViewDelegate> @property (weak, nonatomic) id<MTLocationsViewControllerDelegate> delegate; @property (weak, nonatomic) IBOutlet UITableView *tableView; @end @protocol MTLocationsViewControllerDelegate <NSObject> - (void)controllerShouldAddCurrentLocation:(MTLocationsViewController *)controller; - (void)controller:(MTLocationsViewController *)controller didSelectLocation:(NSDictionary *)location; @end
Open MTLocationsViewController.xib, add a table view to the view controller's view, and set the table view's dataSource
and delegate
outlets to the File's Owner object. Select the File's Owner object and connect its tableView
outlet with the table view that we added to the view controller's view (figure 5).
Step 3: Populating the Table View
Before we implement the UITableViewDataSource
and UITableViewDelegate
protocols, we need to create a property that will serve as the table view's data source. Create a class extension at the top of MTLocationsViewController.m and create a property named locations
of type NSMutableArray
. It will store the locations managed by our application.
#import "MTLocationsViewController.h" @interface MTLocationsViewController () @property (strong, nonatomic) NSMutableArray *locations; @end
Implementing the UITableViewDataSource
and the UITableViewDelegate
protocols is pretty straightforward. We start by declaring a static string constant for the cell reuse identifier. In the view controller's viewDidLoad
method we invoke setupView
, a helper method in which we configure the view controller's user interface. In setupView
, we tell the table view to use the UITableViewCell
class to instantiate new table view cells for the reuse identifier we declared earlier.
static NSString *LocationCell = @"LocationCell";
- (void)viewDidLoad { [super viewDidLoad]; // Setup View [self setupView]; }
- (void)setupView { // Register Class for Cell Reuse [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:LocationCell]; }
Implementing the UITableViewDataSource
and UITableViewDelegate
protocols is trivial as you can see below. Two implementation details require a bit explaining. We store each location as a dictionary with four keys, (1) city, (2) country, (3) latitude, and (4) longitude. With this in mind, the implementation of configureCell:atIndexPath:
should become a bit clearer. Note that configureCell:atIndexPath:
is nothing more than another helper method.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return ([self.locations count] + 1); } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:LocationCell forIndexPath:indexPath]; // Configure Cell [self configureCell:cell atIndexPath:indexPath]; return cell; } - (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath { if (indexPath.row == 0) { [cell.textLabel setText:@"Add Current Location"]; } else { // Fetch Location NSDictionary *location = [self.locations objectAtIndex:(indexPath.row - 1)]; // Configure Cell [cell.textLabel setText:[NSString stringWithFormat:@"%@, %@", location[@"city"], location[@"country"]]]; } } - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return NO; } - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { return NO; }
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; if (indexPath.row == 0) { // Notify Delegate [self.delegate controllerShouldAddCurrentLocation:self]; } else { // Fetch Location NSDictionary *location = [self.locations objectAtIndex:(indexPath.row - 1)]; // Notify Delegate [self.delegate controller:self didSelectLocation:location]; } // Show Center View Controller [self.viewDeckController closeLeftViewAnimated:YES]; }
The tableView:didSelectRowAtIndexPath:
also requires a short explanation. If the user taps the first row, labeled Add Current Location, the delegate is notified that the current location should be added to the list of locations. If any other row in the table view is tapped, the corresponding location is passed as the second argument of controller:didSelectLocation:
, another delegate method of the MTLocationsViewController
delegate protocol. This informs the delegate that the application should set the new location as the application't default location and the weather data for that location should be fetched.
The last line of tableView:didSelectRowAtIndexPath:
is also worth mentioning. The IIViewDeckController
instance assigns itself to the view controllers it manages. The viewDeckController
property provides access to a view controller's view deck controller, which is convenient if you need to access the view controller's view deck. It works very much like the navigationController
property of a view controller instance. In the last line of tableView:didSelectRowAtIndexPath:
, we tell the view deck controller to close the left view, which means that the center view becomes visible again.
Step 4: Keys and Constants
Before we continue populating the locations table view, we need to pay attention to some best practices. We currently use string literals to access the values of the location dictionary. Even though this works perfectly fine, it is better and safer to use string constants for this purpose. To make all this easy and maintainable, we declare the string constants in a central location. Let me show you how this works.
Create a subclass of NSObject
and name it MTConstants
. Replace the contents of MTConstants.h and MTConstants.m with the snippets shown below. It should be clear that MTConstants
is not an Objective-C class. It is nothing more than a central place to store a set of constants specific to our project.
#pragma mark - #pragma mark User Defaults extern NSString * const MTRainUserDefaultsLocation; extern NSString * const MTRainUserDefaultsLocations; #pragma mark - #pragma mark Notifications extern NSString * const MTRainDidAddLocationNotification; extern NSString * const MTRainLocationDidChangeNotification; #pragma mark - #pragma mark Location Keys extern NSString * const MTLocationKeyCity; extern NSString * const MTLocationKeyCountry; extern NSString * const MTLocationKeyLatitude; extern NSString * const MTLocationKeyLongitude;
#import "MTConstants.h" #pragma mark - #pragma mark User Defaults NSString * const MTRainUserDefaultsLocation = @"location"; NSString * const MTRainUserDefaultsLocations = @"locations"; #pragma mark - #pragma mark Notifications NSString * const MTRainDidAddLocationNotification = @"com.mobileTuts.MTRainDidAddLocationNotification"; NSString * const MTRainLocationDidChangeNotification = @"com.mobileTuts.MTRainLocationDidChangeNotification"; #pragma mark - #pragma mark Location Keys NSString * const MTLocationKeyCity = @"city"; NSString * const MTLocationKeyCountry = @"country"; NSString * const MTLocationKeyLatitude = @"latitude"; NSString * const MTLocationKeyLongitude = @"longitude";
To make MTConstants
really useful, add an import statement for MTConstants.h to your project's precompiled header file so the constants declared in MTConstants
are available throughout the project.
#import <Availability.h> #ifndef __IPHONE_3_0 #warning "This project uses features only available in iOS SDK 3.0 and later." #endif #ifdef __OBJC__ #import <UIKit/UIKit.h> #import <Foundation/Foundation.h> #import <QuartzCore/QuartzCore.h> #import <CoreLocation/CoreLocation.h> #import <MobileCoreServices/MobileCoreServices.h> #import <SystemConfiguration/SystemConfiguration.h> #import "AFNetworking.h" #import "SVProgressHUD.h" #import "IIViewDeckController.h" #import "MTConstants.h" #endif
We can now update configureCell:atIndexPath:
(MTLocationsViewController.m) as shown below. Not only will this give use code completion and a very, very small performance gain, the true benefit of this best practice is that the compiler will warn us in case of typos. I'm sure I don't have to tell you that typos are one of the most common causes of bugs in software development.
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath { if (indexPath.row == 0) { [cell.textLabel setText:@"Add Current Location"]; } else { // Fetch Location NSDictionary *location = [self.locations objectAtIndex:(indexPath.row - 1)]; // Configure Cell [cell.textLabel setText:[NSString stringWithFormat:@"%@, %@", location[MTLocationKeyCity], location[MTLocationKeyCountry]]]; } }
At the moment, the locations
property is empty and so will the table view. In initWithNibName:bundle:
, we invoke loadLocations
, a helper method that loads the array of locations. In loadLocations
, load the array of locations that is stored in the application's user defaults database. Note that we use another string constant that we declared in MTConstants.h.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { // Load Locations [self loadLocations]; } return self; }
- (void)loadLocations { self.locations = [NSMutableArray arrayWithArray:[[NSUserDefaults standardUserDefaults] objectForKey:MTRainUserDefaultsLocations]]; }
Step 5: Delegate Assignment
As I mentioned earlier, the MTWeatherViewController
instance will serve as the delegate of the locations view controller. Revisit MTAppDelegate.m and update application:didFinishLaunchingWithOptions:
as shown below.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Initialize View Controllers MTLocationsViewController *leftViewController = [[MTLocationsViewController alloc] initWithNibName:@"MTLocationsViewController" bundle:nil]; MTForecastViewController *rightViewController = [[MTForecastViewController alloc] initWithNibName:@"MTForecastViewController" bundle:nil]; MTWeatherViewController *centerViewController = [[MTWeatherViewController alloc] initWithNibName:@"MTWeatherViewController" bundle:nil]; // Configure Locations View Controller [leftViewController setDelegate:centerViewController]; // Initialize View Deck Controller self.viewDeckController = [[IIViewDeckController alloc] initWithCenterViewController:centerViewController leftViewController:leftViewController rightViewController:rightViewController]; // Initialize Window self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Configure Window [self.window setRootViewController:self.viewDeckController]; [self.window makeKeyAndVisible]; return YES; }
By making this change, a warning should immediately pop up telling you that MTWeatherViewController
does not conform to the MTLocationsViewControllerDelegate
protocol. The compiler is right so let's fix this.
Open MTWeatherViewController.h, import the header file of MTLocationsViewController
, and conform MTWeatherViewController
to the MTLocationsViewControllerDelegate
protocol.
#import <UIKit/UIKit.h> #import "MTLocationsViewController.h" @interface MTWeatherViewController : UIViewController <MTLocationsViewControllerDelegate> @end
Wait. Another warning? We haven't implemented the two required methods of the delegate protocol yet hence the warning. Open MTWeatherViewController.m and add a stub implementation for each of the delegate methods.
- (void)controllerShouldAddCurrentLocation:(MTLocationsViewController *)controller { NSLog(@"%s", __PRETTY_FUNCTION__); } - (void)controller:(MTLocationsViewController *)controller didSelectLocation:(NSDictionary *)location { NSLog(@"%s", __PRETTY_FUNCTION__); }
Step 6: Fetching the Current Location
It is finally time to fetch the current location of the device. Add a class extension at the top of MTWeatherViewController.m and declare two properties, (1) location
(NSDictionary
) to store the application's default location and (2) locationManager
(CLLocationManager
), which we will use to fetch the device's location. Conform the MTWeatherViewController
class to the CLLocationManagerDelegate
protocol and declare an instance variable named _locationFound
of type BOOL
. The purpose of _locationFound
will become clear in a few minutes.
#import "MTWeatherViewController.h" @interface MTWeatherViewController () <CLLocationManagerDelegate> { BOOL _locationFound; } @property (strong, nonatomic) NSDictionary *location; @property (strong, nonatomic) CLLocationManager *locationManager; @end
In the class's designated initializer, initWithNibName:bundle:
, we initialize and configure the location manager. We assign the view controller as the location manager's delegate and set the location manager's accuracy
property to kCLLocationAccuracyKilometer
. There is no need for better accuracy as we only need the location for weather data.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { // Initialize Location Manager self.locationManager = [[CLLocationManager alloc] init]; // Configure Location Manager [self.locationManager setDelegate:self]; [self.locationManager setDesiredAccuracy:kCLLocationAccuracyKilometer]; } return self; }
The next piece of the puzzle is implementing, locationManager:didUpdateLocations:
, one of the methods of the CLLocationManagerDelegate
protocol, which is invoked each time the location manager has updated the device's location. The second argument of locationManager:didUpdateLocations:
is an array CLLocation
instances. The implementation of locationManager:didUpdateLocations:
also reveals the purpose of _locationFound
. Despite the fact that we tell the location manager to stop updating the location as soon as locationManager:didUpdateLocations:
is invoked, it is not uncommon that another update of the location invokes locationManager:didUpdateLocations:
again even after sending the location manager a message of stopUpdatingLocation
. If this were to happen, the same location would be added twice to the list of locations. The simple solution is to use a helper variable, _locationFound
, that keeps track of the state that we're in.
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { if (![locations count] || _locationFound) return; // Stop Updating Location _locationFound = YES; [manager stopUpdatingLocation]; // Current Location CLLocation *currentLocation = [locations objectAtIndex:0]; // Reverse Geocode CLGeocoder *geocoder = [[CLGeocoder alloc] init]; [geocoder reverseGeocodeLocation:currentLocation completionHandler:^(NSArray *placemarks, NSError *error) { if ([placemarks count]) { _locationFound = NO; [self processPlacemark:[placemarks objectAtIndex:0]]; } }]; }
We extract the first location from the array of locations and use the CLGeocoder
class to reverse geocode that location. Reverse geocoding simply means finding out the name of the (closest) city and the location's country. The completion handler of reverseGeocodeLocation:
returns an array of placemarks. A placemark object is nothing more than a container for storing location data for a coordinate.
- (void)processPlacemark:(CLPlacemark *)placemark { // Extract Data NSString *city = [placemark locality]; NSString *country = [placemark country]; CLLocationDegrees lat = placemark.location.coordinate.latitude; CLLocationDegrees lon = placemark.location.coordinate.longitude; // Create Location Dictionary NSDictionary *currentLocation = @{ MTLocationKeyCity : city, MTLocationKeyCountry : country, MTLocationKeyLatitude : @(lat), MTLocationKeyLongitude : @(lon) }; // Add to Locations NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; NSMutableArray *locations = [NSMutableArray arrayWithArray:[ud objectForKey:MTRainUserDefaultsLocations]]; [locations addObject:currentLocation]; [locations sortUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:MTLocationKeyCity ascending:YES]]]; [ud setObject:locations forKey:MTRainUserDefaultsLocations]; // Synchronize [ud synchronize]; // Update Current Location self.location = currentLocation; // Post Notifications NSNotification *notification2 = [NSNotification notificationWithName:MTRainDidAddLocationNotification object:self userInfo:currentLocation]; [[NSNotificationCenter defaultCenter] postNotification:notification2]; }
In processPlacemark:
, we extract the data that we're looking for, city, country, latitude, and longitude, store it in a dictionary, and update the array of locations in the application's user defaults database. Note that we sort the array of locations before updating the user defaults database. The view controller's location
property is updated with the new location and a notification is sent to notify any object interested in this event.
That's not all, though. I have also overridden the setter of the view controller's location
property. Because the MTWeatherViewController
class is in charge of adding new locations, we can delegate a few additional responsibilities to this class, such as updating the default location in the user defaults database. Because other parts of the application also need to know about a change in location, a notification with name MTRainLocationDidChangeNotification
is posted. We also invoke updateView
, another helper method that we will implement shortly.
- (void)setLocation:(NSDictionary *)location { if (_location != location) { _location = location; // Update User Defaults NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; [ud setObject:location forKey:MTRainUserDefaultsLocation]; [ud synchronize]; // Post Notification NSNotification *notification1 = [NSNotification notificationWithName:MTRainLocationDidChangeNotification object:self userInfo:location]; [[NSNotificationCenter defaultCenter] postNotification:notification1]; // Update View [self updateView]; } }
Step 7: Adding a Label
We won't spend much time on the user interface in this tutorial, but to make sure that everything works as we expect, it is good to have some visual feedback by adding a label to the weather view controller's view displaying the selected location. Open MTWeatherViewController.h and create an outlet of type UILabel
and name it labelLocation
.
#import <UIKit/UIKit.h> #import "MTLocationsViewController.h" @interface MTWeatherViewController : UIViewController <MTLocationsViewControllerDelegate> @property (weak, nonatomic) IBOutlet UILabel *labelLocation; @end
Open MTWeatherViewController.xib, add a label to the view controller's view, and connect the outlet with the newly added label (figure 6). In updateView
(MTWeatherViewController.m), we update the label with the new location as shown below.
- (void)updateView { // Update Location Label [self.labelLocation setText:[self.location objectForKey:MTLocationKeyCity]]; }
Step 8: Implementing the Delegate Protocol
Thanks to the work we have done so far, implementing the two methods of the MTLocationsViewControllerDelegate
protocol is easy. In controllerShouldAddCurrentLocation:
, we tell the location manager to start updating the location. In controller:didSelectLocation:
, we set the view controller's location
property to the location that the user selected in the locations view controller, which in turn invokes the setter method we overrode a bit earlier.
- (void)controllerShouldAddCurrentLocation:(MTLocationsViewController *)controller { // Start Updating Location [self.locationManager startUpdatingLocation]; } - (void)controller:(MTLocationsViewController *)controller didSelectLocation:(NSDictionary *)location { // Update Location self.location = location; }
4. Final Touches
Before wrapping up the first installment of this series, we need to add a few final touches. When the user launches the application, for example, the location
property of the MTWeatherViewController
class needs to be set to the location stored in the application's user defaults. In addition, when the user launches our application for the very first time, a default location is not yet set in the application's user defaults. This isn't a big problem, but to offer a good user experience it would be better to automatically fetch the user's current location when the application launches for the first time.
We can make both changes by amending the weather view controller's viewDidLoad
as shown below. The view controller's location property is set to the location stored in the user defaults database. If no location is found, that is, self.location
is nil
, we tell the location manager to start updating the location. In other words, when the application is launched for the first time, the current location is automatically retrieved and stored.
- (void)viewDidLoad { [super viewDidLoad]; // Load Location self.location = [[NSUserDefaults standardUserDefaults] objectForKey:MTRainUserDefaultsLocation]; if (!self.location) { [self.locationManager startUpdatingLocation]; } }
There is one more loose end that we need to tie up. When a new location is added to the array of locations, the weather view controller posts a notification. We need to update the MTLocationsViewController
class so that it adds itself as an observer for these notifications. By doing so, the locations view controller can update its table view whenever a new location is added.
Revisit MTLocationsViewController.m and update the initWithNibName:bundle:
as shown below. We add the view controller as an observer for notifications with a name of MTRainDidAddLocationNotification
. The implementation of didAddLocation:
is straightforward, that is, we add the new location to the array of locations, sort the array by city, and reload the table view.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { // Load Locations [self loadLocations]; // Add Observer [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didAddLocation:) name:MTRainDidAddLocationNotification object:nil]; } return self; }
- (void)didAddLocation:(NSNotification *)notification { NSDictionary *location = [notification userInfo]; [self.locations addObject:location]; [self.locations sortUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:MTLocationKeyCity ascending:YES]]]; [self.tableView reloadData]; }
Don't forget to remove the view controller as an observer in the view controller's dealloc
method. It is also good practice to set the view controller's delegate
property to nil
in dealloc
.
- (void)dealloc { // Remove Observer [[NSNotificationCenter defaultCenter] removeObserver:self]; if (_delegate) { _delegate = nil; } }
Build and run the application to see how all this works together. You may want to run the application in the iOS Simulator instead of a on physical device, because the iOS Simulator supports location simulation (figure 7), which makes it much easier to test location based applications such as the one we created.
Conclusion
Even though we haven't even touched the Forecast API, we did quite a bit of work in this article. I hope you have tried CocoaPods and are convinced of its power and flexibility. In the next installment of this series, we focus on the Forecast API and the AFNetworking library.
Comments