Create an iPhone App

Requirements

Once you created an iPhone App you need to add a few entries to the .plist file:

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>...</string>

<key>NSLocationAlwaysUsageDescription</key>
<string>...</string>

<key>NSLocationUsageDescription</key>
<string>...</string>

<key>NSLocationWhenInUseUsageDescription</key>
<string>...</string>

<key>NSMotionUsageDescription</key>
<string>...</string>

Further you have to give your App the permission to collect location data in the background. So please add the background capability to your app and activate the mode location updates. img

Installing the MotionSDK

  • Download the SDK and unzip it. Please contact Dev-Ops team for the password. MotionSDK.dmg

  • Create a lib subdirectory in your project directory and copy the contents of the MotionSDK disk image into that folder.
  • Right click your project and select “Add files to …” and select the lib subdirectory.
  • Verify that libMotionSDK.a appears under Frameworks, Libraries, and Embedded Content
  • Go to Build Settings and verify that $(PROJECT_DIR)/lib is listed under Search Path/Library Search Paths
  • In Build Settings add $(PROJECT_DIR)/lib to Swift-Compiler - Search Paths/Import Paths

Now you should be able to import MotionSDK.

Using the MotionSDK

For the simplest possible use-case you need to do two things:

  • Create and hold a reference of the AutomaticRecorder class. Please make sure that this reference is tied to the lifecycle of your app. The AppDelegate would be an obvious choice, here. @StateObject if you prefer SwiftUI lifecycle.

  • Implement handleEventsForBackgroundURLSession in the AppDelegate and set the tripUploadCompletionHandler property of your AutomaticRecorder to completionHandler.

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        os_log("handleEventsForBackgroundURLSession with identifier <%@>", log: OSLog.TripRecorder, String(describing: identifier))
        tripRecorder.tripUploadCompletionHandler = completionHandler
    }

Congratiulations! You’re ready to hit the road. All your trips will be automatically collected and uploaded to the Motion-S backend.

Observe the SDK

The MotionSDK published all state through combine publishers. So you can directely use it in a SwiftUI view:

import SwiftUI
import MotionSDK

struct ContentView: View {
    @ObservedObject var recorder: AutomaticRecorder

    var body: some View {
        Text("Recoder State: \(recorder.authorizationStatus)")
    }
}

You can pass in the recorder when your view is contructed in the SceneDelegate:

// Create the SwiftUI view that provides the window contents.
let recorder = (UIApplication.shared.delegate as! AppDelegate).recorder
let contentView = ContentView(recorder: recorder)

In the AppDelegate you can initialize the AutomaticRecorder instance:

let recorder = AutomaticRecorder()

See it working

While the app is running in the simulator go to Features->Location->Freeway Drive in the Simulator. Wait a few moments and you can observe the recorder recording location data in the Xcode console or the Console app on your mac. Filter for subsystem: com.motions.

See Section Test your App in the simulator

More observable properties

A view showing more recorder state can be achieved like this:

var body: some View {
  VStack {
    Group {
      Text("Recorder: \(recorder.state.rawValue)")
      Text("Authorization: \(recorder.authorizationStatus)")
      Text("Last Location: \(recorder.wrappedLastLocation)")
      Text("Recorded: \(recorder.recordedLocationsCount)")
      Text("Start: \(recorder.wrappedTripStartTime)")
      Text("Distance: \(recorder.wrappedTripDistance)")
      Text("Region: \(recorder.wrappedMonitoredRegion)")
    }
    Text("Last Trip: \(recorder.wrappedLastTripId)")
    Group {
      Text("Motion: \(recorder.motionActivity)")
      Text("Confidence: \(recorder.motionActivityConfidence)")
      Text(recorder.wrappedWillStopRecording)
    }
  }
}

If you want you can add a subscriber to any of the properties of the Recorder. For example the lastTrip property:

let _ = tripRecorder.$lastTrip
.compactMap { $0 }
.sink(receiveValue: { trip in
  print("The last saved trip has the id <\(trip.id)>.")
})

Test your App in the simulator

The MotionSDK runs in the simulator as well as on a real device. So you can just start your app in the simulator and load a gpx file. The recorder will automatically start recording if the provided trip will leave the 100m circular region that the recorder monitors. Since the Simulator cannot simulate CoreMotion it is useful to provide a way to call the stop() function.

Button(action: {
  self.tripRecorder.stop()
}) {
  Text("Stop Recorder")
}

Saving trips in your app

The recorder has a publisher for trips that iOS has successfully uploaded in a background session. You can use that publisher together with a very simple model that has a local id and the Motion-S sessionID as properties. You can use either to query the Motion-S backend for a trip summary or matched locations.

Let’s say you want to save the uploaded trips in a CoreData model you could achieve this with the following code:

// observe the SDK for uploaded trips and save them in the db
tripSubscriptionToken = tripRecorder.$lastTrip
	.compactMap { $0 }
	.sink(receiveValue: { (lastUploadedTrip: MotionSDK.Trip) in
  	let context = self.persistentContainer.viewContext
    context.perform {
      let trip = Trip(context: context)
      trip.id = lastUploadedTrip.id
      trip.sessionId = lastUploadedTrip.sessionId
      do {
        try context.save()
      } catch {
        os_log("Could not safe trip <%@> to the db:", log: OSLog.TripRecorder, trip, error.localizedDescription)
      }
      
      os_log("Trip <%@> saved to the db.", log: OSLog.TripRecorder, String(describing: trip))
    	}
	})

If you implement a service that talks to the Motion-S backend to get the trip summary you can dispatch that right inside the sink:

DispatchQueue.global(qos: .background).asyncAfter(deadline: DispatchTime.now() + .seconds(Profiler.gracePeriodBeforeTripSummary)) {
	Profiler.shared.updateTrip(withSummaryForTrip: trip)
}

Wait at least 50+ seconds for the backend to process the trip. Sometimes that is not enough.

Configuration Management

If you want to change the SDK configuration you can initialize a RuntimeConfigViewModel and just update it’s properties. It takes care of persisting the configuration. user and password will be saved in the secure enclave.

Final

The Recorder publishes its state via Combine-Publishers. You don’t need SwiftUI but Combine and thus iOS 13 is a requirement.

Motion-S Developer