Search code examples
iosswiftwatchkitwatchos-2apple-watch-complication

Complication works on Simulator, but not on Device


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.


Solution

  • 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.