1

I'm getting the following error when requesting a dictionary of XML items via the above method:

> NSLocalizedDescription=The UIApplicationDelegate in the iPhone App never called reply() in -[UIApplicationDelegate application:handleWatchKitExtensionRequest:reply:]

I have no problem passing an NSDictionary consisting of an NSMutableArray of NSStrings.

From the interface controller:

- (void) requestFeedsFromPhone
{
    [WKInterfaceController openParentApplication:@{@"request":@"feeds"}
                                       reply:^(NSDictionary *replyInfo, NSError *error) {

                                           // the request was successful
                                           if(error == nil) {

                                               // get the array of items
                                               NSMutableDictionary *tempDictionary = replyInfo[@"feeds"];


                                               NSLog(@"tempDictionary: %@", tempDictionary[@"feedsArray"]);
                                               self.feeds = tempDictionary[@"feedsArray"];
                                               [self setupTable];
                                           }
                                           else{
                                               NSLog(@"ERROR: %@", error);
                                           }
                                       }];
}

In the app delegate:

- (void) application:(UIApplication *)application
handleWatchKitExtensionRequest:(NSDictionary *)userInfo
           reply:(void (^)(NSDictionary *))reply
{

    MasterViewController *mainController = (MasterViewController*)  self.window.rootViewController;

    //this is the troublesome line - calling this method results in the error
    NSDictionary *feedsDictionary = [mainController returnFeedsDictionary];

    reply(@{@"feeds": feedsDictionary});
}

In the MasterViewController:

-(NSDictionary *) returnFeedsDictionary
{

    NSURL *url = [NSURL URLWithString:@"http://www.nasa.gov/rss/dyn/lg_image_of_the_day.rss"];
    SeparateParser *separateParser = [[SeparateParser alloc] initWithURL:url];
    [separateParser parse];
    NSArray *tempArray = [separateParser returnFeeds];
    return @{@"feedsArray": tempArray};

}

The returnFeeds method returns an NSMutableArray of NSMutableDictionarys filled with NSMutableStrings (title, link, imageURL, etc).

I'm assuming my problem is that some of my data isn't property list compliant but I thought arrays, strings, and dictionaries were acceptable.

MayNotBe
  • 2,110
  • 3
  • 32
  • 47
  • Arrays, strings and dictionaries are acceptable. The data recursively inside of them also must be acceptable. I will also note that there may be size limitations... may. So you might want to restrict the amount of data your sending over to something reasonable. – TheCodingArt May 05 '15 at 18:51
  • @TheCodingArt I'll try restricting the data and let you know. I like that "may", like ... who knows? – MayNotBe May 05 '15 at 18:56
  • Well, it would be undocumented and there are serialization limits for other things like data in the Photos framework (and you'd assume that would exist here due to the obvious limitations going on). You need to ensure all data complies with plist formatting though (which you can setup code to generate a plist file to double check this). – TheCodingArt May 05 '15 at 18:57
  • It may be better for you to setup an app group and write the plist file to NSUserDefaults using the shared app group. If you do this atomically, sharing data back and forth won't require notifications or the Darwin notification system. (making it more reliable/faster/efficient). You'd just update your feed and then have a callback that would re read the file. – TheCodingArt May 05 '15 at 18:59
  • Well, size wasn't a problem. I'm going to try to generate a plist and then I'll look into using NSUserDefaults. Thanks! – MayNotBe May 05 '15 at 19:03
  • Well - it generated a plist just fine. what could the problem be! – MayNotBe May 05 '15 at 19:28
  • Well, now that I'm reading your error as well... how long does it take to call the reply callback. There is a time limit on this. – TheCodingArt May 05 '15 at 19:29
  • The iPhone app will wake up in background mode. That being provided... a time limit will be set meaning the app on the phone could be canned before calling the reply call. You can attach a debugger to the phone app while it's running and check this (or try to log this some other way to confirm). One way to test this would be to set a dispatch after operation and test if reply is called back in there after a period of waiting. The error above definitely doesn't indicate any issues with the plist. – TheCodingArt May 05 '15 at 19:31
  • I don't think it's a time thing - I had `returnFeedsDictionary` create a temporary array on the spot with two strings and reply still wasn't called. Maybe it has to do with how I'm accessing the MasterViewController? – MayNotBe May 05 '15 at 19:37
  • I've actually done a very similar thing, you can check to ensure the VC is loading correctly (and in all honesty you shouldn't be relying on a ViewController for data.. you should separate that into it's own class). At this point I would assume everything is configured and you have your root VC setup. Take note you may want to check that this is running the most recent version of your application (run both the app on the phone and then run the extension on the phone). – TheCodingArt May 05 '15 at 19:41
  • I have yet been required to place something into a background task... but then again, every networking operation I perform has been done in it's own framework. – TheCodingArt May 05 '15 at 19:42
  • https://medium.com/@kangaroo5383/4-reasons-your-watchkit-app-is-sucking-a2ad12374eed – TheCodingArt May 05 '15 at 19:43
  • This article should help you debug this. Definitely split that code out from the VC. It is possible your rootVC is not configured and your app is crashing. – TheCodingArt May 05 '15 at 19:44
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/77039/discussion-between-maynotbe-and-thecodingart). – MayNotBe May 05 '15 at 20:00

3 Answers3

4

You are hitting the Law of Leaky Abstractions.

Your iPhone-App and WatchKit-Extension run as two separate processes and two separate sandboxes. The OpenParentApplication/handleWatchKitExtensionRequest/reply mechanism is but a convenience mechanism that Apple provides to wrap intra-process-communication.

The Abstraction Leak:

reply(feedsDictionary)

The returned dictionary can only contain

  • NSData
  • NSString
  • NSDate
  • NSArray
  • NSDictionary

    If your return dictionary contains anything beyond that, like a custom class instance, then you get that crappy error saying reply() was never called, even though you did!



Solution 1: Serialize your object and send it from app-to-Watch, and then de-serialize on watch (aka Marshalling/Unmarshalling).

  • This requires using NSCoding, as mentioned in an above post: Serialize to NSData* stream, send reply(dataStream) and then de-serialize in the WatchKit Extension.
  • NSCoding is a pain. You need to use NSKeyedArchiver explicitly to serialize before sending, and NSKeyedUnarchiver to de-serialize tutorial here.
  • Requires a lot of cruft code to support encode/Decode within the class Object itself (to satisfy NSCoder)

Solution 2: Simply pass a string to act as Key, and instantiate your object inside of the Watch Extension (instead of passing the fat object instance itself)

  • Allow WatchKit Extension to compile against your class files, via clicking on the {class}.m, pressing command+option+1, and clicking the "...WatchKit Extension" checkbox under Target Membership (you need to do this anyway, even with solution 1)

  • e.g. Your WatchKit Extension would alloc/init a new instance of the model, rather than trying to pass that across

  • e.g. You would Pass the URL, as a string, and the WatchKit extension would make NSURLConnection call.
1

In handleWatchKitExtensionRequest, make sure that you start a background task as specified in the documentation. This ensures that the main app on the iPhone is not suspended before it can send its reply.

Code in the app delegate of the main app on iPhone:

- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void ( ^)( NSDictionary * ))reply
{
   __block UIBackgroundTaskIdentifier watchKitHandler;
   watchKitHandler = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"backgroundTask"
                                                               expirationHandler:^{
                                                                 watchKitHandler = UIBackgroundTaskInvalid;
                                                               }];

   if ( [[userInfo objectForKey:@"request"] isEqualToString:@"getData"] )
   {
      // get data
      // ...
      reply( data );
   }

   dispatch_after( dispatch_time( DISPATCH_TIME_NOW, (int64_t)NSEC_PER_SEC * 1 ), dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
       [[UIApplication sharedApplication] endBackgroundTask:watchKitHandler];
    } );
}
Pang
  • 9,564
  • 146
  • 81
  • 122
John
  • 8,468
  • 5
  • 36
  • 61
0

You may want to try using NSKeyedArchiver on your reply dictionary. I had this issue because of serialization issues with the data and this helped.

rmp
  • 3,503
  • 1
  • 17
  • 26