Create a Weather App with Forecast - Project Setup

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

Creating a Weather App for iOS with Forecast.io: Part 1 - Setting Up the Project
Figure 1: Setting Up the Project
Creating a Weather App for iOS with Forecast.io: Part 1 - Configuring the Project
Figure 2: Configuring the Project

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.

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

Creating a Weather App for iOS with Forecast.io: Part 1 - Linking the Project Against a Handful of Frameworks
Figure 3: Linking the Project Against a Handful of Frameworks

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.


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

Creating a Weather App for iOS with Forecast.io: Part 1 - Creating the Three Main View Controller Classes
Figure 4: Creating the Three Main View Controller Classes

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.

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.

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.

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.

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

Creating a Weather App for iOS with Forecast.io: Part 1 - Adding a Table View to List the Locations
Figure 5: Adding a Table View to List the Locations

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

Creating a Weather App for iOS with Forecast.io: Part 1 - Adding the Location Label to the Weather View Controller
Figure 6: Adding the Location Label to the Weather View Controller

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.


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.

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.

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.

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.

Creating a Weather App for iOS with Forecast.io: Part 1 - Simulating Locations with the iOS Simulator
Figure 7: Simulating Locations with the iOS Simulator

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.

Tags:

Comments

Related Articles