Part 1: Sample App for Collecting High-Frequency Sensor Data I have played tennis for 25 years, and I always try to improve my game – whether by watching some coaching videos or trying new apps like . At the latest WWDC, I noticed a session about introducing higher-frequency sensor data. The presenter was showing how such data can be used to analyze a golf swing. I thought that tennis could also be a perfect use case for leveraging the new sensors, so I set out to build an app that would collect raw motion data and share it as a file for further analysis. Swing Vision What's New in Core Motion In this part 1, I will show how to build such an app and share sample . In the following parts, I will present data from a real tennis session and try to turn it into something meaningful. code New in Core Motion Core Motion has been with us since the early days of iOS SDK, and it has always provided interesting capabilities that led to fun applications – such as the famous – or games where you could tilt your phone to control objects. On iPhones, Core Motion has also powered utility things like navigation and, eventually, Health and Activity. With the advent of the Apple Watch, motion data became even more useful in various workout apps where developers could use Apple-provided APIs or just raw data from the sensors. iBeer Starting from WatchOS 10.0, there is a new class, , that can give updates, which are 8x and 2x, respectively, more frequent than in the previous versions. CMBatchedSensorManager 800-Hz accelerometer and 200-Hz gyroscope Sample App Let's implement a simple Watch app that can collect the data from the new sensors and transmit them to the iPhone so that we can use this data later. Full code is available on my GitHub: https://github.com/pavelshadrin/tennis-motion First of all, we need a shared model that would be able to hold the sensor data. will let us encode and pass this data between the devices or store it on disk if needed: Codable Codable struct AccelerometerSnapshot: Codable { let timestamp: TimeInterval let accelerationX: Double let accelerationY: Double let accelerationZ: Double } struct GyroscopeSnapshot: Codable { let timestamp: TimeInterval let rotationX: Double let rotationY: Double let rotationZ: Double } struct TennisMotionData: Codable { let accelerometerSnapshots: [AccelerometerSnapshot] let gyroscopeSnapshots: [GyroscopeSnapshot] } struct TennisDataChunk: Codable { let date: Date let data: TennisMotionData // init from non-codable CM classes } On the Watch, apart from the UI to start and stop motion data collection, we need to implement several main things: 1. Set up CMBatchedSensorManager This is the main driving force of our app. let sensorManager = CMBatchedSensorManager() 2. Set up HealthKit and workout-related logic As a bonus, the app will record tennis workouts and save them in the Health app. let healthStore = HKHealthStore() var workoutSession: HKWorkoutSession? var builder: HKLiveWorkoutBuilder? // ... // sensor data can be collected only during workout let configuration = HKWorkoutConfiguration() configuration.activityType = .tennis configuration.locationType = .outdoor do { workoutSession = try HKWorkoutSession(healthStore: healthStore, configuration: configuration) builder = workoutSession?.associatedWorkoutBuilder() } catch { return } builder?.dataSource = HKLiveWorkoutDataSource( healthStore: healthStore, workoutConfiguration: configuration ) 3. Set up WatchConnectivity session to pass the recorded data to iPhone let session = WCSession.default if WCSession.isSupported() { session.delegate = self session.activate() } 4. Connect all those pieces together When the user starts data collection, we need to start the activity and begin data collection: let startDate = Date() workoutSession?.startActivity(with: startDate) builder?.beginCollection(withStart: startDate) { (success, error) in if success { self.state = .active } Task { do { for try await data in CMBatchedSensorManager().accelerometerUpdates() { let dataChunk = TennisDataChunk(date: Date(), accelerometerData: data, gyroscopeData: []) sendToiPhone(dataChunk: dataChunk) } } catch let error as NSError { print("\(error)") } } Task { do { for try await data in CMBatchedSensorManager().deviceMotionUpdates() { let dataChunk = TennisDataChunk(date: Date(), accelerometerData: [], gyroscopeData: data) sendToiPhone(dataChunk: dataChunk) } } catch let error as NSError { print("\(error)") } } } With this code above, every chunk of fresh data from the sensors will be immediately sent to the iPhone. If we don't send it right away, the data will become too large to fit into a message payload that can be sent quickly with . Otherwise, we would have to pass files. The simplest and quickest way is still to send a message with a : session.sendMessage Dictionary private func sendToiPhone(dataChunk: TennisDataChunk) { let dict: [String : Any] = ["data": dataChunk.encodeIt()] session.sendMessage(dict, replyHandler: { reply in print("Got reply from iPhone") }, errorHandler: { error in print("Failed to send data to iPhone: \(error)") }) } Then, on the receiving end, the main things to take care of are: 1. Receive and decode the motion data chunks to display them in a table view extension ViewController: WCSessionDelegate { // ... func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { guard let data: Data = message["data"] as? Data else { return } let chunk = TennisDataChunk.decodeIt(data) DispatchQueue.main.async { self.tennisDataChunks.append(chunk) } } } – for example, a .csv and then share it using to send it via AirDrop or save it to iCloud drive: 2. Be able to export those chunks as a file UIActivityViewController private func exportAccelerometerData() { var result = tennisDataChunks.reduce("") { partialResult, chunk in if !chunk.data.accelerometerSnapshots.isEmpty { return partialResult + "\n\(chunk.createAcceletometerDataCSV())" } } shareStringAsFile(string: result, filename: "tennis-acceleration-\(Date()).csv") } // same for gyroscope ^ // creating file and sharing it with share sheer private func shareStringAsFile(string: String, filename: String) { if string.isEmpty { return } do { let filename = "\(self.getDocumentsDirectory())/\(filename)" let fileURL = URL(fileURLWithPath: filename) try string.write(to: fileURL, atomically: true, encoding: .utf8) let vc = UIActivityViewController(activityItems: [fileURL], applicationActivities: []) self.present(vc, animated: true) } catch { print("cannot write file") } } private func getDocumentsDirectory() -> String { let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) let documentsDirectory = paths[0] return documentsDirectory } Now, we can run both apps simultaneously and see the data popping up on the iPhone. This is what the final app looks like: Conclusion , one of which collects the motion data, encodes it, and sends the payload to the companion app. On the other side, we decode the payload, render it in the table view, and export it by writing the data into a .csv file and launching the system's share sheet. I hope this can serve as a good example of how to use the new APIs and leverage the frameworks that are commonly used in various workout and motion Apple Watch apps. We've implemented two apps What's Next In the next article, I will show the motion data collected from my Apple Watch during a tennis session warm-up.