Building a watchOS and iOS app using CoreMotion and WatchConnectivity.

For a class project this semester, my team and I developed an iOS app and a paired watchOS app that predicts different exercises in a real-time manner.

www.instagram.com/p/BlyCBPfAWYk/

While the other team members worked on creating an ML model, I worked on developing the app in Swift. I have the most amount of experience in Java compared to any other languages just because most of my courses happened to be in Java. I’m always very grateful for Google and Stack Overflow but while working with Swift, especially with Swift 4, it made me realize that perhaps I rely too often on the power of the internet when times get hard.

I found that the number of answers to specific Swift questions for app development on Stack Overflow was very low compared to that of Java, Python, and C++. More specifically, even some of the blog posts or resources from 2015 were already outdated because there had been some changes between Swift 2 and Swift 4, that I felt were significant. Granted, it is a very young language compared to others like Java and perhaps I was just not familiar enough with Swift yet when the project started 😅.


Collecting Movement Data

For collecting user movement data, I used Apple’s Core Motion and referenced Introduction to Apple WatchKit with Core Motion — Tracking Jumping Jacks by Eric Hsiao with modifications. Here, Eric’s app tracks User Acceleration, Gravity, Attitude, and RotationRate. He explains each measurement in detail with clear photos and extracts the raw data from the log files.

Unlike Eric’s app, we needed to programmatically extract much more granular data points. I also realized that when the Apple Watch screen fell asleep, the app could not collect data points like User Acceleration. The screen can fall asleep depending on the watch orientation on the wrist. In order to account for the different wrist orientation during the exercises, we had to implement a way for the app to work in the background such as using Apple’s HealthKit or location services.

All the exercise data points were saved using the Codable protocol and then written into a temporary file, in a temporary directory (the temporary files may be deleted by the system after three days).

// At the beginning, I initialized these variables
// ExerciseData is my Codable struct
let jsonEncoder = JSONEncoder()
var data: [ExerciseData] = []
.....
func startCollection() {
tempDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
tempFileName = ProcessInfo().globallyUniqueString;
tempFileURL = tempDirectoryURL.appendingPathCompoent(tempFileName)
// etc for data collection...
}
func stopCollection() {
// finish processing...
do {
let allData = try jsonEncoder.encode(data)
try allData.write(to: tempFileURL, options: .atomicWrite)
} catch {
print(error.localizedDescription)
}
}

Using WatchConnectivity for File Transfers

One of the biggest hurdles that I came across was establishing a proper connection between the Apple Watch app and its paired iPhone app. Because we had decided to send all the data points to our server from the iOS app, I had to figure out a way to transfer the data from the watch to the phone.

Apple provides a framework called WatchConnectivity which implements communication between Apple Watches and their paired iPhones. Apple’s documentation states:

Use this framework to transfer data between your iOS app and the WatchKit extension of a paired watchOS app. You can pass small amounts of data or entire files. You also use this framework to trigger an update to your watchOS app’s complication.

There are two types of communications that can be established between the watch and the phone apps:

A. Interactive Messaging (INSTANT)

  • sendMessage which sends a single message and sendMessageData which sends a data object.
  • Useful for when information is needed immediately
  • Especially useful for waking up the iPhone app in the background (only when the phone’s state is “reachable”) and the watch app is in the foreground.

B. Background Transfers (WHENEVER)

  • applicationContext is used only for the latest information. Any following call will replace the previous call’s information.
  • transferUserInfo does NOT replace the previous information and the content is delivered in a FIFO manner.
  • transferFile delivers files to the paired app. The device will try to send the file as quickly as possible. The file must be synchronously moved to a new location or processed once it is delivered because the system will delete it after the method returns.

I found both iOS Development: Watch Connectivity by Vanessa Forney and watchOS 2: How to communicate between devices using Watch Connectivity by Kristina Fox to be very helpful in understanding various aspects of the watch to phone communication. But most importantly, WatchOS5 — Communication between iPhone and Apple Watch and Vice Versa on Swift by Lito was the perfect tutorial with some modifications.

Initially, I had implementedsendMessage of the interactive messaging system. However, just like the problem I had mentioned earlier in regard to the watch screen turning off, the app was not able to send messages to the phone app while it was in the background.

Then I switched to transferUserInfo but ran into issues with lagging transfers. It was difficult to know when the final data point would arrive.

Finally, I ended up using sendMessage to wake up the phone app and transferFile to send the final JSON file from the watch to the phone.

Extending Lito’s code:

typealias FileReceived = (session: WCSession, file: WCSessionFile)
protocol iOSDelegate: AnyObject {
func fileReceived(tuple: FileReceived)
}
class WatchSessionManager: NSObject {
static let sharedManager = WatchSessionManager()
weak var iOSDelegate: iOSDelegate?
// establish session and make sure it is valid 
// start session
}
extension WatchSessionManager {
func transferFile(file: URL, user_metadata: [String : Any]?) -> WCSEssionFileTransfer? {
return validSession?.transferFile(file, metadata: user_metadata)
}
func session(_ session: WCSession, didReceive file: WCSessionFile) {
handleFileSession(session, didReceiveFile: file)
}
func handleFileSession(_ session: WCSession, didReceiveFile file: WCSessionFile) {
iOSDelegate?.fileReceived(tuple: (session, file))
}
}

Then in order to send the file from the watch,

func sendData(_ message: [String: Any]) -> Void {
connectivityHandler.transferFile(file: sensorManager.temporaryFileURL, user_metadata: fileMetaData)
}

And to receive the file on the phone,

var appFolderURL: URL?
// initialize a directory to move all the incoming files to
if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
appFolderURL = documentsDirectory.appendingPathComponent("appData")
if !FileManager.default.fileExists(atPath: appFolderURL!.path) {
do {
try FileManager.default.createDirectory(atPath: appFolderURL!.path, withIntermediateDirectories: true, attributes: nil)
} catch {
print(error.localizedDescription)
}
}
}
// this class must declare iOSDelegate
func fileReceived(tuple: FileReceived) {
let tempFileName = ProcessInfo().globallyUniqueString
let tempFileURL = appFolderURL?.appendingPathComponent(tempFileName)
do {
try FileManager.default.moveItem(at: tuple.file.fileURL, to: tempFileURL!)
} catch {
print(error.localizedDescription)
}
}

The phone app displayed which exercise Core ML predicted on the watch after receiving the result via sendMessage .

www.nbc.com/saturday-night-live

While the code or the app may not be perfect, it was a great introduction to learning Swift and app development. Though it may only be a minimum viable product, it was exciting to have been part of the initial development of something that could potentially improve the lives of cancer survivors!

*Special thanks to Andy Davila for all the endless help!*