To try and combine my love for programming, with my passion for exercise, I’m currently in the process of writing an iOS app to help out with my triathlon training.
In this post, I will cover how to register your app for external file types, how to open them and how to parse GPX using an XML parser and dynamic member lookup.
GPX
GPX (GPS Exchange Format) is a form of XML which tracks data for any given exercise. For example, when I went on a run last week and recorded my progress with my Apple Watch I was able to export my data from that run as a GPX file. GPX is supported by many 3rd party application so tends to be the norm when exporting exercise data. TCX is another common file format introduced by Garmin which is similar to GPX.
Below is an example of a GPX file from my run. It only contains two entries but the real version would have thousands, as the Apple Watch records data every two seconds as you can see by looking at the timestamps.
<gpx>
<metadata>
<time>2019-02-14T07:08:19Z</time>
</metadata>
<trk>
<name>Morning Run</name>
<trkseg>
<trkpt lat="53.2693780" lon="-2.3478250">
<ele>64.7</ele>
<time>2019-02-14T07:08:19Z</time>
<extensions>
<gpxtpx:TrackPointExtension>
<gpxtpx:hr>123</gpxtpx:hr>
<gpxtpx:cad>94</gpxtpx:cad>
</gpxtpx:TrackPointExtension>
</extensions>
</trkpt>
<trkpt lat="53.2693780" lon="-2.3478250">
<ele>64.7</ele>
<time>2019-02-14T07:08:21Z</time>
<extensions>
<gpxtpx:TrackPointExtension>
<gpxtpx:hr>123</gpxtpx:hr>
<gpxtpx:cad>94</gpxtpx:cad>
</gpxtpx:TrackPointExtension>
</extensions>
</trkpt>
</trkseg>
</trk>
</gpx>
This GPX data is very simple, it contains a start time which is in the metadata. After the metadata, the file is broken into tracks <trk>, and then track segments <trkseg>. As a run is just one continuous activity, we only have one track and one track segment. You may have multiple track segments if your hardware loses GPS signal and has to reconnect. The track then has a name and an array of track points <trkpt>. The track points contain all the data, and this is how we analyse the entire run. In this example, we have the longitude (lat) and latitude (long) which is required for all track points. It also contains a time, which is the time the data was created, and elevation <ele>. There is more data that can be in a track point, but this is all the Apple Watch gives you. It then adds some private elements, which can be anything the hardware chooses to add. In the case of the Apple Watch is adds heart rate <gpxtpx:hr> and cadence <gpxtpx:cad>.
GPX Importer
Now you know what is contained within a GPX file I want to show you how I went about importing it into my application so it could then eventually be saved within Health Kit.
Register File Type
To start with, I needed to get the OS to understand what to do when the user opens a GPX file. In this example, whenever the user tries to open a GPX file, I want them to be given the option to open it within my app. To do this you first have to register the file extension with the application, so that the OS knows the app can open the file.
To do this you need to add an entry to your info.plist for the imported UTI, and register the document type.
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>com.testapp.gpx</string>
<key>UTTypeDescription</key>
<string>GPS Exchange Format (GPX)</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.xml</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>gpx</string>
</array>
<key>public.mime-type</key>
<string>application/gpx+xml</string>
</dict>
</dict>
</array>
This first snippet allows the OS to defer the importing of a file to your application. The UTTypeIdentifier must be the same as the document type that we register next.
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>GPS Exchange Format (GPX)</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>com.testapp.gpx</string>
</array>
</dict>
</array>
Again within the plist we need to register the document type to tell the OS that the GPX file type is allowed for our application. Remember the identifier here has to match the one in the imported UTIs.
Handle URL
Once that is all in place, your application will be able to open and import any GPX file. Next we need to handle the process of when the app is actually launched due to importing the GPX file.
Again this is very straightforward, all we need to do is handle the URL which is passed in through the app delegate.
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
if url.pathExtension == "gpx" {
// handle GPX url
}
return true
}
Parse File
Now we have the URL to the file, we just have to parse it, and create some custom objects to hold our data, before we can add it into health kit. As GPX is in XML format, I used an XML parser to convert the GPX. For this example I used SwiftyXMLParser. It’s a lightweight XML parser, but has implemented dynamic member lookup to make accessing the elements even easier.
// Once you convert the file into Data, it can then be passed into the parser.
let xml = XML.parse(data)
// Most of the elements can then easily be retrieved, thanks to dynamic member lookup.
let gpx = xml.gpx
let startDate = gpx.metadata.time
let track = gpx.trk
let name = track.name
// Next is to retrieve the trackpoint data. To do this we first get an array of trackpoint data and then map it to our custom object.
let trackpoints = track.trkseg.trkpt
let workout = Workout()
workout.locations = trackpoints.map({ point -> TrackPoint in
let longitude = point.attributes["lon"]
let latitude = point.attributes["lat"]
let elevation = point.ele.text
let time = point.time.text
let extensions = point.extensions["gpxtpx:TrackPointExtension"]
let heartRate = extensions["gpxtpx:hr"].text
let cadenece = extensions["gpxtpx:cad"].text
// Then create the custom object with the above data and return.
})
Due to the fact that the longitude and latitude data are inside the trackpoint tag, we have to retrieve the values by accessing the attributes dictionary. Again as we can’t access the track point private elements using dynamic member lookup we have to revert to accessing the dictionary using a string.
We’ve now parsed the data into our model objects, we now just need to add them to Health Kit, which I will cover in the next post.