I have an iPhone app that performs some computations. It works well. Now I'd like to create a WatchKit extension and target in order to control the start of the computations and display the results once the computations are finished as well as update the progress (e.g. percentage completed when Watch app is displayed, and send a notification from the iPhone app to the iWatch when the progress reaches a significant percentage mark).
I have found some tutorials that explain to use NSUserDefaults and initWithSuitName providing an app group and then add the data object to synchronise. I could use KVO to display a percentage label on the Watch.
As I am exploring the possible architectures here, are you aware of some alternative methods to achieve this? Is using app group the method that Apple recommends?
You can use open app to launch your computation. then you can send information with handoff.
You can follow this steps
After setting Watch kit session and creating data dictionary you can use send message with this
[[WCSession defaultSession] sendMessage:<Your Dictionary> replyHandler:^(NSDictionary<NSString *,id> * _Nonnull replyMessage) {
//You task on completion
} errorHandler:^(NSError * _Nonnull error) {
if (error)
{
//Handle the error
}
}];
at watch extension side
You can set delegate method
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message replyHandler:(void(^)(NSDictionary<NSString *, id> *replyMessage))replyHandler
{
//Handle the received data
}
And you are done
App groups no longer work above watchOS 1. You need to use WatchConnectivity framework. Once you get the values in watch you can use Reactive or Rx to update the values instead of KVO.
Helpful Links:
https://developer.apple.com/documentation/watchconnectivity
https://kristina.io/watchos-2-how-to-communicate-between-devices-using-watch-connectivity/
https://www.natashatherobot.com/watchconnectivity-application-context/
Related
Our project uses WebRTC for VOIP calls and it works fine before accessing the CallKit framework. But when I tried to access the CallKit framework, there was a situation where neither side could hear each other's speech. When I removed CallKit, everything returned to normal.
CallKit's answer button is the same function as the original answer button in the project.
And what amazed me was that it was not necessary to hear no sound. Sometimes everything is normal, but sometimes there will be problems. Well, the probability of a problem is greater.
I found the following flowchart, I suspect the problem lies in the order of function calls. But I do not know how WebRTC corresponds to the functions in the diagram.
In addition, I am curious whether socket instability will cause the CallKit framework to work abnormally
Please forgive me English is not good, but this problem has been haunted me for several days, I do not know where exactly a problem, is not where the conflict with the CallKit framework?
Hope you can help me, thank you very much!
Few steps need to be done to connect webrtc and callkit in proper way:
First of all, you have to use the RTCAudioTrack and add the RTCAudioSession for handling the audio. Old legacy RTCAudioSession added directly into RTCPeerConnection works but it's not prefered a way to do that.
Second thing is to use manualAudio. When app is booted you should change useManualAudio flag on RTCAudioSession:
RTCAudioSession.sharedSession().useManualAudio = true
which gives you possibility to postpone the audio until CallKit informs that audio session was activated, so inside the ProviderDelegate you should implement following method:
(void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession
RTCAudioSession.sharedSession().didActivecated(audioSession)
RTCAudioSession.sharedSession().isAudioEnabled = true
and for second audio delegate method don't forget to add:
(void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession
RTCAudioSession.sharedSession().didDeactivecated(audioSession)
RTCAudioSession.sharedSession().isAudioEnabled = false
Apple suggests us to wait till the Connection gets established and then fulfill the performAnswerAction. Below is the source
Apple Suggestion for Call Kit Documentation
If the recipient of a call answers before the app establishes a connection to your server, don't fulfill the CXAnswerCallAction object sent to the provider:performAnswerCallAction: method of your delegate immediately. Instead, wait until you establish a connection and then fulfill the object. While it waits for your app to fulfill the request, the incoming call interface lets the user know that the call is connecting, but not yet ready.
So we need to wait for a second or two before we fulfill the action in performAnswerCallAction
In the end, I solved the problem, but I still do not understand why it can be solved.Below is my solution:
First of all, I delay the call of "fulfill" by 1 second (note that this time can not be less than 1 second)
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
if (self.delegate && [self.delegate respondsToSelector:#selector(callKitManager:refreshCurrentCallStatus:)]) {
[self.delegate callKitManager:self refreshCurrentCallStatus:EUCCallKitStatusAnswerAccept];
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[action fulfill];
});}
Second, I also delayed my network request call by one second (here longer than the previous one)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.peerConnection offerForConstraints:[self offerConstraintsRestartIce:NO] completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
[self peerConnection:self.peerConnection didCreateSessionDescription:sdp error:error];
}];
});
In this way, my problem is solved.
If you know why this can solve this problem, please comment on me, thank you!
For people wanting to reply quickly without reading the post: I am not hitting any memory limits. Read the whole post for details.
My WatchKit extension cannot properly function without the user first being "onboarded" through the phone app. Onboarding is where the user must accept the permissions that we require, so it's very crucial.
On my WatchKit extension, I wanted to display a simple warning for users who had not finished onboarding within our phone app yet.
As such, I thought I'd get the status of onboarding from the phone in two ways:
When the user opens the app/the app is activated (I use the willActivate method to detect this)
When the app finishes onboarding it sends a message to the watch of its completion (if the extension is reachable, of course)
Both of these combined would ensure that the status of onboarding is always kept in sync with the watch.
I wrote the first possibility in, utilizing reply handlers to exchange the information. It worked just fine, without any troubles. The warning telling the user to complete disappears, the extension does not crash, and all is well.
I then wrote in the second possibility, of the extension being reachable when the user finishes onboarding (with the phone then directly sending the companion the new status of onboarding). My extension crashes when it receives this message, and I am stuck with this odd error.
Program ended with exit code: 0
My extension does not even get a chance to handle the new onboarding status, the extension just quits and the above error is given to me.
I am not hitting any sort of memory limit. I have read the technical Q&A which describes what a memory usage limit error looks like, and I don't receive any sort of output like that whatsoever. As well, before the extension should receive the message, this is what my memory consumption looks like.
I have monitored the memory consumption of the extension right after finishing onboarding, and I see not a single spike indicating that I've gone over any kind of threshold.
I have tried going line by line over the code which manages the onboarding error, and I cannot find a single reason that it would crash with this error. Especially since the reply handler method of fetching the onboarding status works so reliably.
Here is the code of how I'm sending the message to the watch.
- (void)sendOnboardingStatusToWatch {
if(self.connected){
[self.session sendMessage:#{
LMAppleWatchCommunicationKey: LMAppleWatchCommunicationKeyOnboardingComplete,
LMAppleWatchCommunicationKeyOnboardingComplete: #(LMMusicPlayer.onboardingComplete)
}
replyHandler:nil
errorHandler:^(NSError * _Nonnull error) {
NSLog(#"Error sending onboarding status: %#", error);
}];
}
}
(All LMAppleWatchCommunicationKeys are simply #define'd keys with exactly their key as the string value. ie. #define LMAppleWatchCommunicationKey #"LMAppleWatchCommunicationKey")
Even though it's never called by the extension, here is the exact receiving code of the extension which handles the incoming data, if it helps.
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message {
NSString *key = [message objectForKey:LMAppleWatchCommunicationKey];
if([key isEqualToString:LMAppleWatchCommunicationKeyOnboardingComplete]){
BOOL newOnboardingStatus = [message objectForKey:LMAppleWatchCommunicationKeyOnboardingComplete];
[[NSUserDefaults standardUserDefaults] setBool:newOnboardingStatus
forKey:LMAppleWatchCommunicationKeyOnboardingComplete];
dispatch_async(dispatch_get_main_queue(), ^{
for(id<LMWCompanionBridgeDelegate> delegate in self.delegates){
if([delegate respondsToSelector:#selector(onboardingCompleteStatusChanged:)]){
[delegate onboardingCompleteStatusChanged:newOnboardingStatus];
}
}
});
}
}
Before including this onboarding-related code, my WatchKit extension was tested by over 100 people, without any troubles. I am using the exact same custom error dialogue that I was using before, just with a different string. I cannot for the life of me figure out what is causing this crash, and the ambiguity of it has given me very little to work with.
Any help would be greatly appreciated. Thank you very much for taking your time to read my post.
Edit: I just tried creating a symbolic breakpoint for exit(), which is never hit. If I call exit() myself, it calls the breakpoint, so I know the breakpoint itself is working.
I am working on the iPhone app in Objective-C in which I count steps of user by using CMAccelerometerData.
It works fine when app is running in background ,
But when I kill my app (double-clicking the Home button and then swiping app away) then I can not get the steps count.
The feature of app is that fetch steps count if my app is in kill state (double-clicking the Home button and then swiping app away) and I run it from icon.
Is there any way to get steps count if our app is killed (double-clicking the Home button and then swiping app away) ?
Here is the method to get steps
- (void)startDetectionWithUpdateBlock:(void (^)(NSError *))callback
{
if (self.motionManager.isAccelerometerActive) {
return;
}
[self.motionManager startAccelerometerUpdatesToQueue:self.queue withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {
if (error) {
if (callback) {
dispatch_async(dispatch_get_main_queue(), ^{
callback (error);
});
}
return ;
}
CMAcceleration acceleration = accelerometerData.acceleration;
CGFloat strength = 1.2f;
BOOL isStep = NO;
if (fabs(acceleration.x) > strength || fabs(acceleration.y) > strength || fabs(acceleration.z) > strength) {
isStep = YES;
}
if (isStep) {
if (callback) {
dispatch_async(dispatch_get_main_queue(), ^{
callback (nil);
});
}
}
}];
}
And through this way I update steps count :
[[SOLocationManager sharedInstance] start];
[[SOStepDetector sharedInstance] startDetectionWithUpdateBlock:^(NSError *error) {
if (error) {
NSLog(#"%#", error.localizedDescription);
return;
}
self.actualCheckpoint.stepsCount ++;
NSLog(#"Total Steps: %ld",(long)self.actualCheckpoint.stepsCount);
[[NSNotificationCenter defaultCenter] postNotificationName:SBCompetitionStepsCountDidChange object:self.actualCheckpoint];
}];
you can catch the data in appDelegate's
- (void)applicationWillTerminate:(UIApplication *)application {}
You can't access HealthKit data in the background when the device is locked, according to Apple:
For security, the HealthKit store is encrypted when the device is locked. The HealthKit store can only be accessed by an authorized app. As a result, you may not be able to read data from the store when your app is launched in the background; however, apps can still write data to the store, even when the phone is locked. HealthKit temporarily caches the data and saves it to the encrypted store as soon as the phone is unlocked.
https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HealthKit_Framework/
You'll need to read the data on startup to 'catch up' from the last time your app was run.
If the app is killed by the user by swiping up, rather than by simply putting the app into the background, it is no longer allows to execute any code until the user opens it again. However, there's no way to get direct access to the accelerometer if the app is completely killed.
There are ways to handle keeping the app open indefinitely if it goes into the background (not manually killed by the user). However, having the accelerometer powered up constantly would be a significant battery drain, and if all you want to do it track steps, you can simply use the HealthKit API. (If you need your app to work on old devices, you will have to resort to background processing.)
HealthKit
As an Apple OS-level API, HealthKit is always working and (if the user's device supports it and the user hasn't disabled it) is constantly tracking steps in the background, using Apple's accurate, battery efficient algorithm (it uses the device's motion coprocessor).
All you need to do is integrate your app with HealthKit such that it can read the device's Steps metric. Take a look at this objective-C tutorial.
What I want to achieve is the following:
Complication(s) get updated in the background at intervals of 30
minutes
Complication(s) get updated whenever the watch app runs and
receives its own updated data
Complication(s) get updated whenever
the iOS app runs and the user changes a setting which affects the
watch data (such as changing location of weather observations, or
display units)
Items 1. and 2. seem to be straightforward, and nicely addressed here: What is the flow for updating complication data for Apple Watch?
However, for item 3, in the iOS app, I set up a WCSession instance and call transferCurrentComplicationUserInfo, sending the new settings as NSDictionary. In the watch extension, this invokes didReceiveUserInfo in WCSessionDelegate.
- (void)session:(WCSession *)session didReceiveUserInfo:(NSDictionary<NSString *,id> *)userInfo {
// Code here to apply the new settings
// ....
// Invoke a NSUSRLSession-based web query to get new data
[self queryForNewDataWithCompletionHandler:^(NCUpdateResult result) {
if (result == NCUpdateResultNewData) {
// Have new data from web to display
CLKComplicationServer *server = [CLKComplicationServer sharedInstance];
for (CLKComplication *complication in server.activeComplications) {
[server reloadTimelineForComplication:complication];
}
}
// Set date for next complication update to 30 mins from now
// ...
}];
}
The problem I am having is that watchOS is calling requestedUpdateDidBegin in a separate thread, shortly after it invoked didReceiveUserInfo and this starts executing BEFORE I have a chance to obtain updated data using the new settings in the newly received UserInfo dictionary from the app.
Consequently, the complications get updated twice in short succession - once by the WatchOS having called requestedUpdateDidBegin, which simply re-updates the complication with existing (stale) data, before I very soon after receive new data from the web and then have to update them again in my own code.
This seems unnecessary and a waste of resources, not to mention the limited budget of updates that Apple allows (supposedly 2 per hour).
Am I doing anything wrong here? How can I prevent watchOS2 from calling requestedUpdateDidBegin before I have had a chance to acquire new data from the web?
The purpose of transferCurrentComplicationUserInfo is to immediately pass current complication data to the extension. In your code, you are passing settings, however you are not including any weather data.
The issue you're seeing stems from trying to asynchronously fetch new data within the extension (which is returning before the data is available).
To handle this, you should fetch the current weather data on the phone based on the new settings, then pass (the new settings along with) the weather data in the current complication user info.
- (void)session:(WCSession *)session didReceiveUserInfo:(NSDictionary<NSString *,id> *)userInfo {
// Code here to apply the new settings for future updates
// ....
// Code here to update cache/backing store with current weather data just passed to us
// ....
CLKComplicationServer *server = [CLKComplicationServer sharedInstance];
for (CLKComplication *complication in server.activeComplications) {
[server reloadTimelineForComplication:complication];
}
}
This way, the complication server can immediately update the timeline using the current complication data you just transferred to the watch.
No stale data, no unnecessary second update.
Our app lets a user select records on iPhone that they want to be displayed in the watch app.
It works like this:
The user taps "Add to watch" on a record from their iPhone
A new version of the watch database is generated and sent to the watch
The watch app receives and saves the file and updates its interface
A new database file is sent to the watch and processed for each change. This is fine if the watch is awake since it will give the user live updates, but if the watch is asleep while the user makes 7 changes, it means the watch is accepting and processing 7 new files as soon as it wakes up.
We really only care about the most recent version of the watch database, so I'm trying to cancel all old outstanding file transfers.
Code:
On iPhone, each time a record is added/removed from watch database, we attempt (unsuccessfully) to cancel pending file transfers and then queue the latest database file:
// create watch database and store it at self.urlToDatabase
[self generateNewWatchDatabase];
if ([WCSession isSupported])
{
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
// this is the problem - cancel doesn't seem to do anything
for (WCSessionFileTransfer *fileTransfer in session.outstandingFileTransfers)
[fileTransfer cancel];
[session transferFile:self.urlToDatabase metadata:nil];
}
In the above code, calling [fileTransfer cancel] successfully removes the WCSessionFileTransfer object from session.outstandingFileTransfers, but didReceiveFile is still being called multiple times below.
Accepting the file on the watch:
- (void)session:(WCSession *)session didReceiveFile:(WCSessionFile *)file
{
// this method gets called once for every time -transferFile:metadata: is called above,
// even after cancelling outstanding file transfers
[self replaceDatabaseWithFile:file];
[self refreshItemsTable];
}
How do we cancel outstanding file transfers?
Edit
As per #ccjensen's recommendation, I tried the following in the method that fires when the user adds/removes a record to/from the watch:
// store a reference to the file transfer and immediately cancel it
WCSessionFileTransfer *transfer = [session transferFile:self.urlToDatabase metadata:nil];
[transfer cancel];
This still results in the file being sent to the watch, instead of cancelling it as one would expect.
I also tried the following:
Kill watch app (by holding the side button until 'Power Off' appears, and then holding it again)
Add/remove records from iPhone
Relaunch watch app
Even in this scenario the watch receives all 'cancelled' file transfers.
The documentation for the cancel method says:
Use this method to cancel a file transfer before it completes. If the file has already been transferred, calling this method has no effect.
So it sounds like the cancels are "best effort" and might not end up being able to cancel them in all cases, especially if the file has already been transferred.
Are you seeing it never work, even if you call cancel immediately (try testing without the watch app running as that seems to expedite the transfers)?
turned out the reason this worked was that the file url did not match the transfer's url I was checking 🙈
I recently found that keeping hold of the WCSessionFileTransfer in my own array and canceling them proved more reliable than using [WCSession defaultSession].outstandingFileTransfers.
NSMutableArray<WCSessionFileTransfer*>* inProgressTransfers = [NSMutableArray array];
So each time you call TransfeFile: metaData:
WCSessionFileTransfer* transfer = [[WCSession defaultSession] transferFile:url metadata:metadata];
if (transfer)
{
[self.inProgressTransfers addObject:transfer];
}
then at an appropriate time
for (WCSessionFileTransfer* ourTransfer in self.inProgressTransfers)
{
[ourTransfer cancel];
[self.inProgressTransfers removeObject:ourTransfer];
}
For some reason, keeping hold of the transfers ourself makes calling cancel work much more reliably.. hope that helps someone