An open source weather app that calculates UV light intensity for the user's current time and location and gives a suggestion for how long it will take to make vitamin D. It also forecasts UV light intensities for the coming week. The code is up on Github.
Background:
This project is based on a tutorial from Ray Wenderlich's great website. (sample app: SimpleWeather). It adds Ozone data from the TEMIS database, using libxml2
and Hpple
to parse the .html
based on another tutorial how to parse HTML on iOS. It also includes my astronomy code from previous projects for calculating solar angles and estimating UV intensities.
Technologies
The iOS best practices tutorial included a number of interesting technologies. I've ignored the image blur filter they use in the main view, but I keep these:
- cocoapods -- package manager and dependency tracker software. Basic usage:
> edit podfile
cocoapods searches for a file named 'podfile'. Include the dependencies to be included with the project there (see example in the directory)> pod install
obtains the dependencies, creates a directorypods/
to hold the dependencies. It also creates a.xcworkspace
file to hold the new project with dependencies. Use this workspace in xcode.> pod update
will update a dependency to the latest versions. I had to do this in this project because the TSMessage project had suffered from a problem at GitHub and they needed to put in a bug fix which broke my older version. Cocoapods to the rescue!
- Mantle -- a project from GitHub for creating data models: aids conversions between JSON <--> NSObject (very handy with a json data feed, as we'll see!). I only wish there were more useful tools for directly converting a dictionary into an object. I didn't come up with a good solution for the ozone data, but at least it works, and it's pretty easy to see how it could be improved.
- ReactiveCocoa -- allows you to use functional programming constructions in iOS apps. Ever run into a spaghetti of callbacks with KVO? Functional Reactive programming may not be the ultimate solution, but it certainly provides a different paradigm that applies to many common situations. Wow. If you haven't used it you, you gotta try this stuff.
- hpple I needed an HTML parser for iOS. There are a lot of choices, but this one had a decent tutorial at Ray Wenderlich.com, and seemed easy to use (and it was). The parsing problem is pretty small as I only want to do a single page that hasn't changed in years. Regex would have worked, but I wanted to learn how to do it better.
- TSMessages -- a ticker-style alert message system.
- git for version control -- the technology driving collaborative development on github. Can't say I've mastered the learning curve yet. This is one of those situations where a video explanation really helps. Jessica Kerr (@jessitron) does a great one, git happens -- with sticky notes! (this particular presentation is for coders coming from a background in subversion, but I've also seen Jessica do a great intro for novices who haven't heard of version control at all. Just search for git happens.
As with any newly hatched project, there's the question of what to add to .gitignore
. For a project using cocoapods
, see the pros and cons at guides.cocoapods.org. For this project, I want to keep it lightweight, but also keep track of the dependencies. To do this, I'll add the pods/
directory to .gitignore
, but keep the podfile
, podfile.lock
and other files under version control. I used a stackoverflow post to get an appropriate .gitignore
file.
TEMIS
The ozone data I'm using is basically scraping a website. It is old website, by the Tropospheric Emission Monitoring Internet Service (TEMIS) and lacks a friendly api. Raw science. To get the column ozone for a location, I need to query the website with a url string of the type http://www.temis.nl/uvradiation/nrt/uvindex.php?lon=5.18&lat=52.1
, where the lon
and lat
values are provided by my code. The response is only available as .html
, so I need to get this response and parse it to extract the desired column ozone values. I'll use hpple
to parse the html response and it's xpath query system to walk the DOM and extract values from the relevant table.
The entire page is formated as a series of nested tables. The page header is one table; a second table holds the body of the page with one column holding the frame to the left, a blank column, and a column holding the data table I'm interested in.
html -> body -> 2nd table -> tbody -> tr -> 3rd td -> dl ->
dd -> table ->tbody ->
tr -> td -> <h2> location </h2>
tr -> 3x td -> (headers tagged <i>) Date, UV index, ozone
tr -> 3x td -> (data values) day Month year, .1f, .1f DU
There are a lot of XML parsers and JSON parsers available. In my perfect world, .html
files could be parsed as xml
, but it doesn't work out that way. Many normal tags in .html
are not xml compliant, so most xml parsers break down right away, inclduing the NSXMLParser
included in iOS. Parsing .html
is not an uncommon problem, so there are a number of librarires on GitHub that people have used. I was able to combine the Hpple
parser library with reactive cocoa to create a pipeline straight from the .html
response to my model objects.
There are still a few wrinkles that could use ironing. When dealing with locatioins time zones and solar data, times and dates become difficult to handle. The native date-handling in iOS doesn't make it easier. In particular, the UV Index published by TEMIS is for solar noon at the lat,lon of the location. There is no accurate way to use only iOS internals to capture this date correctly, particularly when daylight savings time is taken into account in different jurisdictions. Astronomy calculations are needed to assign these values to correct times.
Lessons
DateFormats:
'HH' parses 24hr time, hh parses am, pm time. 'hh' won't parse 17:20.
'YYYY' doesn't mean 2014. For normal years you need 'yyyy'.
The full spec for iOS 7 is at unicode.org
ReactiveCocoa:
NSLog
is useful for getting info on intermediate stages.
There are some mysteries about what filter:
and ignore:
do that I should get a grip on.
There is a lot of potential in this library, and many functions to explore. map:
is your friend. Use it flexibly.
There are many useful discussions in the issues on GitHub and on SO. One word of warning: this library is changing rapidly -- 2.0 was recently released and 3.0 is being crafted. The terminology is shifting, even for core ideas. Older posts may contain outdated code, and that's likely to change even faster with Apple's introduction of Swift!
- prefer
RACSignal
overRACSequence
. - Prefer 1-way binding with RAC over 2-way binding. (see discussion of issue proposign to drop RACChannel)
- avoid
subscribeNext:
,doError:
, anddoNext:
. These break the functional paradigm. See this StackOverflow question on fetching a collection Note:RACAble
was replaced withRACObserve
in 2.0.
I'm finding it somewhat difficlt to chain my signals together in the correct way, probably because I have some processing to do with different parts of the ozone signal.
Here's a helpful bit of code from techsfo.com/blog/2013/08 for managing nested asynchronous network calls in which one call depends on the results of the previous. Note: this is from August, so before RAC 2.0. I believe weakself
is now created with the decorator pattern @weakify
and destroyed with the decorator @strongify
.
__weak id weakSelf = self;
[[[[[self signalGetNetworkStep1] flattenMap:^RACStream*(id *x) {
// perform your custom business logic
return [weakSelf signalGetNetworkStep2:x];
}] flattenMap:^RACStream*(id *y) {
// perform additional business logic
return [weakSelf signalGetNetworkStep3:y];
}] flattenMap:^RACStream*(id *z) {
// more business logic
return [weakSelf signalGetNetworkStep4:z];
}] subscribeNext:^(id*w) {
// last business logic
}];
Unit Testing
The code coverage isn't good, but I did create some logic tests for my astronomy code using XCTest. My test class wouldn't let me call any private methods in the class I was testing, and I didn't want to move those function definitions into the public interface. Luckily I found another way: create a category in your test class (stackoverlfow).
// category used to test private methods
@interface OWSolarWrapper (Test)
// private functions from OWSolarWrapper.m
// date functions
-(NSNumber *) julianDateFor:(NSDate *)date;
-(NSNumber *) julianCenturyForJulianDate:(NSNumber *)julianDate;
-(NSNumber *) julianDateRelative2003For:(NSDate *)date;
// basic astronomy calcs
-(NSNumber *) equationOfTimeFor:(NSDate *)date;
-(NSDictionary *) solarParametersForDate:(NSDate *)date;
@end
Beware of floatValue
:
[[NSNumber numberWithDouble:3.14159283] floatValue]
Write an extension so this raises an error! floatValue
is only 24 bit, so it truncates after 6 decimal places. This introduced a bizarre rounding error in my astronomy code.
Location Testing:
Nice comment under the original tutorial:
If you want a specific location not included in xcode, you can create a gpx file for any location. You then import it into xcode to include it in your xcode location toggles. - marciokoko on March 4, 2014, 9:18 AM
but to calculate solar positions, I really need TimeZone information along with my testing locations, which .gpx doesn't include.
TODO
- use different sky images for the background to reflect the weather prediction.
- The original background of the table view had a blur filter attached. This was accomplished with a library, but I think similar is possible with CALayer. Might be good to explore a CALayer filter on the table-view cells or the underlying scrollview. The filter would respond to scrollview position.
- UV information is only currently encoded as uvIndex ranges giving a color on the icon background. That's a start, but I'd like to do some calculation and determine two kinds of risk:
- . oveall intensity -- risk of acute sunburn, and
- . relatively high ratios of UVA at high intensity -- risk of deep damage that is harder for the body to repair.
To do this, I need to work more at combining the weather signals, possibly changing the model significantly. Thank goodness for git branches!