help me! (data sync between watch and phone)

I'm making an Apple Watch app and an iPhone app. The structure of the app is as follows.

  1. (On Apple Watch) Count the number and store it in Userdefault.(title, number)
  2. (On Apple Watch) Press the 'Count Complete' button to send the title and number data to the iPhone app.
  3. (In the iPhone app) Save the data received from the Apple Watch as CoreData.

Here's the problem. I don't know the proper way to transfer the data (title, number) generated by the Apple Watch to the iPhone.

Answered by AncientCoder in 686982022

The key documentation is here https://developer.apple.com/documentation/watchconnectivity/wcsession

In essence, you need a class (can be the AppDelegate) in your iOS app that implements the WCSessionDelegate protocol, establishes a WCSession and then processes incoming watch data via func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any]).

In the watch app Extension you need to create a class that adopts WCSessionDelegate, then when required create and activate a session within that class, then the class sends the information (title, number) to the phone using session.transferUserInfo(obsDict) - where session is the WCSession you instantiated and activated.

In each case, don't forget to assign the delegate.

There are various methods of sending data between the watch and the phone, but the above is probably the best suited to your needs. This is what I mostly use, especially if the phone app is not active at the time the watch sends the data.

There's also some other housekeeping to do, such as checking that a session is active: the documentation describes this.

Good luck, Michaela

Accepted Answer

The key documentation is here https://developer.apple.com/documentation/watchconnectivity/wcsession

In essence, you need a class (can be the AppDelegate) in your iOS app that implements the WCSessionDelegate protocol, establishes a WCSession and then processes incoming watch data via func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any]).

In the watch app Extension you need to create a class that adopts WCSessionDelegate, then when required create and activate a session within that class, then the class sends the information (title, number) to the phone using session.transferUserInfo(obsDict) - where session is the WCSession you instantiated and activated.

In each case, don't forget to assign the delegate.

There are various methods of sending data between the watch and the phone, but the above is probably the best suited to your needs. This is what I mostly use, especially if the phone app is not active at the time the watch sends the data.

There's also some other housekeeping to do, such as checking that a session is active: the documentation describes this.

Good luck, Michaela

You’ll also find this WWDC session helpful: https://developer.apple.com/videos/play/wwdc2021/10003/

Here's a working version for SwiftUI. I haven't implemented storage in UserDefaults nor CoreData, so as to keep the solution brief for posting here. Also, in your watch app you'll probably want to disable the Send button after the user has sent and before more items are counted: i.e. prevent duplicate sending of data to the phone.

It's probably best to create a new Xcode project, e.g. WatchDemo, and specify SwiftUI with a SwiftUI interface. Then, in the WatchKIt Extension part of the project create a Swift WatchOS file (Xcode) containing the following code:

import Foundation
import WatchConnectivity
class WatchDataModel : NSObject, WCSessionDelegate, ObservableObject {
    static let shared = WatchDataModel()
    let session = WCSession.default
    @Published var watchCount : Int = 0
    override init() {
        super.init()
        if WCSession.isSupported() {
            session.delegate = self
            session.activate()
        } else {
            print("ERROR: Watch session not supported")
        }
    }
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        if let error = error {
            print("session activation failed with error: \(error.localizedDescription)")
            return
        }
    }
}

The WatchKit Extension ContentView for this demo app is the following:

import SwiftUI
struct ContentView: View {
    @ObservedObject var data = WatchDataModel.shared
    var body: some View {
        VStack{
            HStack{
                Button("Count Me") {
                    data.watchCount += 1
                } .foregroundColor(.red)
                Text(" ==> \(data.watchCount)")
            }
            Button("Send to Phone") {
                data.session.transferUserInfo(["WatchCount":data.watchCount])
            } .foregroundColor(.blue)
        }
    }
}

In the Phone app part (group) of your Xcode project, create an iOS file containing:

import Foundation
import WatchConnectivity
class PhoneDataModel : NSObject, WCSessionDelegate, ObservableObject {
    static let shared = PhoneDataModel()
    let session = WCSession.default
    @Published var watchCount : Int = 0
    override init() {
        super.init()
        if WCSession.isSupported() {
            session.delegate = self
            session.activate()
        } else {
            print("ERROR: Watch session not supported")
        }
    }
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        if let error = error {
            print("session activation failed with error: \(error.localizedDescription)")
            return
        }
    }
    func sessionDidBecomeInactive(_ session: WCSession) {
        session.activate()
    }
    func sessionDidDeactivate(_ session: WCSession) {
        session.activate()
    }
    func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any]) {
        guard let newCount = userInfo["WatchCount"] as? Int else {
            print("ERROR: unknown data received from Watch")
            return
        }
        DispatchQueue.main.async {
            self.watchCount = newCount  // resets count to be last sent count from watch
        }
    }
}

The Phone app's ContentView for this demo app is:

import SwiftUI
struct ContentView: View {
    @ObservedObject var data = PhoneDataModel.shared
    var body: some View {
        Text("Count received from watch = \(data.watchCount)")
            .padding()
    }
}

NOTE that with this method of sending data from the watch, the Phone app does not need to be active, nor the phone nearby etc. Transfer data are queued until the phone app is launched and a session established. If you want to send data to the watch, then add a func session(_ session: WCSession, didReceiveUserInfo userInfo: function to the WatchDataModel and send data from the phone by calling data.session.transferUserInfo at an appropriate point (where data = PhoneDataModel.shared)

Once you've got your mind around how this demo app works, you should be able to construct your own, or modify this one.

Good luck and best wishes, Michaela

Further to my last comment re your transferUserInfo problem, put the following struct definition (or something similar depending on your specific needs) in each of your data model files (WatchDataModel & PhoneDataModel) before the class definition

struct CountData {
    var title : String = ""
    var tCounts = [Int]()  // an array of counts
    var creationDate = Date()
}

In your watch app Extension code create an instance of CountData e.g. var countData = CountData() somewhere in WatchDataModel, then (as required) add your title, the various counts and the date created. For the counts, you can do countData.tCounts.append(integer) each time you determine a count. The date/time is already set when you create the countData, but it might be best to update it when you know you've collected all counts e.g. countData.creationDate = Date()

Then in your ContentView (Send button) use data.session.transferUserInfo(["CountData":data.countData])

In your PhoneDataModel replace the didReceiveUserInfo function with:

func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any]) {
        guard let countData = userInfo["CountData"] as? CountData else {
            print("ERROR: unknown data received from Watch")
            return
        }

        // now do something with the received CountData

    }

If needs be, you can send an array of CountData structs from the watch i.e. multiple titles with their counts. If you need to do this I'll show you how, assuming that you've worked out the code to create the array.

Regards, Michaela

I just started development. regardless of developer, I've never seen such a kind person before. you are my first teacher.

To. Michaela

I'm looking at your code and studying it carefully. But I think your teaching will be meaningful only if I can make it myself.

I'm really sorry, but can you explain the process of WatchConnectivity?

No site explains the process in an easy to understand way.

But your code is interesting because it consists only of 'WatchDataModel', 'PhoneDataModel'.

It would be easier to understand if you explain how the 'TransferUserInfo' method works in your code.

Specific questions are as follows:

  1. I think you should use the userdefaults method to keep the count data even when multitasking of the watch app is turned off. So, can we record title, count1, and count2 as UserDefaults in the ContentView file of the watch app and send them to the Phone app using the transferuserinfo method?

  2. Is the 'WatchDataModel' class a class that implements a data transmission method?

  3. Is data.session.transferUserInfo(["WatchCount":data.watchCount]) in the button area of ​​the watch app's ContentView the same as WCSession.default.transferUserInfo(["WatchCount":data.watchCount])?

  4. If so, does func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) in PhoneDataModel class receive data.watchCount (= watchCount variable in WatchDataModel class) with key "WatchCount"?

  5. If this is a function that receives data, can I write a code that directly saves the received data as CoreData?

  6. If so, wouldn't it be enough to simply output the number of CoreData in the Phone app's ContentView?

It may be cumbersome, but in Korea where I live, there is no one who can answer these questions. There are very few people majoring in programming in Korea, and there is no space to share knowledge. So I'm studying by myself with a translator. Your answer is taking me step by step. Thank you so much.

I’ll answer your questions in 3 sections: 1. DataModels and SwiftUI, 2. Watch Connectivity and 3. Saving and synchronising data.

DataModels and SwiftUI

A key concept of SwiftUI is that the user interface (SwiftUI Views) only interacts with the user, displaying text etc and collecting input (e.g. button presses).  All other app processing (e.g. data manipulation, data storage and retrieval) is done elsewhere in an app-specific class (typically called “DataModel”) created by the developer.  Your app essentially has two parts: the watch app and the phone app.  Each needs a DataModel - “WatchDataModel” and “PhoneDataModel” - being names that I decided.  The tasks of the WatchDataModel are to 1. Collect and aggregate (?) titles and counts, 2. Transmit those to the phone 3. Maintain a local copy of the collected data 4. Prepare data for displaying to the user.  The PhoneDataModel has to 1. Receive aggregated data from the watch 2. Store those data locally, synchronising as required with existing data (?) 3. Prepare data for displaying to the user, i.e. creating a property that a SwiftUI View can refer to for displaying on the screen.

In my sample code, each data model is created as a singleton - meaning that there is only ever one copy created, no matter how many times it gets referred to elsewhere within your app.

class WatchDataModel : NSObject, WCSessionDelegate, ObservableObject {
    static let shared = WatchDataModel() // when referring to WatchDataModel.shared only one version is ever created
    let session = WCSession.default  // this essentially creates a shortcut to WCSession.default

Watch Connectivity

Within each data model, the connectivity to/from the watch is provided by the WatchConnectivity framework and the DataModel’s class has to adopt the WCSessionDelegate protocol for this to work. 

import WatchConnectivity  // the Apple code to control the watch-phone connection
class WatchDataModel : NSObject, WCSessionDelegate, ObservableObject {   // WCSessionDelegate allows this class to be "delegated" certain tasks from WCSession

When your app starts up (Watch or Phone), it needs to initialise its data model, which then needs to tell the connectivity controller (WCSession.default) to delegate certain tasks (e.g. in PhoneDataModel, deal with incoming data) to the data model.  You can refer to this controller directly as WCSession.default, but typically one creates a static property let session = WCSession.default and then refer to that property. 

Before sending / receiving, you have to activate a session (in fact, you should really check that a session is active before attempting to send anything).

if WCSession.isSupported() {  // checks if this device is capable of watch-phone communication (e.g. an iPad is not)
            session.delegate = self // allows this DataModel (class) to process tasks delegated (assigned) to it by WCSession
            session.activate()  // activates a session so that communication can occur
        } else {
            print("ERROR: Watch session not supported")
        }

As I’ve said previously, there’s a number of ways of sending / receiving data:  the choice depends on the volume and type of data, and the urgency of the communication.  The one I usually use is transferUserInfo, because of its reliability under various situations.

transferUserInfo sends a dictionary of property list values e.g. “Title”:”Hello World” or “Count” : 21 - the first part is the key and the second is the actual value.  But there are various ways of constructing a Dictionary, especially with multiple values.  Ref https://developer.apple.com/documentation/swift/dictionary. I don’t know enough about the purpose of your app and the volume / complexity of the data for me to make any suggestion, apart from my previous idea of a struct:  

struct CountData {
    var title : String = ""
    var tCounts = [Int]()  // an array of counts
    var creationDate = Date()
}
data.session.transferUserInfo(["CountData":data.countData])  // in the watch's ContentView we send a CountData struct (in data.countData) with a key of "CountData" via transferUserInfo to the phone

Importantly, when your phone app receives the dictionary, you must be able to access the dictionary values and do whatever you need to do.

func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any]) {
// WCSession has delegated to this PhoneDataModel the task of receiving and processing user info 
        guard let countData = userInfo["CountData"] as? CountData else { // check that what has been received is the info that was sent.  In this example a struct we defined, CountData containing title, counts and date, was sent with a key "CountData"
            print("ERROR: unknown data received from Watch")
            return
        }
        // now do something with the received CountData
    }

Saving and Synchronising Data

Having received the watch data on the phone via didReceiveUserInfo you now have to do something with it. But what?

I can think of at least 6 different ways of saving and synchronising your data (i.e. making sure the watch and phone have identical sets of data). But, again, I don’t know enough about the purpose of your app and the volume / complexity of the data for me to make any recommendation. Your idea of UserDefaults could be problematic because of your data characteristics and the limits of UserDefaults. Ref https://developer.apple.com/documentation/foundation/userdefaults

I normally use SQLIte instead of CoreData, but that's only because I'd been using SQL databases for decades before I came to Apple development and I find SQL's queries, views, joins and indexes far more flexible - and within my knowledge base.


I'll have to finish here: I'm tired (: Regards, Michaela

To Michaela

Couldn't explain my app in detail.

summary:

(in watch app) Set the name of the item to be counted and count two.

That is, there are count 1 and count 2. This app should keep the item's name and count number even when multitasking is finished.

And when the 'send to phone' button is pressed, the data set is sent to the phone and the variables are initialized.

(in phone app) Store the data received from the watch in CoreData. In the contentview area, we will output the data stored in CoreData as a table. Simple... that's all.

Questions:

  1. Why not use userdefaults inside WatchDataModel?

  2. Is there a method that receives data from PhoneDataModel to WCSession and saves it in CoreData at the same time? (Entity:Goal/Attributes:title, criationdate, Shooting, save)

I will never forget thank you.

OK, now that I understand better what you are doing, this is what I would recommend (assuming that your data volumes are not several megabytes in total):

Use CoreData on both the Watch and the Phone, with a CoreData entity model that is exactly the same on both. When the watch app starts, create a new Goal (using the entity class created by Xcode in XCDataModel) and set the item name, then start counting.

When the "Send to Phone" button is pressed, save (write) the new Goal to Coredata on the Watch and also send it to the phone using transferUserInfo(["AddGoal":newGoal]). This sends a CoreData object to the phone, for saving in the phone's CoreData storage.

In the phone app, with the same CoreData entity model, get the newGoal from didReceiveUserInfo userInfo:

func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any]) {
        guard let newGoal = userInfo["AddGoal"] as? Goal else { // "Goal" is the class generated by Xcode from your CoreData entity model, so newGoal is a CoreData object.
            print("ERROR: unknown data received from Watch")
            return
        }
        // now write newGoal to CoreData in the same way as you did on the watch
    }

I rarely use CoreData, so I'm not confident in writing sample code for you - but I know that this solution should work.

You can then also use CoreData and SwiftUI to display a table of data on the watch (if you want).


Time for me to put my feet up and relax :). Cheers, Michaela

help me! (data sync between watch and phone)
 
 
Q