Fetching and Parsing Web data with Xuni for iOS
It’s increasing common that a mobile app relies on some data which is not local to the device itself and is instead delivered via a web service. A few months ago we introduced the native versions of Xuni with a pair of webinars that demonstrated using Xuni to create a simple weather application. We chose this specific example since it represents the common case of visualizing up to the minute data obtained by a web service, and allows us to use FlexGrid and FlexChart to neatly present the data. Today, we’ll be further examining fetching web data and parsing either JSON or XML on iOS.
Fetching Web data
The first step that we’ll cover is how to actually retrieve the data for your application. Once again we'll use the OpenWeatherMap API since it is easily accessible, well documented, and able to deliver data in both JSON and XML form. There’s an abundance of information available on OpenWeatherMap’s API page. Obtaining this data in the app isn’t too difficult since Apple provides some tools for making these types of web requests. NSURLSession gives us a mechanism for making an asynchronous web request. To this end we will create a FetchWeatherData class with methods to handle obtaining JSON data and XML data. Since these are asynchronous calls we will at some point need to handle refreshing the UI elements affected by the updated data. Since we're also going to need to handle parsing the data we'll also update the UI elements once the data has finished parsing.
@interface FetchWeatherData : NSObject<NSURLConnectionDelegate>
+(void)placeJSONGetRequest:(NSString *)action withHandler:(void (^)(NSData \*data, NSURLResponse \*response, NSError *error))ourBlock;
+(void)placeXMLGetRequest:(NSString *)action withHandler:(void (^)(NSData \*data, NSURLResponse \*response, NSError *error))ourBlock;
@end
+(void)placeJSONGetRequest:(NSString *)location withHandler:(void (^)(NSData \*data, NSURLResponse \*response, NSError *error))ourBlock {
NSString *urlString = [[@"http://api.openweathermap.org/data/2.5/forecast?zip=" stringByAppendingString:location] stringByAppendingString:@"&units=imperial&APPID=a0a0e5e6d1ded0c79e853990c86f957b"];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:ourBlock] resume];
}
+(void)placeXMLGetRequest:(NSString *)location withHandler:(void (^)(NSData \*data, NSURLResponse \*response, NSError *error))ourBlock {
NSString *urlString = [[@"http://api.openweathermap.org/data/2.5/forecast?zip=" stringByAppendingString:location] stringByAppendingString:@"&units=imperial&mode=xml&APPID=a0a0e5e6d1ded0c79e853990c86f957b"];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:ourBlock] resume];
}
@end
The Data Model
The model for our weather data just needs to capture the data we’re interested in. To this end we will only include properties for the date, weather description (which alerts us to the expected conditions), high temp, low temp, humidity, and air pressure. We can also create our own init method for object creation with all of the properties in place.
@interface WeatherData : NSObject
@property NSDate *date;
@property NSString *weatherDescription;
@property NSNumber *highTemp;
@property NSNumber *lowTemp;
@property NSNumber *humidity;
@property NSNumber *pressure;
-(id)initWithDate:(NSDate *)date weatherDescription:(NSString *) weatherDescription highTemp:(NSNumber *)highTemp lowTemp:(NSNumber *)lowTemp humidity:(NSNumber *)humidity pressure:(NSNumber *)pressure;
@end
@implementation WeatherData
-(id)initWithDate:(NSDate *)date weatherDescription:(NSString *) weatherDescription highTemp:(NSNumber *)highTemp lowTemp:(NSNumber *)lowTemp humidity:(NSNumber *)humidity pressure:(NSNumber *)pressure{
self = [super init];
if(self){
_date = date;
_weatherDescription = weatherDescription;
_highTemp = highTemp;
_lowTemp = lowTemp;
_humidity = humidity;
_pressure = pressure;
}
return self;
}
@end
Parsing JSON
We've examined this a little bit previously in our iOS webinar, but parsing JSON isn’t too difficult on iOS. Once we’ve retrieve our NSData object from our web request we can use NSJSONSerialization to organize the data into an NSDIctionary representing all of the data we’ve retrieve. We’ll have to further iterate through the data since it isn’t a completely flat structure (in other words we have other dictionaries nested inside this object). Note that I'm also passing the FlexGrid control in as a parameter since we'll need to set the itemssource after all of the data has finished loading and been parsed. We can accomplish this by using Grand Central Dispatch (GCD) to update the itemssource on the main thread once parsing has completed.
#import <Foundation/Foundation.h>
#import <XuniFlexGridKit/XuniFlexGridKit.h>
@interface JSONParser : NSObject
@property NSMutableArray *array;
-(id)initWithArray: (NSMutableArray *) array;
-(void)parseDataFor:(FlexGrid *) grid;
@end
@implementation JSONParser
-(id)initWithArray: (NSMutableArray *) array{
self = [super init];
if (self) {
self.array = array;
}
return self;
}
-(void)parseDataFor:(FlexGrid *) grid{
[FetchWeatherData placeJSONGetRequest:@"15232" withHandler:^(NSData \*data, NSURLResponse \*response, NSError *error) {
NSMutableDictionary *fullData = [ NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
NSArray* forecastList = fullData[@"list"];
NSDictionary *temp = [[NSDictionary alloc]init];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
for (NSDictionary* forecastDictionary in forecastList) {
WeatherData *forecastData = [[WeatherData alloc] init];
NSArray *weather = forecastDictionary[@"weather"];
temp = forecastDictionary[@"main"];
forecastData.date = [dateFormatter dateFromString:(NSString *)forecastDictionary[@"dt_txt"]];
forecastData.highTemp = temp[@"temp_max"];
forecastData.lowTemp = temp[@"temp_min"];
forecastData.humidity = temp[@"humidity"];
forecastData.pressure = temp[@"pressure"];
for (NSDictionary* weatherDictionary in weather) {
forecastData.weatherDescription = weatherDictionary[@"description"];
}
[self.array addObject:forecastData];
}
dispatch\_async(dispatch\_get\_main\_queue(), ^{
grid.itemsSource = self.array;
});
}];
}
@end
Parsing XML
Parsing XML can be a little bit more complicated since it involves using NSXMLParserDelegate implementing its protocol. This means that we will have to add some code to handle these parser functions. Largely we’re interested in capturing each starting element, identifying which node that element corresponds to and capturing the property within that node, and finally identifying when we’ve reached the ending element for each weather forecast at a given time. Thus we’re looking for each element beginning with the time node. When we match an element that we're interested in we can look through the associated attributes and grab the correct ones. The last element we're interested in is the humidity so we can save push all of the current values into our array once we've captured the humidity value attribute. Once again we will use GCD to set the itemssource for our chart on the main thread so that the UI updates correctly.
#import <Foundation/Foundation.h>
#import <XuniFlexChartKit/XuniFlexChartKit.h>
@interface XMLParser : NSObject<NSXMLParserDelegate>
@property NSString *element;
@property NSDictionary *attributes;
@property NSDateFormatter *dateFormatter;
@property NSXMLParser *parser;
@property NSMutableArray *array;
@property NSDate *currentDate;
@property NSString *currentWeatherDescription;
@property NSNumber *currentHighTemp;
@property NSNumber *currentLowTemp;
@property NSNumber *currentHumidity;
@property NSNumber *currentPressure;
-(id)initWithArray: (NSMutableArray *) array;
-(void)parseDataFor:(FlexChart *) chart;
@end
@implementation XMLParser
-(id)initWithArray: (NSMutableArray *) array{
self = [super init];
if (self) {
self.array = array;
}
return self;
}
-(void) parseDataFor:(FlexChart *) chart{
[FetchWeatherData placeXMLGetRequest:@"15232" withHandler:^(NSData \*data, NSURLResponse \*response, NSError *error) {
[self setDateFormat];
self.parser = [[NSXMLParser alloc] initWithData:data];
self.parser.delegate = self;
[self.parser parse];
dispatch\_async(dispatch\_get\_main\_queue(), ^{
chart.itemsSource = self.array;
});
}];
}
-(void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary<NSString *,NSString *> *)attributeDict{
self.element = elementName;
self.attributes = attributeDict;
if ([self.element isEqual: @"time"]) {
self.currentDate = [self.dateFormatter dateFromString:(NSString *)self.attributes[@"from"]];
}
else if ([self.element isEqual: @"symbol"]) {
self.currentWeatherDescription = self.attributes[@"name"];
}
else if ([self.element isEqual: @"temperature"]) {
self.currentHighTemp = self.attributes[@"max"];
self.currentLowTemp = self.attributes[@"min"];
}
else if ([self.element isEqual: @"pressure"]) {
self.currentPressure = self.attributes[@"value"];
}
else if ([self.element isEqual: @"humidity"]) {
self.currentHumidity = self.attributes[@"value"];
WeatherData *forecast = [[WeatherData alloc] initWithDate:self.currentDate weatherDescription:self.currentWeatherDescription highTemp:self.currentHighTemp lowTemp:self.currentLowTemp humidity:self.currentHumidity pressure:self.currentPressure];
[self.array addObject:forecast];
}
}
-(void)setDateFormat {
self.dateFormatter = [[NSDateFormatter alloc] init];
[self.dateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss"];
}
-(void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
if ([self.element isEqual: @"time"]) {
self.currentDate = [self.dateFormatter dateFromString:(NSString *)self.attributes[@"from"]];
}
}
-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName{
self.element = nil;
}
@end
Using the Parsers in the ViewController
We can now configure our controls in the viewDidLoad method as normal. All of the hard work is finished, and we can eaily get our parsed data by using the following code:
jsonArray = [[NSMutableArray alloc] init];
JSONParser *json = [[JSONParser alloc] initWithArray:jsonArray];
[json parseDataFor:self.grid];
xmlArray = [[NSMutableArray alloc] init];
XMLParser *xmlParse = [[XMLParser alloc] initWithArray:xmlArray];
[xmlParse parseDataFor: self.chart];
Summing Things Up
This covers a couple of basic mechanisms for working with web data, and hopefully this eases the process if you're new to iOS development. Once the data is parsed it's quite easy to work with it using the Xuni controls, but it can help to have a good starting point.