2

The complication "updates immediately" problem in WatchOS

Here I have a problem about update the apple watch complication's data,In the Apple Official Reference There are 5 way to update the complications,but in my repository,I think the reloadTimeline() is the best way to do so.

But there is some problem here when the Mobile APP pass the data through the WatchConnectivity to watchAPP,when I receive the data,I set a didSet in the property to trigger reloadTimeline() and update the complications.

The result isn't good enough for complications to updated immediately,I have to emphasize the most important thing is "immediately".

In my case,I need to touch the watchFace template, get in the watchAPP then back to watchFace,the update would works which reload the infomation on the template. In my sight, I think reloadTimeline() just work while the action be done.

So, you can see the way I try to get the complication data reload,but not immediately, not the expect way.

I have been search for many reference, even thousand time the Apple official Reference,but seens like nothing works here.

Some Code in the context could help your considering.

  • WatchConnectivityManager
import ClockKit
import Foundation
import WatchConnectivity

class WatchConnectivityManager: NSObject, ObservableObject {
    static let shared = WatchConnectivityManager()

    var isReachable: Bool = false

    private let kMessageKey = "message"
    private let k1MessageKey = "realtime_message"

    private let session: WCSession? = WCSession.isSupported() ? WCSession.default : nil

    private var watchCoffeeStruct: WatchCoffeeStruct? {
        didSet {
            guard let watchCoffeeStruct = watchCoffeeStruct else { return }
            
            let watchCoffeeManager = WatchCoffeeManager.shared
            watchCoffeeManager.coffeeBeanWeight = watchCoffeeStruct.coffeeBeanWeight
            watchCoffeeManager.coffeeWater = watchCoffeeStruct.coffeeWater
            watchCoffeeManager.coffeeKind = watchCoffeeStruct.coffeeKind
            watchCoffeeManager.coffeeDripper = watchCoffeeStruct.coffeeDripper
            watchCoffeeManager.waterTimeInput = watchCoffeeStruct.waterTimeInput
            watchCoffeeManager.drinkPerson= watchCoffeeStruct.drinkPerson
            print("watchCoffeeReload")
            Task {
                print("reloadActiveComplications")
                await reloadActiveComplications()
            }
        }
    }

    var validSession: WCSession? {
        #if os(iOS)
            if let session = session, session.isPaired, session.isWatchAppInstalled {
                return session
            }
        #elseif os(watchOS)
            return session
        #endif
        return nil
    }

    override init() {
        super.init()
        registerWCSession()
    }

    private func registerWCSession() {
        if let session = session {
            session.delegate = self
            session.activate()
        } else {
            print("error register WCSession")
        }
    }

    func send(notificationStruct: WatchCoffeeStruct, didSend: @escaping (String?) -> Void) {
        guard let session = session else {
            didSend("session is nil")
            return
        }

        guard session.activationState == .activated else {
            didSend("activationState is not activated. \(session.activationState)")
            return
        }

        #if os(iOS)
            guard session.isWatchAppInstalled else {
                didSend("watch app is not install")
                return
            }

        #else
            guard session.isCompanionAppInstalled else {
                didSend("app is not install")
                return
            }

        #endif

        session.sendMessage(notificationStruct.messageToDic(), replyHandler: nil) { error in
            print("did send message")
            didSend(error.localizedDescription)
        }
    }

    func send(_ communicationType: WatchCommunicationType) {
        guard let session = session else {
            return
        }

        guard session.activationState == .activated else {
            return
        }

        #if os(iOS)
            guard session.isWatchAppInstalled else {
                return
            }

        #else
            guard session.isCompanionAppInstalled else {
                return
            }

        #endif

        session.sendMessage(WatchCommunicationStruct(type: communicationType).communicationToDic(), replyHandler: nil) { error in
            print("did send message")
        }
    }

    func send(_ unit: String, didSend: @escaping (String?) -> Void) {
        let sendUnitData = ["unit": unit]
        guard let session = session else {
            didSend("session is nil")
            return
        }

        guard session.activationState == .activated else {
            didSend("activationState is not activated.")
            return
        }

        #if os(iOS)
            guard session.isWatchAppInstalled else {
                didSend("watch app is not install")
                return
            }

        #else
            guard session.isCompanionAppInstalled else {
                didSend("app is not install")
                return
            }

        #endif

        session.sendMessage(sendUnitData, replyHandler: nil) { error in
            print("did send message")
            didSend(error.localizedDescription)
        }
    }

    func updateApplication(notificationStruct: WatchCoffeeStruct, didSend: @escaping (String?) -> Void) {
        guard let session = session else {
            didSend("session is nil")
            return
        }
        do {
            try session.updateApplicationContext(notificationStruct.messageToDic())
            print("did updateApplication message")
        } catch {
            didSend("updateApplicationContext error: \(error.localizedDescription)")
        }
    }

    func updateApplication(unit: String, didSend: @escaping (String?) -> Void) {
        let sendUnitData = ["unit": unit]
        guard let session = session else {
            didSend("session is nil")
            return
        }
        do {
            try session.updateApplicationContext(sendUnitData)
        } catch {
            didSend("updateApplicationContext error: \(error.localizedDescription)")
        }
    }
}

extension WatchConnectivityManager: WCSessionDelegate {
    func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
        print("didReceiveApplicationContext")
        DispatchQueue.main.async { [self] in
            if let notificationStruct = applicationContext.dicToMessage() {
                self.watchCoffeeStruct = notificationStruct
                return
            }

            if let unit = applicationContext["unit"] as? String {
                self.watchCoffeeStruct?.unit = unit
                return
            }

            if let watchCommunicationStruct = applicationContext.dicToCommunication() {
                if watchCommunicationStruct.type == .getHistoryData {
                    NotificationCenter.default.post(
                        name: .getHistoryData,
                        object: nil,
                        userInfo: nil
                    )
                }
                return
            }
            print("didReceiveMessage error")
        }
    }

    func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
        DispatchQueue.main.async {
            if let notificationStruct = message.dicToMessage() {
                self.watchCoffeeStruct = notificationStruct
                return
            }

            if let unit = message["unit"] as? String {
                self.watchCoffeeStruct?.unit = unit
                return
            }

            if let watchCommunicationStruct = message.dicToCommunication() {
                if watchCommunicationStruct.type == .getHistoryData {
                    NotificationCenter.default.post(
                        name: .getHistoryData,
                        object: nil,
                        userInfo: nil
                    )
                }
                return
            }
            print("didReceiveMessage error")
        }
    }

    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        print("activationDidCompleteWith session = \(session.isReachable)")
        print("activationDidCompleteWith error = \(error?.localizedDescription ?? "null")")
        isReachable = session.isReachable
    }

    #if os(iOS)

        func sessionReachabilityDidChange(_ session: WCSession) {
            printD("sessionReachabilityDidChange = \(session.isReachable)")
            isReachable = session.isReachable
        }

        func sessionWatchStateDidChange(_ session: WCSession) {
            printD("sessionWatchStateDidChange = \(session.isReachable)")
            isReachable = session.isReachable
        }

        func sessionDidBecomeInactive(_ session: WCSession) {
            printD("sessionDidBecomeInactive = \(session.isReachable)")
        }

        func sessionDidDeactivate(_ session: WCSession) {
            session.activate()
            printD("sessionDidDeactivate = \(session.isReachable)")
        }
    #endif
}

  • WatchManager
class WatchCoffeeManager: ObservableObject {
static let shared = WatchCoffeeManager()

    @Published var coffeeBeanWeight: Double? = nil
    @Published var coffeeWater: Double? = nil
    @Published var coffeeKind: String? = nil
    @Published var coffeeDripper: String? = nil
    @Published var waterTimeInput: [Double]? = nil
    @Published var drinkPerson: [String]? = nil
}

  • ComplicationController
import ClockKit
import SwiftUI

enum Complications {
    case Chart, BigIndicator, TrendBall, ModularCoffeeValue, UtilitarianLargeFlatCoffeeValue, GraphicCornerCoffee
    var identifier: String {
        switch self {
        case .Chart: return "Chart"
        case .BigIndicator: return "BigIndicator"
        case .TrendBall: return "TrendBall"
        case .ModularCoffeeValue: return "ModularCoffeeValue"
        case .UtilitarianLargeFlatCoffeeValue: return "UtilitarianLargeFlatCoffeeValue"
        case .GraphicCornerCoffee: return "GraphicCornerCoffee"
        }
    }

    var displayName: String {
        switch self {
        case .Chart: return "Chart"
        case .BigIndicator: return "BigIndicator"
        case .TrendBall: return "TrendBall"
        case .ModularCoffeeValue: return "CoffeeValue"
        case .UtilitarianLargeFlatCoffeeValue: return "CoffeeValue"
        case .GraphicCornerCoffee: return "CoffeeValue"
        }
    }
}

class ComplicationController: NSObject, CLKComplicationDataSource {
    @ObservedObject private var watchManager = WatchCoffeeeManager.shared

    func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) {
        let coffeeBigIndicator = CLKComplicationDescriptor(identifier: Complications.BigIndicator.identifier, displayName: Complications.BigIndicator.displayName, supportedFamilies: [.circularSmall, .graphicCircular])

        let coffeeChartView = CLKComplicationDescriptor(identifier: Complications.Chart.identifier, displayName: Complications.Chart.displayName, supportedFamilies: [.graphicRectangular])

        let modularSmallCoffeeValue = CLKComplicationDescriptor(identifier: Complications.ModularCoffeeValue.identifier, displayName: Complications.ModularCoffeeValue.displayName, supportedFamilies: [.modularSmall])

        let utilitarianLargeFlatCoffeeValue = CLKComplicationDescriptor(identifier: Complications.UtilitarianLargeFlatCoffeeValue.identifier, displayName: Complications.UtilitarianLargeFlatCoffeeValue.displayName, supportedFamilies: [.utilitarianLarge])

        let graphicCornerCoffeeValue = CLKComplicationDescriptor(identifier: Complications.GraphicCornerCoffee.identifier, displayName: Complications.GraphicCornerCoffee.displayName, supportedFamilies: [.graphicCorner])

        handler([coffeeBigIndicator,coffeeChartView,modularSmallCoffeeValue,utilitarianLargeFlatCoffeeValue,graphicCornerCoffeeValue])
    }

    func handleSharedComplicationDescriptors(_ complicationDescriptors: [CLKComplicationDescriptor]) {
        // Do any necessary work to support these newly shared complication descriptors
    }

    func getTimelineEndDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) {
        // Call the handler with the last entry date you can currently provide or nil if you can't support future timelines
        handler(nil)
    }

    func getPrivacyBehavior(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationPrivacyBehavior) -> Void) {
        // Call the handler with your desired behavior when the device is locked
        handler(.showOnLockScreen)
    }

    func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
        var entry = CLKComplicationTimelineEntry()
        switch (complication.family, complication.identifier) {
        case (.graphicCircular, Complications.BigIndicator.identifier):
            let template = Templates.makeCoffeeIndicator(coffee: 
            .coffeeValue ?? 0, unit: watchManager.coffeeUnit ?? Unit.mgdL.rawValue)
            entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
        case (.graphicRectangular, Complications.Chart.identifier):
            let template = Templates.makeGraghicRectangularFullView(coffeeBeanWeight: watchManager.coffeeBeanWeight ?? 0,
                                                                    coffeeWater: watchManager.coffeeWater ?? "",
                                                                    coffeeKind: watchManager.coffeeKind ?? 0,
                                                                    coffeeDripper: watchManager.coffeeDripper ?? 0,
                                                                    coffeeArray: watchManager.coffeeArray ?? [0],
                                                                    drinkPerson watchManager.dateArray ?? ["", "", "", ""])
            entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
        case (.modularSmall, Complications.ModularCoffeeValue.identifier):
            let template = Templates.makeModularCoffeeValue(coffee: watchManager.coffeeValue ?? 0, unit: watchManager.coffeeUnit ?? Unit.mgdL.rawValue)
            entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
        case (.utilitarianLarge, Complications.UtilitarianLargeFlatCoffeeValue.identifier):
            let template = Templates.makeUtilitarianLargeFlatCoffeeValue(coffee: watchManager.coffeeValue ?? 0, unit: watchManager.coffeeUnit ?? Unit.mgdL.rawValue)
            entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
        case (.modularSmall, Complications.ModularCoffeeValue.identifier):
            let template = Templates.makeModularCoffeeValue(coffee: watchManager.coffeeValue ?? 0, unit: watchManager.coffeeUnit ?? Unit.mgdL.rawValue)
            entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
        case (.utilitarianLarge, Complications.UtilitarianLargeFlatCoffeeValue.identifier):
            let template = Templates.makeUtilitarianLargeFlatCoffeeValue(coffee: watchManager.coffeeValue ?? 0, unit: watchManager.coffeeUnit ?? Unit.mgdL.rawValue)
            entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
        case (.graphicCorner, Complications.GraphicCornerCoffee.identifier):
            let template = Templates.makeGraphicCornerCoffeeValue(coffeeValue: watchManager.coffeeValue ?? 0, unit: watchManager.coffeeUnit ?? Unit.mgdL.rawValue)
            entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
        default:
            print("Complication family not supported")
        }
        handler(nil)
    }

    func getTimelineEntries(for complication: CLKComplication, after date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) {
        handler(nil)
    }

    func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
        switch (complication.family, complication.identifier) {
        case (.graphicCircular, Complications.BigIndicator.identifier):
            let template = Templates.makeCoffeeIndicator(coffee: watchManager.coffeeValue ?? 0, unit: watchManager.coffeeUnit ?? Unit.mgdL.rawValue)
            handler(template)
        case (.graphicCircular, Complications.TrendBall.identifier):
            let template = Templates.makeTrendBallIndicator(coffee: Int(watchManager.coffeeValue ?? 0), trend: watchManager.trend ?? 0)
            handler(template)
        case (.modularSmall, Complications.ModularCoffeeValue.identifier):
            let template = Templates.makeModularCoffeeValue(coffee: watchManager.coffeeValue ?? 0, unit: watchManager.coffeeUnit ?? Unit.mgdL.rawValue)
            handler(template)
        case (.utilitarianSmall, nil):
            handler(nil)
        case (.utilitarianSmallFlat, nil):
            handler(nil)
        case (.utilitarianLarge, Complications.UtilitarianLargeFlatCoffeeValue.identifier):
            let template = Templates.makeUtilitarianLargeFlatCoffeeValue(coffee: watchManager.coffeeValue ?? 0, unit: watchManager.coffeeUnit ?? Unit.mgdL.rawValue)
            handler(template)
        case (.circularSmall, nil):
            handler(nil)
        case (.extraLarge, nil):
            handler(nil)
        case (.graphicCorner, Complications.GraphicCornerCoffee.identifier):
            let template = Templates.makeGraphicCornerCoffeeValue(coffeeValue: watchManager.coffeeValue ?? 0, unit: watchManager.coffeeUnit ?? Unit.mgdL.rawValue)
            handler(template)
        case (.graphicBezel, nil):
            handler(nil)
        case (.graphicCircular, nil):
            handler(nil)
        case (.graphicRectangular, Complications.Chart.identifier):
            let template = Templates.makeGraghicRectangularFullView(coffeeBeanWeight: watchManager.coffeeBeanWeight ?? 0,
                                                                    coffeeWater: watchManager.coffeeWater ?? "",
                                                                    coffeeKind: watchManager.coffeeKind ?? 0,
                                                                    coffeeDripper: watchManager.coffeeDripper ?? 0,
                                                                    coffeeArray: watchManager.coffeeArray ?? [0],
                                                                    drinkPerson watchManager.dateArray ?? ["", "", "", ""])
            handler(template)
        case (.graphicExtraLarge, nil):
            handler(nil)
        @unknown default:
            handler(nil)
        }
    }
}


There is my research reference in the past time:

Michael M.
  • 10,486
  • 9
  • 18
  • 34
  • You've provided a lot of code which can be hard to deal with when tracking down a bug. Can you provide a [Minimal Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example)? – Michael M. Sep 20 '22 at 01:52

0 Answers0