I'm working on the WatchKit Extension of my app, and have some issues with complications.
I have a complication that displays a given total amount, which depends of what the user is doing on the iOS app. When the WatchKit Extension is running, the iOS app updates the watch app context using the -[WCSession updateApplicationContext:] method. It works fine, and then in the ExtensionDelegate of my Watch app, I manually update the complication with the new data.
But this is OK only when the extension is running (if it's not, it won't get the application context until the next launch).
So I edited my code to send the complication data directly to the Watch when user changed something in the iOS app, using the -[WCSession transferCurrentComplicationUserInfo:] method (it's written in the documentation that the ExtensionDelegate should be woken up to receive the user info in background).
I've implemented the -session:didReceiveUserInfo: method of the ExtensionDelegate to update the complication when it received data from the iOS app, but it doesn't work when the extension is not running... (and I don't know if it ever receives the user info as I can't log it)
How should I do to keep my complications up to date even when the extension is not running??
Thanks
PS: I'm using the Watch Simulator, and to "close" the extension I just Reboot the Watch (from the Hardware menu)
Edit: I managed to log out statements when the app is not running (by opening the Watch Simulator system log), and I get these lines when the iOS send a new complication user data to the watch extension:
Oct 18 18:08:11 pc16 WatchApp Extension[26615]: Extension received
request to wake up for complication support.
Oct 18 18:08:11 pc16 assertiond[26585]: assertion failed: 15A284 13S343: assertiond + 15398 [B48FCADB-A071-3A46-878B-538DC0AFF60B]: 0x1
So the watch receives well the user info dictionary, but seems to fail waking up the extension...
Edit 2: here is the part of code in the ExtensionDelegate that should receive the complication user info (but which is not called when the app is not running):
- (void) session: (WCSession *)session didReceiveUserInfo: (NSDictionary *)userInfo
{
NSLog(#"session:didReceiveUserInfo: %#", userInfo);
NSString *userInfoType = userInfo[KHWASessionTransferUserInfoType];
NSDictionary *userInfoContents = userInfo[KHWASessionTransferUserInfoContents];
// Complication Data
if ([userInfoType isEqualToString:KHWASessionTransferUserInfoTypeComplicationData]) {
// Store the complication data into user defaults
[[NSUserDefaults standardUserDefaults] setValue:userInfoContents[KHWAComplicationTotalBalance] forKey:KHWAComplicationTotalBalance];
[[NSUserDefaults standardUserDefaults] synchronize];
// And refresh the complications
CLKComplicationServer *complicationServer = [CLKComplicationServer sharedInstance];
for (CLKComplication *complication in complicationServer.activeComplications) {
[complicationServer reloadTimelineForComplication:complication];
}
}
}
Edit 3: the WCSession is set in the extension delegate applicationDidFinishLaunching method:
- (void) applicationDidFinishLaunching
{
// Setup the WatchConnectivity session
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
[...]
}
Wow, I finally resolved the issue!
It seems that, even if I didn't see it in the log files (see my last comment), the init method of WCExtensionDelegate is well called when waking up the app.
So I just had to move the WCSession setting bloc into the init method :
- (id) init
{
if (self = [super init]) {
// Setup the WatchConnectivity session
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
return self;
}
And for the while it works fine...
Related
I have an app with multiple local notifications. When I try to clear all the delivered notifications, I call this removeAllDeliveredNotifications method. It's working fine till ios 11.1. In ios 11.2 and above, it doesn't work as expected. The notification still remains in the notification center. Could someone please help me out on this.
Thanks in advance.
It is still working for us. I just checked it on iOS 11.2.2.
I am using removeDeliveredNotificationsWithIdentifiers: inside getDeliveredNotificationsWithCompletionHandler:, calling getDeliveredNotificationsWithCompletionHandler on Main Thread.
- (void)removePendingNotificationsForObjectID:(SJFObjectID *)objectID {
__weak __typeof(self) weakSelf = self;
[self.userNotificationCenter getDeliveredNotificationsWithCompletionHandler:^(NSArray<UNNotification *> *notifications) {
__strong __typeof(weakSelf) self = weakSelf;
NSMutableArray <NSString *> *identifiersToRemove = [#[] mutableCopy];
for (UNNotification *notification in notifications) {
SJFObjectID *objectIDFromNotification = [self.notificationToObjectIDMarshaller marshalNotification:notification];
if ([objectID isEqual:objectIDFromNotification]) {
[identifiersToRemove addObject:notification.request.identifier];
}
}
[self.userNotificationCenter removeDeliveredNotificationsWithIdentifiers:identifiersToRemove];
}];
}
Though I experience strange behavior if I am debugging the completionHandler. If a pause too long (whatever that means) the completion handler will not finish (even on continue process execution) resulting in an unresponsive app. Maybe the completionhandler gets terminated.
This appears to be a bug in iOS that was fixed in iOS 11.3.
Did you try using this method:
removeDeliveredNotifications(withIdentifiers:)
You will need to pass an array of all the notification identifiers that you need to delete.
I'm trying to adapt my code from using only WCSessionDelegate callbacks in the foreground to accepting WKWatchConnectivityRefreshBackgroundTask via handleBackgroundTasks: in the background. The documentation states that background tasks may come in asynchronously and that one should not call setTaskCompleted until the WCSession's hasContentPending is NO.
If I put my watch app into the background and transferUserInfo: from an iPhone app, I am able to successfully receive my first WKWatchConnectivityRefreshBackgroundTask. However, hasContentPending is always YES, so I save away the task and simply return from my WCSessionDelegate method. If I transferUserInfo: again, hasContentPending is NO, but there is no WKWatchConnectivityRefreshBackgroundTask associated with this message. That is, subsequent transferUserInfo: do not trigger a call to handleBackgroundTask: –- they're simply handled by the WCSessionDelegate. Even if I immediately setTaskCompleted without checking hasContentPending, subsequent transferUserInfo: are handled by session:didReceiveUserInfo: without me needing to activate a WCSession again.
I'm not sure what to do here. There doesn't seem to be a way to force a WCSession to deactivate, and following the documentation about delaying setTaskCompleted seems to be getting me into trouble with the OS.
I've posted and documented a sample project illustrating my workflow on GitHub, pasting my WKExtensionDelegate code below. Am I making an incorrect choice or interpreting the documentation incorrectly somewhere along the line?
I've looked at the QuickSwitch 2.0 source code (after fixing the Swift 3 bugs, rdar://28503030), and their method simply doesn't seem to work (there's another SO thread about this). I've tried using KVO for WCSession's hasContentPending and activationState, but there's still never any WKWatchConnectivityRefreshBackgroundTask to complete, which makes sense to be given my current explanation of the issue.
#import "ExtensionDelegate.h"
#interface ExtensionDelegate()
#property (nonatomic, strong) WCSession *session;
#property (nonatomic, strong) NSMutableArray<WKWatchConnectivityRefreshBackgroundTask *> *watchConnectivityTasks;
#end
#implementation ExtensionDelegate
#pragma mark - Actions
- (void)handleBackgroundTasks:(NSSet<WKRefreshBackgroundTask *> *)backgroundTasks
{
NSLog(#"Watch app woke up for background task");
for (WKRefreshBackgroundTask *task in backgroundTasks) {
if ([task isKindOfClass:[WKWatchConnectivityRefreshBackgroundTask class]]) {
[self handleBackgroundWatchConnectivityTask:(WKWatchConnectivityRefreshBackgroundTask *)task];
} else {
NSLog(#"Handling an unsupported type of background task");
[task setTaskCompleted];
}
}
}
- (void)handleBackgroundWatchConnectivityTask:(WKWatchConnectivityRefreshBackgroundTask *)task
{
NSLog(#"Handling WatchConnectivity background task");
if (self.watchConnectivityTasks == nil)
self.watchConnectivityTasks = [NSMutableArray new];
[self.watchConnectivityTasks addObject:task];
if (self.session.activationState != WCSessionActivationStateActivated)
[self.session activateSession];
}
#pragma mark - Properties
- (WCSession *)session
{
NSAssert([WCSession isSupported], #"WatchConnectivity is not supported");
if (_session != nil)
return (_session);
_session = [WCSession defaultSession];
_session.delegate = self;
return (_session);
}
#pragma mark - WCSessionDelegate
- (void)session:(WCSession *)session activationDidCompleteWithState:(WCSessionActivationState)activationState error:(NSError *)error
{
switch(activationState) {
case WCSessionActivationStateActivated:
NSLog(#"WatchConnectivity session activation changed to \"activated\"");
break;
case WCSessionActivationStateInactive:
NSLog(#"WatchConnectivity session activation changed to \"inactive\"");
break;
case WCSessionActivationStateNotActivated:
NSLog(#"WatchConnectivity session activation changed to \"NOT activated\"");
break;
}
}
- (void)sessionWatchStateDidChange:(WCSession *)session
{
switch(session.activationState) {
case WCSessionActivationStateActivated:
NSLog(#"WatchConnectivity session activation changed to \"activated\"");
break;
case WCSessionActivationStateInactive:
NSLog(#"WatchConnectivity session activation changed to \"inactive\"");
break;
case WCSessionActivationStateNotActivated:
NSLog(#"WatchConnectivity session activation changed to \"NOT activated\"");
break;
}
}
- (void)session:(WCSession *)session didReceiveUserInfo:(NSDictionary<NSString *, id> *)userInfo
{
/*
* NOTE:
* Even if this method only sets the task to be completed, the default
* WatchConnectivity session delegate still picks up the message
* without another call to handleBackgroundTasks:
*/
NSLog(#"Received message with counter value = %#", userInfo[#"counter"]);
if (session.hasContentPending) {
NSLog(#"Task not completed. More content pending...");
} else {
NSLog(#"No pending content. Marking all tasks (%ld tasks) as complete.", (unsigned long)self.watchConnectivityTasks.count);
for (WKWatchConnectivityRefreshBackgroundTask *task in self.watchConnectivityTasks)
[task setTaskCompleted];
[self.watchConnectivityTasks removeAllObjects];
}
}
#end
From your description and my understanding it sounds like this is working correctly.
The way this was explained to me is that the new handleBackgroundTasks: on watchOS is intended to be a way for:
the system to communicate to the WatchKit extension why it is being launched/resumed in the background, and
a way for the WatchKit extension to let the system know when it has completed the work it wants to do and can therefore be terminated/suspended again.
This means that whenever an incoming WatchConnectivity payload is received on the Watch and your WatchKit extension is terminated or suspended you should expect one handleBackgroundTasks: callback letting you know why you are running in the background. This means you could receive 1 WKWatchConnectivityRefreshBackgroundTask but several WatchConnectivity callbacks (files, userInfos, applicationContext). The hasContentPending lets you know when your WCSession has delivered all of the initial, pending content (files, userInfos, applicationContext). At that point, you should call the setTaskCompleted on the WKWatchConnectivityRefreshBackgroundTask object.
Then you can expect that your WatchKit extension will soon thereafter be suspended or terminated unless you have received other handleBackgroundTasks: callbacks and therefore have other WK background task objects to complete.
I have found that when attaching to the processes with a debugger that OSs might not suspended them like they normally would, so it'd suggest inspecting the behavior here using logging if you want to be sure to avoid any of those kind of issues.
I am developing an apple watch extension for an already existing application.
My watch application has contact us section where customer can call toll free number.
My question is how can i start call in apple watch on click of button rather than keeping my application in foreground and starting the call.
Currently i am using this code to start call
+ (void)callWithNumberWithoutPrompt:(NSString *)phoneNo {
NSString *prefixedMobileNumber = [phoneNo hasPrefix:#"+"]?phoneNo:[NSString stringWithFormat:#"+%#",phoneNo];
NSString *phoneNumber = [#"tel://" stringByAppendingString:prefixedMobileNumber];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:phoneNumber]];
}
Note: this was true on WatchOS 1, and may have changed with the release of WatchOS 2.
From Ray Wenderlich WatchKit FAQ:
Can third-party apps make phone calls from a watch app?
No. There is no public API that lets you initiate a phone call directly from a WatchKit extension. Since the companion iPhone app can’t be brought to the foreground either, the system silently ignores all phone call or openURL: requests from the companion iPhone app.
Using WatchConnectivity in WatchOS2 you can send data from the watch app back to the parent app and then attempt to initiate the call from the parent app. Here is an example:
//App Delegate in iOS Parent App
#pragma mark Watch Kit Data Sharing
-(void)initializeWatchKit{
if ([WCSession isSupported]){
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
}
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message{
DLog(#"%#", message);
[self callRestaurantWithNumber:[NSString formattedPhoneNumber:[message valueForKey:#"phone_number"]]];
}
-(void)callRestaurantWithNumber:(NSString *)formattedPhoneNumber{
[[UIApplication sharedApplication]
openURL:[NSURL URLWithString:[NSString stringWithFormat:#"tel:%#",
formattedPhoneNumber]]];
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self initializeWatchKit];
return YES;
}
Now inside the watchKit extension you can send the data back to the parent application like this:
override func willActivate() {
super.willActivate()
if WCSession.isSupported() {
let defaultSession = WCSession.defaultSession()
defaultSession.delegate = self
defaultSession.activateSession()
if defaultSession.reachable == true {
let phoneNumberDict = [ "phone_number": "123-456-7890"]
defaultSession.sendMessage(phoneNumberDict, replyHandler: nil, errorHandler: { (error) -> Void in
print("THERE WAS AN ERROR SENDING DATA TO THE IOS APP: \(error.localizedDescription)")
})
}
}
}
However the one limitation I've encountered with this approach is the parent application needs to be open to actually receive the data and make the call. The documentation seems to state that the application will be opened in the background when you send a message from the watch to the ios parent app. However in my testing so far, on both real devices (watch and iphone) and simulators, the parent app only receives the data when the parent ios app is open and foregrounded. Even when the ios parent app is in a background state it still does not seem to initiate the call. I'm running this on watch os2 and iOS 9.0.2.
From another post that also appears to be running into the same problem.
Another watchKit SO post
Link to the Apple documentation too:
Apple Doc sendMessage:
I have registered for Calendar Change Notifications using the following:
- (void) registerForLocalCalendarChanges
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(localCalendarStoreChanged) name:EKEventStoreChangedNotification object:self.store ];
}
This should call the following when a change is made to the local calendar:
- (void) localCalendarStoreChanged
{
// This gets called when an event in store changes
// you have to go through the calendar to look for changes
// launch this in a thread of its own!
ashsysCalendarEventReporter *eventReport = [ashsysCalendarEventReporter new];
NSLog(#"Local Calendar Store Changed");
[NSThread detachNewThreadSelector:#selector(getCalendarEvents) toTarget:eventReport withObject:nil];
}
BUT...when I start the app, then send it to the background so I can change a calendar entry, nothing happens when I change the calendar entry. It DOES fire when I return to the app. But, of course that is not the objective.
store is defined in the header file with:
#property (strong,nonatomic) EKEventStore *store;
Update...forgot to show the stuff I have in the background fetch.
This is in didFinishLaunchingWithOptions
[application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
This is in the app delegate:
- (void) application:(UIApplication*) application performFetchWithCompletionHandler:(void (^) (UIBackgroundFetchResult))completionHandler
{
// UIBackgroundTaskIdentifier uploadCalInfo = [application beginBackgroundTaskWithExpirationHandler:nil];
NSLog(#"A fetch got called");
// ashsysCalendarEventReporter *eventReport = [ashsysCalendarEventReporter new];
// [eventReport getCalendarEvents];
// // [NSThread detachNewThreadSelector:#selector(getCalendarEvents) toTarget:eventReport withObject:nil];
// [application endBackgroundTask:uploadCalInfo];
completionHandler(UIBackgroundFetchResultNewData);
The performFetch gets called at what seem like random times some not at all related to the calendar. Is there a way to find out what is firing the background fetch? Is it always the calendar? The actual execution is commented out -- is it correct?
What am I missing?
I've been trying to find this answer out myself and I don't think there's an actual way to accomplish this. As per Apple's documentation, we're not allowed access to system resources while in a background state:
Stop using shared system resources before being suspended. Apps that interact with shared system resources such as the Address Book or calendar databases should stop using those resources before being suspended. Priority for such resources always goes to the foreground app. When your app is suspended, if it is found to be using a shared resource, the app is killed.
I'm still looking for a better answer/work around but would love to hear this from anyone else.
I am not sure if this is possible, but I need to grab all of the push notification userinfo when the user opens up the App. I can get all of the push notification userinfo when the App is opened or in the background, but not when the App is completely closed. Any way around this? The code below is how I get the userInfo currently.
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
id data = [userInfo objectForKey:#"data"];
NSLog(#"data%#",data);
}
Unfortunately, it's not currently possible client side with that method to query old notifications that have occurred while the app was completely closed. See this question: didReceiveRemoteNotification when in background.
A way around it is to keep track of which notifications you send from your server per user. When didReceiveRemoteNotification: is called, you can take that notification and compare it against the server's messages for the current user. If one of them matches, mark it some way on the server. That way, if there are messages sent when your app is backgrounded, you can query for messages that haven't been marked from the server and get all 'missed' notifications.
The method you are implementing cannot handle both cases. See the "Local and Push Notification Programming Guide":
If your app is frontmost, the application:didReceiveRemoteNotification: or application:didReceiveLocalNotification: method is called on its app delegate. If your app is not frontmost or not running, you handle the notifications by checking the options dictionary passed to the application:didFinishLaunchingWithOptions: of your app delegate...
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//Notifications
NSDictionary *userInfo = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
if(userInfo){
//open from notification message
}
return YES;
}
You can add this code to your AppDelegate's applicationWillEnterForeground method:
-(void)applicationWillEnterForeground:(UIApplication *)application {
// this method is called when staring an app that was closed / killed / never run before (after applicationDidFinishLaunchingWithOptions) and every time the app is reopened or change status from background to foreground (ex. returning from a mobile call or after the user switched to other app and then came back)
[[UNUserNotificationCenter currentNotificationCenter] getDeliveredNotificationsWithCompletionHandler:^(NSArray<UNNotification *> * _Nonnull notifications) {
NSLog(#"AppDelegate-getDeliveredNotificationsWithCompletionHandler there were %lu notifications in notification center", (unsigned long)[notifications count]);
for (UNNotification* notification in notifications) {
NSDictionary *userInfo = notification.request.content.userInfo;
if (userInfo) {
NSLog(#"Processed a notification in getDeliveredNotificationsWithCompletionHandler, with this info: %#", userInfo);
[self showPushNotificationInAlertController:userInfo]; // this is my method to display the notification in an UIAlertController
}
}
UIApplication.sharedApplication.applicationIconBadgeNumber = 0;
}];
}
}
Remove this line from the method application didFinishLaunchingWithOptions: if you had included it there, because it clears the badge number and also all notifications in notifications center:
UIApplication.sharedApplication.applicationIconBadgeNumber = 0;
This is currently working in iOS 12, hadn't had the chance to test it in earlier versions.