0

I have a complication that works on Simulator, but doesn't work on an actual device when I TestFlight it to test on an actual device (and for clarity sake if there is any confusion, I'm not talking about debugging via device, but just testing if it works on a device).

Specifically, on the Watch device:

  • I select the Complication on the Watch thru customizing the clock face, which gives me the placeholder text (so far so good because getPlaceholderTemplateForComplication works on Simulator too)...
  • but then the Complication always stays as the placeholder text (not correct because getCurrentTimelineEntryForComnplication works on Simulator)...
  • even when scrolling thru Time Travel the placeholder text doesn't change but just dims (not correct because getTimelineEntriesForComplication:afterDate works on Simulator)...

Info on iPhone:

    game.duel = playoffs[“Duel”] as! String
    game.tv = playoffs[“TV”] as! String
    game.td = playoffs[“TD”] as! AnyObject
    let dictionary = [“Duel” : game.duel, “TV” : game.tv, “TD” : game.td]
    let transferComplication = WCSession.defaultSession().transferCurrentComplicationUserInfo(dictionary)

ExtensionDelegate in WatchKit Extension:

    var duelArray = [String]()
    var tvArray = [String]()
    var tdArray = [NSDate]()
    let defaults = NSUserDefaults.standardUserDefaults()

        if let duel = userInfo[“Duel”] as? String, let tv = userInfo[“TV”] as? String, let td = userInfo[“TD”] as? String {
            duelArray.append(duel)
            tvArray.append(tv)
            tdArray.append(td as! NSDate)
            defaults.setObject(duelArray, forKey: “DuelSaved”)
            defaults.setObject(tvArray, forKey: "TVSaved”)
            defaults.setObject(tdArray, forKey: "TDSaved”)
}

ComplicationController in WatchKit Extension:

    func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {
switch complication.family {
        case .ModularLarge:
            let mlTemplate = CLKComplicationTemplateModularLargeStandardBody()
                if let currentDuel = defaults.arrayForKey(“DuelSaved”) as? [String] {
                        let firstDuel = currentDuel[0]
                        let headerTextProvider = CLKSimpleTextProvider(text: firstDuel)
                        mlTemplate.headerTextProvider = headerTextProvider
                } else {
                    // …

                }
                if let currentTV = defaults.arrayForKey(“TVSaved”) as? [String] {
                    let firstTV = currentTV[0]
                    let body1TextProvider = CLKSimpleTextProvider(text: firstTV)
                    mlTemplate.body1TextProvider = body1TextProvider
                } else {
                    // …
                }
                if let currentTD = defaults.arrayForKey("TDSaved"){
                        let firstTD = currentTD[0]
                        let body2TextProvider = CLKTimeTextProvider(date: firstTD as! NSDate)
                        mlTemplate.body2TextProvider = body2TextProvider
                } else {
                    // …
                }
                let timelineEntry = CLKComplicationTimelineEntry(date: NSDate(), complicationTemplate: mlTemplate)
                handler(timelineEntry)
    // …
}


    func getTimelineEntriesForComplication(complication: CLKComplication, afterDate date: NSDate, limit: Int, withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {
        let headerArray = defaults.arrayForKey(“DuelSaved”)
        let body1Array = defaults.arrayForKey("TVSaved")
        let body2Array = defaults.arrayForKey("TDSaved")

        guard let headers = headerArray, texts = body1Array, dates = body2Array else { return }
        var entries = [CLKComplicationTimelineEntry]()
for (index, header) in headers.enumerate() {
            let text = texts[index]
            let date1 = dates[index]
            let headerTextProvider = CLKSimpleTextProvider(text: header as! String, shortText: headerShort as? String)
            let body1TextProvider = CLKSimpleTextProvider(text: text as! String)
            let timeTextProvider = CLKTimeTextProvider(date: date1 as! NSDate)
            let template = CLKComplicationTemplateModularLargeStandardBody()

            template.headerTextProvider = headerTextProvider
            template.body1TextProvider = body1TextProvider
            template.body2TextProvider = timeTextProvider

            switch complication.family {
            case .ModularLarge:
                let timelineEntry = CLKComplicationTimelineEntry(date: date1 as! NSDate, complicationTemplate: template)
                entries.append(timelineEntry)
            // …
}

    func requestedUpdateDidBegin() {        
        let server=CLKComplicationServer.sharedInstance()
        for comp in (server.activeComplications) {
            server.reloadTimelineForComplication(comp)
        }
    }

This is the flow of the data:

transferCurrentComplicationUserInfo passes data to the Watch ExtensionDelegate wherein the data is saved in NSUserDefaults. ComplicationController then pulls its initial data from NSUserDefaults.

victorpulak
  • 131
  • 2
  • 8
  • Not enough info. Where does your complication get its initial data when it is first launched? If there are no entries, it's likely that it's working off an empty (or nil) array. After the update occurs, what is `createData()` doing? is it synchronous? If not, the complication will update with no data, and the data will arrive after the update occurred. –  Mar 27 '16 at 18:25
  • @PetahChristian Sounds good, didn't want to post way too much info, so thanks for letting me know I need to add some more. The complications initial data comes from `NSUserDefaults` that was created inside the `ExtensionDelegate`s `didReceiveUserInfo` which is passed from the `transferCurrentComplicationUserInfo` – victorpulak Mar 27 '16 at 18:37
  • ClockKit will have already requested initial data *before* information is received, so there won't be any initial entries to display. Once you get the initial data, you need to manually update the complication (by reloading the timeline). –  Mar 27 '16 at 18:41
  • Is the `requestingUpdateDidBegin` method not reloading the initial timeline? If not, would I need to call `reloadTimelineForComplication()` somehow inside of `createData` to ask to "manually" reload again, or something different? And do you have any idea why this process works differently on the Device than on the Simulator? – victorpulak Mar 27 '16 at 18:47
  • Also, ClockKit will have already requested initial data before `didReceiveUserInfo` gets called your saying even if you pass the data with `transferCurrentComplicationUserInfo`? Thanks, just wanting to make sure I'm crystal clear on what you're saying since I might have not understood that before, since I would have thought my `NSUserDefaults` would have been saved by the time a user selects the Complication, esp when/if the WatchKit app had already been open & thus saved the `NSUserDefaults` which are shared between the Watch even if the WatchKit extension is not running when the Comp is – victorpulak Mar 27 '16 at 18:53
  • `requestedUpdateDidBegin` does not get called initially. It only gets called when your scheduled update occurs. As for why it works differently on the device, it's too complex to explain. It is certain that the watch will ask for your initial complication data before the data is received. If `createData` is what gets data from `NSUserDefaults` then you should be using data immediately once received, not storing it for scheduled update. Please either post more code, or share your project on GitHub or Dropbox. As for testing, you should test in Xcode *using the debugger*, not with TestFlight. –  Mar 27 '16 at 18:58
  • Sorry for the confusion on the "testing" part, tried to add some clarity to that in my question. I added a bunch of code to the question, let me know if you see anything that stands out? I think what stands out is that I'm not sure at all how the initial data is not being loaded initially, and then once that gets solved I'll need to refresh the timeline (I'll probably only refresh every 12 hours). FYI `//...` is really just for when code for the different complications would be repeating myself. – victorpulak Mar 29 '16 at 23:30

1 Answers1

2

At first glance:

  • This doesn't appear to be working code, as you'd be seeing a lot of Xcode fix-it errors about "Unicode curly quote found, ...".

  • Please avoid force downcasting with as! as it can fail and your code will crash. You've got lots of unnecessary type casting taking place. As I mentioned before, you should be typing your variables to allow the compiler to catch any programmer errors.

    For example, if your dictionary's keys and values are both strings, then safely type it as:

    var playoffs: [String: String]
    
  • Your extension delegate code may conditionally fail, if the as? downcast is not possible (because you passed something different than what you expected to receive). Make sure you're passing the types of values that you expect, or that whole block won't run. You can easily check that in the debugger by setting a breakpoint and stepping through that code.

    You also need to explicitly update your complication, once the info is received.

    func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
        if let ... { // Retrieve values from dictionary
    
            // Update complication
            let complicationServer = CLKComplicationServer.sharedInstance()
            guard let activeComplications = complicationServer.activeComplications else { // watchOS 2.2
                return
            }
    
            for complication in activeComplications {
                complicationServer.reloadTimelineForComplication(complication)
            }
        }
    }
    
  • It's really convoluted what you're doing with the arrays and NSUserDefaults. While it's perfectly appropriate to persist data between launches, NSUserDefaults is never meant to be a way to "pass" details from one part of your code to another.

    Your complication data source should get its data from a model or data manager, instead of from NSUserDefaults.

  • The getCurrentTimelineEntryForComplication if let ... { } else { code makes no sense. If you didn't get an array of strings, what do you expect to do in the else block?

    You can also prepare your data before the switch statement, to make your code more readable and compact, like so:

    func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {
        // Call the handler with the current timeline entry
    
        let recentData = DataManager.sharedManager.complicationData ?? "???"
    
        let template: CLKComplicationTemplate?
        let simpleTextProvider = CLKSimpleTextProvider(text: recentData)
    
        switch complication.family {
        case .ModularLarge:
            let modularLargeTemplate = CLKComplicationTemplateModularLargeStandardBody()
            modularLargeTemplate.headerTextProvider = CLKSimpleTextProvider(text: "Update Complication", shortText: "Update")
            modularLargeTemplate.body1TextProvider = simpleTextProvider
            template = modularLargeTemplate
        case .UtilitarianLarge:
            let utilitarianLargeTemplate = CLKComplicationTemplateUtilitarianLargeFlat()
            utilitarianLargeTemplate.textProvider = simpleTextProvider
            template = utilitarianLargeTemplate
        case .CircularSmall:
            let circularSmallTemplate = CLKComplicationTemplateCircularSmallSimpleText()
            circularSmallTemplate.textProvider = simpleTextProvider
            template = circularSmallTemplate
        case .ModularSmall:
            let modularSmallTemplate = CLKComplicationTemplateModularSmallSimpleText()
            modularSmallTemplate.textProvider = simpleTextProvider
            template = modularSmallTemplate
        case .UtilitarianSmall:
            let utilitarianSmallTemplate = CLKComplicationTemplateUtilitarianSmallFlat()
            utilitarianSmallTemplate.textProvider = simpleTextProvider
            template = utilitarianSmallTemplate
        }
        let timelineEntry = CLKComplicationTimelineEntry(date: NSDate(), complicationTemplate: template!)
        handler(timelineEntry)
    }
    

The likely issue is that ClockKit asked for complication data, well before your extension even received it, so your complication data source had no data to provide, and no entries appear.

Even though something happens to work on the simulator, it doesn't mean your code is robust to also work on the actual device. There are all sorts of differences that can account for why it won't work on the real hardware, which is why you absolutely need to debug interactively on the device. It will help you realize why your code isn't working as intended.

Ideally, you should be doing this type of interactive debugging and solving those other issues before coming here, so you can ask a very specific question with a minimal working block of code. Questions which require someone to broadly debug your code really aren't useful to others in general.

Community
  • 1
  • 1