iOS Programming Recipe 15: Building A Weather Application

This recipe will cover building a basic weather application for the iPhone from start to finish (no graphics needed, all code). Initially, this application will be very basic, but many enhancements/improvements may be the topic of future recipes. This recipe will move fast! If you find yourself having trouble keeping up you should go back and work through our earlier recipes before attempting this one. When you’re done with this recipe, you should have an app that looks like the following:

recipe15-8

Before We Get Started

  • This application will be built using CocoaPods which is an Objective-C dependency manager. If you are not familiar with CocoaPods or just need some help getting it installed please visit their website. Additionally, there is a very good video tutorial on CocoaPods that NSScreencast has done which is currently free to view. Make sure you get CocoaPods installed on your system before starting this article, because installation will not be covered in the recipe.
  • The application will also use API provided by Weather Underground which will require you to sign up for a free developer API Key (limited to 500 requests per day I believe). Don’t worry this is very simple and will be covered in the recipe.
  • The source code to this recipe is available online through GitHub, and it may help to check it out when working through this tutorial.

Let’s Get Started

Creating The Project
  • Open Xcode and create a new Single View Application.

1

  • Name the application Weather (choose your own name if you like but, we will refer to it as weather from now on). This application will be for iPhone only so select iPhone from the devices drop-down menu. Make sure Automatic Reference Counting has been checked.

2

Setting Up CocoaPods
  • Close the project you just created.
  • Open Terminal and navigate to the directory containing weather.xcodeproj
  • Make sure CocoaPods is installed on your system then type pod install. This will fail with the following error if you have CocoaPods properly installed.

3

  • The error basically says CocoaPods did not find a Podfile in our project root directory, which is because we haven’t created one. So let’s create one!
  • As the image above shows, we are using TextMate to create this file, but use whatever text editor you like.
  • If you are familiar with CocoaPods, the following Podfile syntax should look familiar to you. Unfortunately there is not enough time to cover Podfiles in this recipe, please refer to NSScreenCast’s CocoaPods Video Tutorial for that.
  • In your text editor type the following Podfile, then save it in the project root direct as Podfile (no extension).

platform :ios, '6.0'
pod 'AFNetworking',         '~> 1.1'
pod 'SVProgressHUD',        '~> 0.8.1'
  • The Podfile describes to CocoaPods which open source dependencies our application needs.
    • AFNetworking will be used for networking (communicating with Weather Underground Web Services)
    • SVProgressHUD will be used in place of the default UIActivityIndicatorView
  • Once you have that Podfile in place, run pod install from the terminal again. You should now see the following output.

5

  • CocoaPods created a Pods folder, a Podfile.lock file, and a new workspace file for your application. It is important that you now open your app using the new weather.xcworkspace file instead of weather.xcodeproj. The workspace will contain the necessary dependencies that your app will need.
Registering With Weather Underground
  • You need to obtain a developer API key from Weather Underground in order to access weather data through their services. This is very easy just click here to get started.

6

  • All you need is a free developer account (of course if you plan on making a commercial weather app, you will need more than that).
  • After registering make sure you copy your Developer API Key, you will need it shortly.
Add Necessary Frameworks
  • Open weather.xcworkspace, you will see 2 projects, one called Pods and the one you created called weather. The Pods project contains both AFNetworking and SVProgressHUD, all we have to do to use them in weather is import the their respective header file! Isn’t that great!
  • We will also need to add a few frameworks to our target so let’s do that.

7

  • You will need to make sure all of the frameworks listed below have been added to your weather target.
    • QuartzCore
    • CoreLocation
    • MobileCoreServices
    • SystemConfiguration
    • UIKit
    • Foundation
    • CoreGraphics
  • Once those frameworks have been added, open weather-Prefix.pch (the pre-compiled header) and a few of those newly added frameworks as shown below.

#import

#ifndef __IPHONE_4_0
#warning "This project uses features only available in iOS SDK 4.0 and later."
#endif

#ifdef __OBJC__
    #import <UIKit/UIKit.h>
    #import <Foundation/Foundation.h>
    #import <SystemConfiguration/SystemConfiguration.h>
    #import <MobileCoreServices/MobileCoreServices.h>
    #import <CoreLocation/CoreLocation.h>
    #import <QuartzCore/QuartzCore.h>
#endif
  • Build your weather target, everything should build without warnings.
Setting Up An AFNetworking HTTPClient
  • First we are going to need the API key from Weather Underground in our app, so let’s create a header file with a const for that. Create a new header file named WeatherAPIKey.h.

#ifndef Weather_WeatherAPIKey_h
#define Weather_WeatherAPIKey_h

//Sign up at http://www.wunderground.com/weather/api to get a free developer key
static NSString * const kWeatherUndergroundAPIKey = @"your_api_key_goes_here";

#endif
  • Make sure you replace the default text with your API key.
  • Next, add another new class to the project called WeatherClient and tell Xcode to inherit from NSObject to create it. We are going to change the inheritance to AFHTTPClient after it has been created.
  • Open WeatherClient.h and add the following interface. Notice we changed the inheritance to AFHTTPClient and imported AFNetworking.

#import <AFNetworking/AFNetworking.h>

@interface WeatherClient : AFHTTPClient

+ (instancetype)sharedClient;

@end
  • Now open WeatherClient.m, let’s implement the singleton class method we created in the header. We are also going to import the WeatherAPIKey.h header we created so the HTTPClient can access the API key.

#import "WeatherClient.h"
#import "WeatherAPIKey.h"

static NSString * const kWeatherUndergroundAPIBaseURLString = @"http://api.wunderground.com/api/";

@implementation WeatherClient

#pragma mark - Singleton

+ (instancetype)sharedClient
{
    static WeatherClient *sharedClient = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
        NSString *baseURLString = [kWeatherUndergroundAPIBaseURLString stringByAppendingString:kWeatherUndergroundAPIKey];
        sharedClient = [[self alloc] initWithBaseURL:[NSURL URLWithString:baseURLString]];
    });

    return sharedClient;
}

#pragma mark - Initialization

- (id)initWithBaseURL:(NSURL *)url
{
    self = [super initWithBaseURL:url];
    if (self)
    {
        [self registerHTTPOperationClass:[AFJSONRequestOperation class]];
        [self setDefaultHeader:@"Accept" value:@"application/json"];
    }

    return self;
}
  • The code above implements a singleton class method that calls the AFNetworking default initializer initWithBaseURL: to create a HTTP Client. We define the Weather Underground API base URL as a constant as well. For the Base URL parameter we pass to initWithBaseURL: we append our API key to the base URL constant.
  • We then override initWithBaseURL: to set our HTTP client up for talking to JSON services.
Creating A Data Model
  • Weather Underground has documentation for all of the API they support and what responses will be given. Since this is a simple app to start with, we will only need to use their conditions API. Here is an example of how this API can be called.

http://api.wunderground.com/api/your_api_key/conditions/q/CA/San_Francisco.json
  • This would request the current conditions for San Francisco. We can also pass in raw latitude and longitude values like so.

http://api.wunderground.com/api/your_api_key/conditions/q/37.78953934,-122.40170288.json
  • Unfortunately, this is not exactly standard REST style so we won’t be able to take full advantage of the simplicity of AFNetworking’s API, but it will still be very easy.
  • Each of these calls will return a JSON result as specified in their docs. Basically the response has two top level keys, response and current_observation. The second will be of interest to us, because it contains all the weather data for the observation. Visit the conditions API reference for details and more examples.
  • Let’s create a class to model this observation data. Add a new class to the project named Observation, have it inherit from NSObject.
  • Open Observation.h add the following interface.

#import <Foundation/Foundation.h>

@interface Observation : NSObject

@property (nonatomic, strong) NSDictionary  *location;
@property (nonatomic, strong) NSDictionary  *observationLocation;
@property (nonatomic, strong) NSDictionary  *weatherUndergroundImageInfo;

@property (nonatomic, strong) NSString      *timeString;
@property (nonatomic, strong) NSString      *timeStringRFC822;
@property (nonatomic, strong) NSString      *weatherDescription;
@property (nonatomic, strong) NSString      *windDescription;
@property (nonatomic, strong) NSString      *temperatureDescription;
@property (nonatomic, strong) NSString      *feelsLikeTemperatureDescription;
@property (nonatomic, strong) NSString      *relativeHumidity;
@property (nonatomic, strong) NSString      *dewpointDescription;
@property (nonatomic, strong) NSString      *iconName;
@property (nonatomic, strong) NSString      *iconUrl;

@property (nonatomic, strong) NSNumber      *temperatureF;
@property (nonatomic, strong) NSNumber      *temperatureC;
@property (nonatomic, strong) NSNumber      *feelsLikeTemperatureF;
@property (nonatomic, strong) NSNumber      *feelsLikeTemperatureC;

+ (instancetype)observationWithDictionary:(NSDictionary *)dictionary;

@end
  • The properties are just a subset of the data that the conditions endpoint returns, but it is all we will need for now. The class method gives us a way to create an observation object from a dictionary (specifically the JSON object returned from the web service).
  • Now open Observation.m and add the following implementation

#import "Observation.h"

@implementation Observation

+ (NSDictionary *)keyMapping
{
    static NSDictionary *keyMapping = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{

        keyMapping = @{
                        @"display_location"             : @"location",
                        @"observation_location"         : @"observationLocation",
                        @"image"                        : @"weatherUndergroundImageInfo",
                        @"observation_time"             : @"timeString",
                        @"observation_time_rfc822"      : @"timeStringRFC822",
                        @"weather"                      : @"weatherDescription",
                        @"wind_string"                  : @"windDescription",
                        @"temperature_string"           : @"temperatureDescription",
                        @"feelslike_string"             : @"feelsLikeTemperatureDescription",
                        @"relative_humidity"            : @"relativeHumidity",
                        @"dewpoint_string"              : @"dewpointDescription",
                        @"icon"                         : @"iconName",
                        @"icon_url"                     : @"iconUrl",
                        @"temp_f"                       : @"temperatureF",
                        @"temp_c"                       : @"temperatureC",
                        @"feelslike_f"                  : @"feelsLikeTemperatureF",
                        @"feelslike_c"                  : @"feelsLikeTemperatureC"
                      };
    });

    return keyMapping;
}

+ (instancetype)observationWithDictionary:(NSDictionary *)dictionary
{
    Observation *observation = nil;
    if (dictionary)
    {
        observation = [[Observation alloc] init];
        NSDictionary *keyMapping = [self keyMapping];
        for (NSString *key in keyMapping)
        {
            id value = dictionary[key];
            if (value)
            {
                [observation setValue:value forKey:keyMapping[key]];
            }
        }
    }

    return observation;
}

@end
  • The first class method keyMapping maps our property names to the parameter names returned from the server. While most servers have a standard convention that would allow us to bypass this mapping, I have found the mapping to be helpful in understanding the relation between model and server. Additionally, if we use this mapping we can name our properties whatever we want!
  • The second method observationWithDictionary: takes a dictionary of key values returned from the server and uses the mapping and KVC to filter those into our properties.
Integrating The Model With Our HTTPClient
  • In our WeatherClient we need to create a method that makes the conditions request and returns one of a our Observation model objects.
  • Open WeatherClient.h and add the following method declaration. The method takes a CLLocation object and returns a block which has an Observation object and an error.

#import <AFNetworking/AFNetworking.h>
#import "Observation.h"

@interface WeatherClient : AFHTTPClient

+ (WeatherClient *)sharedClient;

- (void)getCurrentWeatherObservationForLocation:(CLLocation *)location completion:(void(^)(Observation *observation, NSError *error))completion;

@end
  • Open WeatherClient.m and add the following implementation for getCurrentWeatherObservationForLocation:completion:

- (void)getCurrentWeatherObservationForLocation:(CLLocation *)location completion:(void(^)(Observation *observation, NSError *error))completion
{
    if (location)
    {
        // We have to do this because their API is not exactly rest
        NSString *getPath = [NSString stringWithFormat:@"conditions/q/%.6f,%.6f.json", location.coordinate.latitude, location.coordinate.longitude];
        WeatherClient *client = [WeatherClient sharedClient];
        [client getPath:getPath
             parameters:nil
                success:^(AFHTTPRequestOperation *operation, id responseObject) {
                    Observation *observation = [Observation observationWithDictionary:responseObject[@"current_observation"]];
                    completion(observation, nil);
                }
                failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                    completion(nil, error);
                }
         ];
    }
    else
    {
        completion(nil, [NSError errorWithDomain:@"Invalid Location as argument" code:-1 userInfo:nil]);
    }
}
  • The method first checks for a valid location, then creates the path for the conditions endpoint as specified by Weather Underground’s API Reference. We add the latitude and longitude from the CLLocation object as parameters of the request. We cannot pass these in as parameters to the AFNetworking getPath:parameters:success:failure: because that method expects a more standard format like the following.
  • 
    
     http://website.com/api?lat=12.2&amp;lon=39
  • We then provide two blocks along with the getPath to getPath:parameters:success:failure:, one that is called if the request is successful and one if it is not. If the request is successful we create an Observation object and pass it on in our completion block, if it is not we pass nil for the observation along with the error received.
Getting A Location
  • Now that we have our WeatherClient complete, we need to get a location from the iPhone in order to use getCurrentWeatherObservationForLocation:completion:.
  • Create a new class that inherits from NSObject called LocationManager. This locationManager will help us manage using CoreLocation to retrieve the current location of the device.
  • Open LocationManager.h and add the following interface.

#import <Foundation/Foundation.h>

extern NSString * const kLocationDidChangeNotificationKey;

@interface LocationManager : NSObject

@property (nonatomic, readonly) CLLocation *currentLocation;
@property (nonatomic, readonly) BOOL       isMonitoringLocation;

+ (instancetype)sharedManager;

- (void)startMonitoringLocationChanges;
- (void)stopMonitoringLocationChanges;

@end
  • The string constant at the top defines a notification name. This will be used to signal location changes.
  • We then add a singleton class method, a couple properties, and methods for starting and stopping location monitoring.
  • Open LocationManager.m and add the following implementation.

#import "LocationManager.h"

NSString * const kLocationDidChangeNotificationKey = @"locationManagerlocationDidChange";

@interface LocationManager () <CLLocationManagerDelegate>

@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, readwrite) BOOL           isMonitoringLocation;

@end

@implementation LocationManager

+ (instancetype)sharedManager
{
    static LocationManager *_sharedLocationManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
        _sharedLocationManager = [[LocationManager alloc] init];
    });

    return _sharedLocationManager;
}

#pragma mark - Public API

- (void)startMonitoringLocationChanges
{
    if ([CLLocationManager locationServicesEnabled])
    {
        if (!self.isMonitoringLocation)
        {
            self.isMonitoringLocation = YES;
            self.locationManager.delegate = self;
            [self.locationManager startMonitoringSignificantLocationChanges];
        }
    }
    else
    {
        UIAlertView *servicesDisabledAlert = [[UIAlertView alloc] initWithTitle:@"Location Services Disabled" message:@"This app requires location services to be enabled" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [servicesDisabledAlert show];
    }
}

- (void)stopMonitoringLocationChanges
{
    if (_locationManager)
    {
        [self.locationManager stopMonitoringSignificantLocationChanges];
        self.locationManager.delegate = nil;
        self.isMonitoringLocation = NO;
        self.locationManager = nil;
    }
}

#pragma mark - Accessors

- (CLLocationManager *)locationManager
{
    if (!_locationManager)
    {
        _locationManager = [[CLLocationManager alloc] init];
    }

    return _locationManager;
}

- (CLLocation *)currentLocation
{
    return self.locationManager.location;
}

#pragma mark - CLLocationManagerDelegate

- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
    NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithCapacity:2];
    if (newLocation) {
        userInfo[@"newLocation"] = newLocation;
    }
    if (oldLocation) {
        userInfo[@"oldLocation"] = oldLocation;
    }

    [[NSNotificationCenter defaultCenter] postNotificationName:kLocationDidChangeNotificationKey
                                                        object:self
                                                      userInfo:userInfo];
}

- (void)locationManager:(CLLocationManager*)manager didFailWithError:(NSError*)error
{
    if ([error code]== kCLErrorDenied)
    {
        UIAlertView *servicesDisabledAlert = [[UIAlertView alloc] initWithTitle:@"Location Services Denied" message:@"This app requires location services to be allowed" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [servicesDisabledAlert show];
    }
}

@end
  • The first method is the class singleton, which you should now be comfortable with. The second and third, startMonitoringLocationChanges and stopMonitoringLocationChanges respectively, call off to our private CLLocationManager property (defined in the class extension at the top) to start and stop location monitoring. The start monitoring method also performs a permissions check to ensure we have been granted location monitoring authorization.
  • Next, you will see the locationManager lazy accessor which creates our CLLocationManager if it doesn’t yet exist.
  • Finally, we get to the two CLLocationManager delegate methods, the first of which locationManager:didUpdateToLocation:fromLocation: fires off a notification each time a new location is determined. The second alerts the user that the app needs location services to be allowed in the event we are denied.
  • We also need to add a few methods to AppDelegate.m in order to properly stop location monitoring.

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    [[LocationManager sharedManager] stopMonitoringLocationChanges];
}

- (void)applicationWillEnterForeground:(UIApplication *)application
{
    // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
    [[LocationManager sharedManager] startMonitoringLocationChanges];
}

- (void)applicationWillTerminate:(UIApplication *)application
{
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    [[LocationManager sharedManager] stopMonitoringLocationChanges];
}
Setting Up The Requests
  • Now that we have location information, we need to pass it to the WeatherClient in order to request weather data from Weather Underground. We will do this in a reloadData method in ViewController.m

- (void)reloadData
{
    WeatherClient *client = [WeatherClient sharedClient];
    CLLocation *location = [[LocationManager sharedManager] currentLocation];

    [SVProgressHUD showWithMaskType:SVProgressHUDMaskTypeGradient];

    __weak ViewController *weakSelf = self;
    [client getCurrentWeatherObservationForLocation:location completion:^(Observation *observation, NSError *error) {
        if (error)
        {
            NSLog(@"Web Service Error: %@", [error description]);
        }
        else
        {
            [weakSelf updateUIWithObservation:observation];
        }

        [SVProgressHUD dismiss];
    }];
}

- (void)updateUIWithObservation:(Observation *)observation
{
    // We will update our UI here...
}
  • InreloadData we first create a reference to our weather client and then get the current location from the locationManager and pass it in a call to getCurrentWeatherObservationForLocation:completion:. Once the request has finished, the completion block will be called with the retrieved Observation object. We use SVProgressHUD here to indicate background activity so you will also need to add an import for that to ViewController.m. The weakSelf prevents any retain cycles that could arise because of the block (even though it is probably unnecessary in this app). Finally, after we have the observation and there wasn’t an error we call updateUIWithObservation: which will update the user interface, then we dismiss the progress HUD.
Creating The User Interface
  • First we Need to create a view that displays a nice gradient background. Add a new UIView subclass to the project named GradientView.
  • Open GradientView.m and add the following implementation, which will draw a nice blue gradient.

#import "GradientView.h"

#define GRADIENT_COLOR_1    [[UIColor colorWithRed:80/255.0 green:148/255.0 blue:180/255.0 alpha:1.0] CGColor]
#define GRADIENT_COLOR_2    [[UIColor colorWithRed:13/255.0 green:94/255.0 blue:180/255.0 alpha:1.0] CGColor]

@implementation GradientView

+ (Class)layerClass
{
    return [CAGradientLayer class];
}

- (void)awakeFromNib
{
    CAGradientLayer *gradientLayer  = (CAGradientLayer *)self.layer;
    gradientLayer.colors            = @[(id)GRADIENT_COLOR_1,(id)GRADIENT_COLOR_2];
}

@end
  • Most of the UI will be done using ViewController.xib, but let’s add the following outlets and actions to ViewController.h first.

#import <UIKit/UIKit.h>
#import "GradientView.h"

@interface ViewController : UIViewController

@property (strong, nonatomic) IBOutlet UIView       *shadowContainerView;
@property (strong, nonatomic) IBOutlet GradientView *observationContainerView;

@property (strong, nonatomic) IBOutlet UILabel      *locationLabel;
@property (strong, nonatomic) IBOutlet UILabel      *currentTemperatureLabel;
@property (strong, nonatomic) IBOutlet UILabel      *feelsLikeTemperatureLabel;
@property (strong, nonatomic) IBOutlet UILabel      *weatherDescriptionLabel;
@property (strong, nonatomic) IBOutlet UILabel      *windDescriptionLabel;
@property (strong, nonatomic) IBOutlet UILabel      *humidityLabel;
@property (strong, nonatomic) IBOutlet UILabel      *dewpointLabel;
@property (strong, nonatomic) IBOutlet UILabel      *lastUpdatedLabel;

@property (strong, nonatomic) IBOutlet UIImageView  *currentConditionImageView;
@property (strong, nonatomic) IBOutlet UIImageView  *weatherUndergroundImageView;

- (IBAction)refresh:(id)sender;

@end
  • These will be all the properties that we will need to create the user interface.
  • Now Open ViewController.m and add the following (final implementation).

#import "ViewController.h"

#import <SVProgressHUD/SVProgressHUD.h>

#import "WeatherClient.h"
#import "LocationManager.h"

@implementation ViewController

#pragma mark - UIViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self setupViews];

    __weak ViewController *weakSelf = self;
    [[NSNotificationCenter defaultCenter] addObserverForName:kLocationDidChangeNotificationKey
                                                      object:nil
                                                       queue:[NSOperationQueue mainQueue]
                                                  usingBlock:^(NSNotification *note) {
                                                      NSLog(@"Note: %@", note);
                                                      [weakSelf reloadData];
                                                  }];

    [[LocationManager sharedManager] startMonitoringLocationChanges];
}

- (void)viewDidLayoutSubviews
{
    // Need to adjust the shadow path when the views bounds change
    self.shadowContainerView.layer.shadowPath = [[UIBezierPath bezierPathWithRoundedRect:self.shadowContainerView.bounds cornerRadius:6.0f] CGPath];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [[LocationManager sharedManager] stopMonitoringLocationChanges];
}

#pragma mark - Private

- (void)setupViews
{
    self.observationContainerView.clipsToBounds = YES;
    self.observationContainerView.layer.cornerRadius = 6.0f;
    self.observationContainerView.layer.borderColor = [[UIColor whiteColor] CGColor];
    self.observationContainerView.layer.borderWidth  = 3.0f;

    self.shadowContainerView.backgroundColor = [UIColor clearColor];
    self.shadowContainerView.layer.shadowColor = [[UIColor blackColor] CGColor];
    self.shadowContainerView.layer.shadowOffset = CGSizeZero;
    self.shadowContainerView.layer.shadowOpacity = 0.65f;
    self.shadowContainerView.layer.shadowRadius = 4.0f;
    self.shadowContainerView.hidden = YES;    
}

- (void)updateUIWithObservation:(Observation *)observation
{
    if (observation)
    {
        self.shadowContainerView.hidden = NO;

        [self.currentConditionImageView setImageWithURL:[NSURL URLWithString:observation.iconUrl]];
        [self.weatherUndergroundImageView setImageWithURL:[NSURL URLWithString:observation.weatherUndergroundImageInfo[@"url"]]];

        self.locationLabel.text = observation.location[@"full"];
        self.currentTemperatureLabel.text = observation.temperatureDescription;
        self.feelsLikeTemperatureLabel.text = [@"Feels like " stringByAppendingString:observation.feelsLikeTemperatureDescription];
        self.weatherDescriptionLabel.text = observation.weatherDescription;
        self.windDescriptionLabel.text = observation.windDescription;
        self.humidityLabel.text = observation.relativeHumidity;
        self.dewpointLabel.text = observation.dewpointDescription;
        self.lastUpdatedLabel.text = observation.timeString;
    }
    else
    {
        self.shadowContainerView.hidden = YES;
    }
}

- (void)reloadData
{
    WeatherClient *client = [WeatherClient sharedClient];
    CLLocation *location = [[LocationManager sharedManager] currentLocation];

    [SVProgressHUD showWithMaskType:SVProgressHUDMaskTypeGradient];

    __weak ViewController *weakSelf = self;
    [client getCurrentWeatherObservationForLocation:location completion:^(Observation *observation, NSError *error) {
        if (error)
        {
            NSLog(@"Web Service Error: %@", [error description]);
        }
        else
        {
            [weakSelf updateUIWithObservation:observation];
        }

        [SVProgressHUD dismiss];
    }];
}

#pragma mark - Actions

- (IBAction)refresh:(id)sender
{
    [self reloadData];
}

@end
  • This is the final implementation for ViewController.m. We added setupViews which we call from viewDidLoad , this is where we add a shadow to the shadowView and round the corners of the observationContainerView. We also set the initial hidden state that hides the content if we don’t have any weather data. Additionally, in viewDidLoad we will set ourselves as a listener to the locationManger’s location change notification (we add a block that calls reloadData).
  • We also added an implementation for viewDidLayoutSubviews which is where we set the shadowPath for the shadowView.
  • Then we added the implementation of updateUIWithObservation: This sets our user interface properties using the observation model returned from our weatherClient. Notice the easy convenience method setImageWithURL: that AFNetworking supplies us (in a category on UIImageView) for loading images asynchronously.
  • An implementation was then added for refresh: which calls reloadData.
  • Finally, A call to reloadData  was added to the CLLocationManagerDelegate method locationManager:didUpdateToLocation:fromLocation:.
  • Open ViewController.xib and add views to match the following interface build file.

9

  • Add a static label to the root view with the following text: No Data Available
  • Add a navigation Bar to the top, title it NSCookbook Weather
    • add a UIBarButtonItem in the right of this navigation bar. Have it use system item refresh and link it to your refresh: action.
  • Add a label to the top directly under the navigation bar, link it to locationLabel
  • Add a UIView in the center, link it to shadowContainerView
  • Add a subview to shadowContainerView change it’s class to GradientView make it span the entire size of it’s superview and link it to observationContainerView.
  • All of the following UIElements will be subviews of observationContainerView
    • Add a UIImageView to the upper left corner, link it to currentConditionImageView
    • Add a label to the right of the imageView, link it to currentTemperatureLabel
    • Add a label directly below that label, link it to feelsLikeTemperatureLabel
    • Add a label directly below the imageView, link it to weatherDescriptionLabel
    • Add a static label with the text Wind
    • Add a label directly below that, link it to windDescriptionLabel
    • Add a static directly below that label with the text Relative Humidity
    • Add a label to the right of that label, link it to humidityLabel
    • Add a static directly below that label with the text Dewpoint
    • Add a label to the right of that label, link it to dewpointLabel
    • Add a UIImageView to the bottom left corner, link it to weatherUndergroundImageView
    • Add a static label in the bottom left corner above the imageView with the text Powered By
    • Add a label to the right of the bottom image view, link it to lastUpdatedLabel
  • Refer to the image of interface builder above for fonts, colors, and sizes.
The Finished Application
  • Build & Run, your app should resemble the following.

8

As always comments, questions, and concerns are much appreciated!

The source code to this recipe is now available online through GitHub!

Comments

  1. Thank you!!! :)

  2. First off, thank you, this was a great tutorial.

    How come for LocationManager.h, you didn’t declare the CLLocationManagerDelegate protocol, is it not needed?

    As in,
    @interface LocationManager : NSObject

  3. Edit:
    As in,
    @interface LocationManager: NSObject

  4. Looks like I forgot to declare that LocationManager conforms to the CLLocationManagerDelegate protocol. This should be added to the class extension at the top of LocationManager.m.

    @interface LocationManager () <CLLocationManagerDelegate>

    I’ll update the recipe accordingly, thanks for catching that!

  5. I apologize for asking a newbie question: What files would I edit if I wanted to let the user specify which city to check the weather for? Right now it is always San Francisco and I want to specify Chicago or some other city.

  6. I am very grateful I came across this tutorial, a great example for implementing a weather app. Are you going to be updating this recipe any time soon with the new version of AFNetworking?

  7. David Peterson says:

    I keep getting an error ‘&onceToken’ – undeclared identifier ‘amp’
    The same error occurs when I downloaded the code from github
    Thanks for your help in advance.

Trackbacks

  1. […] Taken from: iOS Programming Recipe 15: Building A Weather Application […]

Speak Your Mind

*

css.php
Privacy Policy