Categories
Uncategorized

iOS Fortnightly Tutorial: A Simple Weather App, Part 1

simpleweather running 4

Welcome to the second installment of iOS Fortnightly, Global Nerdy’s iOS programming tutorial series! If you’ve got a little experience with an object-oriented programming languages, whether it’s client-side JavaScript, server-side Perl, PHP, Python, or Ruby, or desktop Java, C#, or Visual Basic, this series is for you. Every fortnight — that’s a not-so-common word for “two weeks”, I’ll post a tutorial showing you how to write a simple but useful app that you can use as the launching point for your own iOS development projects. The idea behind iOS Fortnightly is to help you become familiar with mobile app development, the Objective-C programming language, the Cocoa Touch programming framework, and iOS application design.

ios fortnightly tutorials

In case you missed the previous iOS Fortnightly installment, go check it out. It’s a simple “Magic 8-Ball” app, but it’s the perfect starter app for someone who’s new to iOS development.

With the introduction out of the way, let’s move on to this fortnight’s app, a simple weather app. Let me start with what I think will be an iOS Fortnightly tradition — the video:

Question 1: Where Will the Weather Data Come From?

open weather map

I decided to go with Open Weather Map, a free-as-in-beer/free-as-in-speech weather data and forecast API in the spirit of Wikipedia and OpenStreetMap. While they recommend the use of a key to access the API (also free), it’s not absolutely necessary, which means that you can get started writing a weather app without having to sign up for an API key and waiting for approval. With a simple HTTP GET call, you can use it to get the current weather for a location that you can specify by:

  • City name,
  • Latitude and longitude, or
  • Numeric city ID

Here’s a sample call to Open Weather Map’s API for the current weather in my current location, Toronto, Canada:

http://api.openweathermap.org/data/2.5/weather?q=Toronto,CA

Here’s the resulting JSON object that came back when I wrote this article. Your results may vary, seeing as you’ll be making the call at a different day and time, but the format will be the same. It comes out as one long, continuous string; I’ve reformatted it so that it’s easier to read:

{"coord":{"lon":-79.416298,
          "lat":43.700111},
"sys":{"country":"CA",
       "sunrise":1378032138,
       "sunset":1378079546},
"weather":[{"id":701,
            "main":"Mist",
            "description":"mist",
            "icon":"50n"}],
"base":"gdps stations",
"main":{"temp":294.11,
        "humidity":93,
        "pressure":1009,
        "temp_min":292.59,
        "temp_max":295.15},
"wind":{"speed":1.3,
        "deg":187.501},
"rain":{"3h":0},
"clouds":{"all":0},
"dt":1378007487,
"id":6167865,
"name":"Toronto",
"cod":200}

Here’s a run-down of the returned object’s keys and values:

Element Description
cod The HTTP status code.
clouds The cloudiness, expressed as an object with a single key:

  • all: The cloud cover, expressed as a percentage (0 – 100).
coord The coordinates of the location for the weather report, expressed as an object with two keys:

  • lat: The location’s latitude, expressed in degrees (- is west, + is east)
  • lon: The location’s longitude, expressed in degrees (- is south, + is north)
dt Time the weather report was sent in the GMT time zone, expressed in Unix time.
main The quantitative weather, expressed as an object with these keys:

  • humidity: The humidity, expressed as a percentage (0 – 100).
  • pressure: The air pressure, in hectopascals (hundreds of pascals; 1 hectopascal is equal to one millibar).
  • temp: The current temperature, in degrees kelvin (1 degree kelvin = 1 degree celsius, 0 degrees C = 273.15 K).
  • temp_max: The predicted maximum temperature, in degrees kelvin.
  • temp_min: The predicted minimum temperature, in degrees kelvin.
name The name of the town or city closest to the location for the weather report.
rain The amount of rain expected to fall, expressed an object with this key:

  • 3h: Expected rainfall over the next 3 hours, in millimetres.
snow The amount of snow expected to fall, expressed as an object with this key:

  • 3h: Expected snowfall over the next 3 hours, in millimetres.
sys Other time and location data, expressed as an object with these keys:

  • country: The country in which the location for the weather report is located, expressed as an ISO two-letter country code.
  • sunrise: The time when sunrise will occur at the location for the weather report, expressed in Unix time.
  • sunset: The time when sunset will occur at the location for the weather report, expressed in Unix time.
weather The qualitative weather, expressed as an array containing one or more objects with the following keys:

  • description: A plain English description of the weather.
  • icon: The filename of the icon corresponding to the current weather. The full URL of the icon is http://openweathermap.org/img/w/filename.png.
  • id: The weather condition code, expressed as a three-digit number. See Open Weather Map’s “weather condition codes” page for an explanation of the codes.
wind The wind, expressed as an object with the following keys:

  • deg: Wind direction in meteorological degrees (0 is a north wind — that is, coming from the north, 90 is east, 180 is south, 270 is west).
  • speed: Wind speed in metres per second.

Question 2: How Will Open Weather Map’s Data be Accessed?

afnetworkingApple’s Cocoa Touch framework has its own class for downloading the contents of an URL — NSURLConnection — but we’ll use a third-party library that’s even simpler to use: AFNetworking, which describes itself as “a delightful networking framework for iOS and OSX” and is used at fine institutions such as Github, Heroku, and Pinterest.

Question 3: How Do We Include AFNetworking in an iOS Project?

cocoapodsAFNetworking can be manually included in a project, but I thought this would be a good opportunity to show the use of CocoaPods, the easy Ruby-based library dependency manager.

To install Cocoapods, fire up Terminal and type in the following lines:

$ sudo gem install cocoapods
$ pod setup

Create the Project

As with the previous project, we start with From the menu bar, choose File → New → Project…. You’ll see this:

single view application

Once again, this application will happen on a single screen, so we’ll go with a single-view application:

  • In the left column, under iOS, make sure that Application is selected.
  • In the big area on the right, select Single View Application.

Click Next to proceed to the next step:

choose options for project

On the “Choose options” screen…

  • Give your app a name by entering one into the Product Name field. I’m going with SimpleWeather.
  • Make sure that in the Devices menu, iPhone is selected.
  • Check the Use Storyboards checkbox.
  • Make sure that the Use Automatic Reference Counting checkbox is checked.

Click Next to proceed to the next step:

save project

Do this:

  • Choose the directory where your project will be saved. Your project will be saved in a directory inside the directory you chose.
  • Check the Create local git repository for this project checkbox. Git’s a good habit to get into, and there’ll come a time when being able to backtrack to a previous version of your code will save your bacon.
  • Make sure that the selection in the Add to: menu is Don’t add to any project or workspace.
  • Click the Create button.

So far, this isn’t all that different from the setup for the previous project. Here’s where it’s different — first, close the project. That’s right; close the project. We’re going to set up Cocoapods to bring AFNetworking into the project. Open the text editor of your choice and enter the following:

platform :ios, '6.0'
pod 'AFNetworking', '~> 1.3.2'

Save the file in your project directory. Double-check that it’s in the right place:

$ pwd
/Users/joey/Developer/iOS/Demo Apps/SimpleWeather
$ ls
Podfile			SimpleWeather		SimpleWeather.xcodeproj
$ cat Podfile
platform :ios, '6.0'
pod 'AFNetworking', '~> 1.3.2'

Once you’ve confirmed that the files are in the right place, install AFNetworking by entering pod install at the command line while in your project’s directory:

$ pod install
Analyzing dependencies
Downloading dependencies
Installing AFNetworking (1.3.2)
Generating Pods project
Integrating client project

[!] From now on use `SimpleWeather.xcworkspace`.

As the command-line output states, you should now open the project using SimpleWeather.xcworkspace, the full workspace file:

simpleweather.xcworkspace

Open the project by double-clicking on SimpleWeather.xcworkspace:

projects

If you look at the Project Navigator on the left side of Xcode’s window, you’ll see that the workspace is made up of two projects:

  • SimpleWeather, our weather app project, where we’ll be doing our work, and
  • Pods, which contains the AFNetworking code. We won’t touch it, but we’ll use its functionality to grab the weather info from Open Weather Map.

(If you want to find out more about workspaces, see this section of Apple’s documentation.)

Next step: drawing a quick user interface.

Draw the User Interface

In the Project Navigator on the left side of the Xcode window, select MainStoryboard.storyboard. Once again, we’re not going to bother with autolayout — we’re not quite at the fancy UI phase yet — so select the File Inspector on the right side of the window, and uncheck the Autolayout box:

screen 1

Click the screen capture to see it at full size.

Now to draw some controls on the view. In the Object Library, select Controls from the drop-down menu to narrow down the selection in the Object Library to just the UI controls. Drag the following from the Object Library onto the view:

  • A Label to a spot to near the top of the screen. Double-click it to edit its text, and change it to Where’s the weather in:.
  • A Text Field to just below the label, and stretch it to nearly the full width of the view.
  • A Round Rect Button to just below the text field. Double-click it to edit its text, and change it to Tell Me.

screen 2

Click the screen capture to see it at full size.

Connect the Controls to Code

First, let’s create an action for the Tell Me button:

  • Select the Assistant Editor so that both the storyboard and view controller header code in ViewController.h are both on display.
  • Select the Tell Me button, and then either right-click or control-click it. A grey window will pop up beside it, which will display actions and outlets.
  • Drag from the circle beside Touch Up Inside into the view controller header code, anywhere between @interface and @end.

screen 3

Click the screen capture to see it at full size.

In the little window that pops up:

  • Give the action a name. I used tellMeButtonPressed.
  • Make sure that the Event is Touch Up Inside.
  • Click the Connect button.

tellmebuttonpressed

You should now have the following line in the view controller header:

- (IBAction)tellMeButtonPressed:(id)sender;

First, let’s create an outlet for the text field:

  • Select the text field, and as we did with the button, right-click or control-click it. As with the button, a grey window will pop up beside it, which will display actions and outlets.
  • Drag from the circle beside New Referencing Outlet into the view controller header code, anywhere between @interface and @end.

screen 4

Click the screen capture to see it at full size.

locationtextField

In the little window that pops up:

  • Give the outlet a name. I used locationTextField.
  • Make sure that the Type is UITextField and that the Storage is Weak.
  • Click the Connect button.

You should now have the following line in the view controller header:

- (IBAction)tellMeButtonPressed:(id)sender;

With the action and outlet code inserted into the header, I did a little rearranging so that it looks like this:

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

// Outlets
// =======
@property (weak, nonatomic) IBOutlet UITextField *locationTextField;

// Actions
// =======
- (IBAction)tellMeButtonPressed:(id)sender;

@end

Commit the Changes

Now’s a good time to commit the changes to git. Choose File → Source Control → Commit… from the menu bar. The “diffs” window will appear, which lets you see the changes you’re about to commit:

commit 1

Click the screen capture to see it at full size.

Enter a commit message — something like “Added controls and their actions and outlets”, then click the Commit 4 Files button to commit the changes.

Create a Model Class for the Weather Data

As I mentioned in the last article in the series, iOS apps are built on the Model-View-Controller pattern. Here’s how we’ll apply the pattern in this app:

simpleweather mvc

We’ve already got a view and view controller from the project setup work we’ve done — let’s create a model class. From the menu bar, File → New → File…. You’ll be presented with this dialog box:

Select Cocoa Touch from the sidebar on the left, then select Objective-C class, then click Next. You’ll see this:

create weather

Provide a name for the class. I gave mine the name Weather and made it a subclass of NSObject, the ultimate base class for every class in Objective-C. Once that’s done, click Next. You’ll be taken to the dialog box to save the class:

save class

Click Create to save the new class files. You can see them in the Project Navigator, marked with the letter A, denoting files that have been added since the last commit:

weather.h and weather.m

Now that we’ve got files for the Weather class’ interface (the header file, Weather.h) and implementation (the module file, Weather.m), let’s code! Let’s take care of the header first, where we specify the properties and methods that our model exposes:

Weather.h

#import <Foundation/Foundation.h>

@interface Weather : NSObject

// Properties
// ==========

// Place and time
@property (nonatomic, copy, readonly) NSString *city;
@property (nonatomic, copy, readonly) NSString *country;
@property (nonatomic, readonly) CGFloat latitude;
@property (nonatomic, readonly) CGFloat longitude;
@property (nonatomic, copy, readonly) NSDate *reportTime;
@property (nonatomic, copy, readonly) NSDate *sunrise;
@property (nonatomic, copy, readonly) NSDate *sunset;

// Qualitative
@property (nonatomic, copy, readonly) NSArray *conditions;

// Quantitative
@property (nonatomic, readonly) NSInteger cloudCover;
@property (nonatomic, readonly) NSInteger humidity;
@property (nonatomic, readonly) NSInteger pressure;
@property (nonatomic, readonly) NSInteger rain3hours;
@property (nonatomic, readonly) NSInteger snow3hours;
@property (nonatomic, readonly) CGFloat tempCurrent;
@property (nonatomic, readonly) CGFloat tempMin;
@property (nonatomic, readonly) CGFloat tempMax;
@property (nonatomic, readonly) NSInteger windDirection;
@property (nonatomic, readonly) CGFloat windSpeed;

// Methods
// =======

- (void)getCurrent:(NSString *)query;

@end

Note that what the model exposes is mostly properties and a single method, getCurrent. The idea is to call getCurrent with a query term, which can be a city name, a latitude/longitude pair, or a numeric city code. The model makes a call to Open Weather Maps using the query term, and uses the result to populate the model’s properties, which are then access by the view controller.

I’ll cover what all the @property stuff means in an article to follow.

Now that we have an interface, let’s code up the implementation:

Weather.m

#import "Weather.h"
#import "AFNetworking.h"

@implementation Weather {
    NSDictionary *weatherServiceResponse;
}

- (id)init
{
    self = [super init];

    weatherServiceResponse = @{};

    return self;
}

- (void)getCurrent:(NSString *)query
{
    NSString *const BASE_URL_STRING = @"http://api.openweathermap.org/data/2.5/weather";

    NSString *weatherURLText = [NSString stringWithFormat:@"%@?q=%@",
                                BASE_URL_STRING, query];
    NSURL *weatherURL = [NSURL URLWithString:weatherURLText];
    NSURLRequest *weatherRequest = [NSURLRequest requestWithURL:weatherURL];

    AFJSONRequestOperation *operation =
    [AFJSONRequestOperation JSONRequestOperationWithRequest:weatherRequest
                                                    success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
                                                        weatherServiceResponse = (NSDictionary *)JSON;
                                                        [self parseWeatherServiceResponse];
                                                    }
                                                    failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
                                                        weatherServiceResponse = @{};
                                                    }
     ];

    [operation start];
}

- (void)parseWeatherServiceResponse
{
    // clouds
    _cloudCover = [weatherServiceResponse[@"clouds"][@"all"] integerValue];

    // coord
    _latitude = [weatherServiceResponse[@"coord"][@"lat"] doubleValue];
    _longitude = [weatherServiceResponse[@"coord"][@"lon"] doubleValue];

    // dt
    _reportTime = [NSDate dateWithTimeIntervalSince1970:[weatherServiceResponse[@"dt"] doubleValue]];

    // main
    _humidity = [weatherServiceResponse[@"main"][@"humidity"] integerValue];
    _pressure = [weatherServiceResponse[@"main"][@"pressure"] integerValue];
    _tempCurrent = [Weather kelvinToCelsius:[weatherServiceResponse[@"main"][@"temp"] doubleValue]];
    _tempMin = [Weather kelvinToCelsius:[weatherServiceResponse[@"main"][@"temp_min"] doubleValue]];
    _tempMax = [Weather kelvinToCelsius:[weatherServiceResponse[@"main"][@"temp_max"] doubleValue]];

    // name
    _city = weatherServiceResponse[@"name"];

    // rain
    _rain3hours = [weatherServiceResponse[@"rain"][@"3h"] integerValue];

    // snow
    _snow3hours = [weatherServiceResponse[@"snow"][@"3h"] integerValue];

    // sys
    _country = weatherServiceResponse[@"sys"][@"country"];
    _sunrise = [NSDate dateWithTimeIntervalSince1970:[weatherServiceResponse[@"sys"][@"sunrise"] doubleValue]];
    _sunset = [NSDate dateWithTimeIntervalSince1970:[weatherServiceResponse[@"sys"][@"sunset"] doubleValue]];

    // weather
    _conditions = weatherServiceResponse[@"weather"];

    // wind
    _windDirection = [weatherServiceResponse[@"wind"][@"dir"] integerValue];
    _windSpeed = [weatherServiceResponse[@"wind"][@"speed"] doubleValue];
}

+ (double)kelvinToCelsius:(double)degreesKelvin
{
    const double ZERO_CELSIUS_IN_KELVIN = 273.15;
    return degreesKelvin - ZERO_CELSIUS_IN_KELVIN;
}

@end

As with the .h file, I’ll explain what’s in the .m file in an article to follow.

Connect the Model to the View Controller

We’ve got a functioning model, and a view controller connected to a view. It’s now time to make the final MVC connection: the one that links the model to the view controller. Change the code in ViewController.m to:

ViewContoller.m

#import "ViewController.h"
#import "Weather.h"

@interface ViewController ()

@end

@implementation ViewController {
    Weather *theWeather;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    theWeather = [[Weather alloc] init];
}

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

- (IBAction)tellMeButtonPressed:(id)sender
{
    [theWeather getCurrent:self.locationTextField.text];

    NSString *report = [NSString stringWithFormat:
                        @"Weather in %@:\n"
                        @"%@\n"
                        @"Current temp.: %2.1f C\n"
                        @"High: %2.1f C\n"
                        @"Low: %2.1f C\n",
                        theWeather.city,
                        theWeather.conditions[0][@"description"],
                        theWeather.tempCurrent,
                        theWeather.tempMax,
                        theWeather.tempMin
                        ];
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Current Weather"
                                                    message:report
                                                   delegate:nil
                                          cancelButtonTitle:@"OK"
                                          otherButtonTitles:nil];
    [alert show];
}

@end

I’ll explain more about this code in a later article.

Run the App

We’ve got all the code we need for a working weather app. It won’t be slick, but it will work. When you run the app, you should see this:

simpleweather running 1

Enter the name of a city into the text field. In my case, I used my own city, Toronto, then tapped the Tell Me button:

simpleweather running 2

The first time you do this, you’ll most likely see a result like the one below. Don’t let it bother you for the time being:

simpleweather running 3

Don’t worry about this for now. I’ll explain why this happens in a follow-up article.

Dismiss the alert box by tapping the OK button, then tap the Tell Me button again. You should see some better results this time:

simpleweather running 4

That’s It for Now

Congratulations! You now have a working weather app. I’ll post a few follow-up articles featuring tweaks to the app, as well as explanations of the code behind it. In the meantime, commit your changes, and more importantly, play around with the code. Experiment. Tweak. Look up anything that seems odd, unfamilair or interesting!

If you have any questions or comments, or got stuck or encountered an error while doing this tutorial, let me know — either in the comments or by dropping me a line!

13 replies on “iOS Fortnightly Tutorial: A Simple Weather App, Part 1”

(1)
Let me suggest that using AFJSONRequestOperation isn’t necessarily the best way to go in terms of long term sustainability (I got burned on the ASI library); and that the “normal” iOS calls are not much more difficult to use.

e.g.. …
[NSURLConnection sendAsynchronousRequest:request queue:self.queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
… stuff …
id jobject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];

(2)

Having also gotten burned on API changes crashing deployed apps, @try blocks are handy around all the result parsing code.

David: If I keep the AFNetworking project as-is in my workspace and don’t update it, shouldn’t the code should continue to work until either Open Weather Maps’ API changes or disappears?

As for NSURLConnection: yeah, it’s not that much more difficult to use. I just happen to have a preference for AFNetworking, simply because I learned it first. I will cover using NSURLConnection to be complete and because it’s “baked in”. I’ll also cover XML parsing.

As for exceptions, I’m covering them soon!

Sorry I got this error. Could you please tell me how shoud I fix it?
————————
Ld /Users/lephuocdai/Library/Developer/Xcode/DerivedData/SimpleWeather-alqzpxyjtlmwivelbyzyrirvseug/Build/Products/Debug-iphonesimulator/SimpleWeather.app/SimpleWeather normal i386
cd /Users/lephuocdai/Documents/Courses/iOSDev/SimpleWeather
setenv IPHONEOS_DEPLOYMENT_TARGET 6.1
setenv PATH “/Applications/Xcode4-6-3.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin:/Applications/Xcode4-6-3.app/Contents/Developer/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin”
/Applications/Xcode4-6-3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -arch i386 -isysroot /Applications/Xcode4-6-3.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator6.1.sdk -L/Users/lephuocdai/Library/Developer/Xcode/DerivedData/SimpleWeather-alqzpxyjtlmwivelbyzyrirvseug/Build/Products/Debug-iphonesimulator -F/Users/lephuocdai/Library/Developer/Xcode/DerivedData/SimpleWeather-alqzpxyjtlmwivelbyzyrirvseug/Build/Products/Debug-iphonesimulator -filelist /Users/lephuocdai/Library/Developer/Xcode/DerivedData/SimpleWeather-alqzpxyjtlmwivelbyzyrirvseug/Build/Intermediates/SimpleWeather.build/Debug-iphonesimulator/SimpleWeather.build/Objects-normal/i386/SimpleWeather.LinkFileList -Xlinker -objc_abi_version -Xlinker 2 -ObjC -framework CoreGraphics -framework MobileCoreServices -framework Security -framework SystemConfiguration -fobjc-arc -fobjc-link-runtime -Xlinker -no_implicit_dylibs -mios-simulator-version-min=6.1 -framework SystemConfiguration -framework MobileCoreServices -framework UIKit -framework Foundation -framework CoreGraphics -lPods -o /Users/lephuocdai/Library/Developer/Xcode/DerivedData/SimpleWeather-alqzpxyjtlmwivelbyzyrirvseug/Build/Products/Debug-iphonesimulator/SimpleWeather.app/SimpleWeather
————————
ld: library not found for -lPods
clang: error: linker command failed with exit code 1 (use -v to see invocation)
————————

hi
when i run pod install it says “no podfile is found in currents working directory”
even i am in my project directory , and i also created pod file…
u will show better how to create pod file . how to save it….

Thanks

Thanks for the tutorial it was really helpful. Could you explain why the first time the “Tell Me” button is pressed the returned results are null.

Thanks, I look forward to more of your tutorials.
Glenn

Thanks for the tutorial. I can’t find the way to fix the problem you mention above “The first time you do this, you’ll most likely see a result like the one below. ” Have already posted a solution? I am dealing with that with no success.
Thanks!

How does the code in Weather.m change to accommodate the AFNetworking 2.0 upgrade?

Comments are closed.