An iOS app for plotting live data: ConAir:iOS
In previous posts on this blog we’ve built a basic environmental monitoring system which exposes data as a simple JSON webservice. This post is looking at how to build an iOS app to consume the timeseries data. We’ll establish the following:
- A datasource object which pulls data from a webservice
- Setting the data source to poll for new data
- Create a UI which updates when new data arrives
- Plotting the data in a chart
All of this can be applied to any webservice available, but since we have built something suitable as part of this blog we’ll use that.
We’re working towards building the sample app I’ve put together at github.com/sammyd/conair-ios. I’ll cover the most salient points, but won’t give an in-depth review of the app’s source code.
A DataSource
We’ll create a class which is responsible for retrieving the data, and storing a local cache of it. Our different UI controllers will interact with this class to get hold of datapoints to render.
We will only want one instance of a datasource at any one time - multiple data sources in one application would be both memory intensive, and would cause multiple network requests for the same data. We therefore make the datasource a singleton:
This header also exposes a readonly data array - which we will use later. The equivalent implementation is as follows:
We’ve redefined the readonly data property to be readwrite, and implemented the
+sharedDataSource
class method, to create a singleton.
In the first instance we’re going to pull the data from the webservice when the datasource is created, and to that end, we implement the standard constructor, and a utility method:
The collectDataFromInternet
method does all the heavy lifting. Firstly we construct
the URL from which we can collect the data. This requires a start date, end date
and a sampling period. The dateAsISO8601String
method is added on NSDate
with a
category (using a NSDateFormatter
to construct a string of the correct format).
We request the data on a background thread so as to not lock the main thread whilst
we wait for a response. Once the data has been retrieved, we can parse the JSON
into an NSArray
using iOS methods - a lot easier since iOS 5 when these
were introducted.
We then iterate through this array, and create a datapoint object for each of
the received data points. Finally, we assign this newly created array to the
data
property on the datasource object. Note that we do this operation back
on the main thread. By working on the main thread we can keep data
property
as nonatomic, without worrying about multi-threading issues.
Now that we have a collected the data we could display the latest temperature in a label really simply:
There main issue with this as it stands is that the data property of the data source
will be nil
until the data has been collected from the internet. We will address
this issue in the “auto-updating UI” section.
Polling for new data
The method we wrote before to pull data down from the internet performs the operation
once. If it is called again, it’ll request a new 24-hr period of data (based on
the current time), and replace the original. In order to support some polling
behaviour, we want to update the collectDataFromInternet
method which to collect
any new data since the most recent data point we already have.
Firstly, we need to make sure that the start time is the timestamp of the last datapoint we already have:
When we are parsing the returned JSON, we should check that the data points
returned are newer than our latest one. This is primarily for the case where we
get a repeated data point at the boundary. Given that we’ve pulled the latest date
out into a local variable currentLatestDate
(i.e. the date of the last object
in the data array), then we add the following conditional to the enumeration block:
And finally, rather than replacing the data
property with the newly collected
datapoints, we might need to append the new datapoints to the existing data
array:
Now, repeatedly calling this collectDataFromInternet
method will update our
cached data array with new datapoints, if they are available. In order to repeatedly
call this we add an NSTimer
to the datasource, and add methods to start and stop
the polling process.
Now we can send a startPolling
message to the shared data source, and be assured
that it will contain the latest data at any given time.
In the app which demonstrates this we start the polling when the app loads and stop it when the app is no longer active:
A simple auto-updating UI
Now we have a datasource which will always have the most recent data, we want to ensure that we are always displaying the latest data. iOS has a helpful mechanism which we can utilise to assist with this task - Key-Value Observing.
KVO allows us to subscribe to be notified whenever a specified key-path is updated, and hence (in this instance) update the UI.
In our view controller, we subscribe to changes in the datasource’s data
property:
The import message to send KVC-compliant objects to observe their property changes
is addObserver:forKeyPath:options:context
. The will mean that the listener will
receive messages when the appropriate key-path changes. It is important to remove
observers when the instance is dealloc’ed, otherwise you’ll start getting zombie issues.
Whenever any KVO changes occur, they all pass a message of the same signature to the observer object. We implement the appropriate method, and update our temperature label:
It’s (almost) that simple. Now, whenever the data
property is changed on the
datasource the temperature label will be updated. If you run up your app at this
point you’ll see that the label appears with updating...
in it, and then after
a short time a temperature value is displayed. Fantastic.
However, there is a slight issue. The KVO will only be triggered when the property itself is changed. This happens when we first receive data, but subsequent polling updates don’t change the property, but instead add the new datapoints to the end of the array. This doesn’t trigger the KVO, so won’t update the label.
In order to get updates when objects are added to our array, we need to implement (and use) the Key-Value Coding collection accessor methods on our datasource. These
There are more details on these methods in the apple documentation.
We wrap the standard NSMutableArray
methods as appropriate.
Then, when we add data to our array, we simply need to use these KVC accessor methods instead of the array directly - this will then trigger the KVO update:
If you run the app up now, then you’ll see that the date label gets updated as the polling process pulls in more data (note, new data on the ConAir service arrives approximately every 2 minutes). Perfect - and with no changes to code in the view controller.
Plotting data in a chart
Although this post has been about getting live data from the internet on an iOS device, the driving force behind it has been plotting the ConAir data we’ve been collecting on a nice chart on an iOS device. We’ve now got a datasource which has the time series we want to plot. So, now to add a chart.
This post isn’t about how to plot charts in iOS, so I’ll just give a quick summary here. We’re going to use a super-cool charting library called ShinobiCharts. You can get a 30-day trial free of charge, so go ahead an grab it and give it a try. If you have serious charting needs for iOS then you should definitely give it a try (disclaimer: I work for the company which creates ShinobiControls).
A chart is a UIView subclass, so we add one to a new view controller, and then provide it with a data source.
We update our existing datasource to adopt the
SChartDatasource
protocol, which can then be used to provide the data to the
chart:
These are the required methods of an SChartDatasource
and are all pretty self
explanatory.
We repeat the same KVO process we did on the label updating view controller:
And we’re done! That gives us a chart of the last 24 hours of temperature data, which will live-update as new readings are send from the arduino board we put together way back when!
As ever, the source code for the sample app I’ve based this post on is available on github at github.com/sammyd/conair-ios. I make no guarantees of code quality - it’s a proof of concept and should be treated as such. If you wish to try the charts then you’ll have to sign up to a trial on shinobicontrols.com, add the framework to the Xcode project and replace the license key in the github repo with the one provided in your trial email.