WatchConnectivity file transfer not working - ios

I am using WatchConnectivity to transfer an image from iOS to Watch OS. When debugging in simulator I am facing a problem
The file is transferred successfully as I can see in (sender side i.e. iOS)
public func session(session: WCSession, didFinishFileTransfer fileTransfer: WCSessionFileTransfer, error: NSError?)
Now from XCode I stop iOS simulator, change target to Watch App, Ctrl+Run Watch App (just run, no build).The below method is called.
public func session(session: WCSession, didReceiveFile file: WCSessionFile)
At last I do
NSFileManager.defaultManager().moveItemAtURL(file.fileURL, toURL: destinationFileURL)
This call throws because there is no file at file.fileURL (which I checked in my MAC also).
The file.fileURL.path! is like this
/Users/<user name>/Library/Developer/CoreSimulator/Devices/DAD8E150-BAA7-43E0-BBDD-58FB0AA74E80/data/Containers/Data/PluginKitPlugin/2CB3D46B-DDB5-480C-ACF4-E529EFBA2657/Documents/Inbox/com.apple.watchconnectivity/979DC929-E1BA-4C24-8140-462EC0B0655C/Files/EC57EBB8-827E-487E-8F5A-A07BE80B3269/image
Any clues?
In actual I am transferring 15-20 images in loop.
Sometime when not debugging I noticed that few image (not all) show
up in watch simulator (also in actual watch). I have no idea what is
happening with WC.
No problem in transferring user info dictionary.

I found the problem. I was dispatching some code to main thread and the file move code was also inside that. WC framework clean up the file just after this method ends so the file must be moved before this function returns. I moved that code outside performInMainThread block and everything is working like charm.
public func session(session: WCSession, didReceiveFile file: WCSessionFile)
{
// Move file here
performInMainThread { () -> Void in
// Not here
}
}

As the Apple documentation of WCSessionDelegate Protocol Reference says regarding
- (void)session:(WCSession *)session didReceiveFile:(WCSessionFile *)file
when getting back the (WCSessionFile *)file parameter:
The object containing the URL of the file and any additional
information. If you want to keep the file referenced by this
parameter, you must move it synchronously to a new location during
your implementation of this method. If you do not move the file, the
system deletes it after this method returns.
So the best to move it ASAP to a new location. It's safe as the system keeps the reference and doesn't delete it during the move.
- (void)session:(WCSession *)session didReceiveFile:(WCSessionFile *)file {
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *cacheDir = [[NSHomeDirectory() stringByAppendingPathComponent:#"Library"] stringByAppendingPathComponent:#"Caches"];
NSURL *cacheDirURL = [NSURL fileURLWithPath:cacheDir];
if ([fileManager moveItemAtURL: file.fileURL toURL:cacheDirURL error: &error]) {
//Store reference to the new URL or do whatever you'd like to do with the file
NSData *data = [NSData dataWithContentsOfURL:cacheDirURL];
}
else {
//Handle the error
}
}
WARNING! You have to be careful with the thread handling as delegates of WCSession run in the background queue so you have to switch to main queue if you'd like to work with UI.

Related

WatchKit - Cannot transferFile in background "Watch App is not installed"

I am attempting to transfer an image file in the background using the WCSession method transferFile.
I am pulling photos from PHPhotoLibrary, (local photos). Then storing them in the caches directory to be sent via the transfer. The images are written successfully and saved to the location.
let manager = PhotoManager()
manager.requestPhotos { (error) in
if error == nil {
if let fileURLs = manager.findFilesInCache() {
for file in fileURLs {
print(file)
self.session.transferFile(file, metadata: nil)
}
}
}
}
}
Watch's side:
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
//Initialize the WCSession
if (WCSession.isSupported()) {
WCSession.defaultSession().delegate = self;
WCSession.defaultSession().activateSession()
}
}
func session(session: WCSession, didReceiveFile file: WCSessionFile) {
print (file.fileURL)
}
However, I continuously get this error from didFinishFileTransfer...
"Error Domain=WCErrorDomain Code=7006 \"Watch app is not installed.\" UserInfo={NSLocalizedRecoverySuggestion=Install the Watch app., NSLocalizedDescription=Watch app is not installed.}"
I have tried everything. Reinstalling the app, rebuilding the project. Reopening Xcode. Reconnecting my watch.... I'm going to try restating my computer.
Any ideas why I am getting this error?

iOS share extension does not receive data from Safari

I have an iOS share extension that needs the URL of the opened web page. Everything works good, especially in a simulator. But on a real device I have around 20-30% cases where the extension does not receive any data i.e.:
NSExtensionItem *inputItem = self.extensionContext.inputItems.firstObject;
NSItemProvider *item = inputItem.attachments.firstObject;
[item loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList options:nil completionHandler:^(NSDictionary *item, NSError *error) {
// here the error is sometimes not nil and thus the _baseURI ends up nil
_baseURI = [item[NSExtensionJavaScriptPreprocessingResultsKey] objectForKey:#"baseURI"];
}];
The error code is -100 with description "No item available for requested type identifier.". This happens mainly when I open the extension several times in a row without changing/refreshing the web page in the Safari.
In those situations I see a device log saying "iPhone kernel[0] : Sandbox: MobileSafari(7033) deny(1) file-read-data /private/var/containers/Bundle/Application/.../bundle.js" where the bundle.js is the javascript with the ExtensionPreprocessingJS object. The bundle.js declares the ExtensionPreprocessingJS object like this (extracted the relevant part):
ExtensionPreprocessingJS = {
run: function(arguments){
arguments.completionFunction({
"baseURI": document.baseURI
})
},
finalize: function(arguments){
}
}
In this situation, it could some time happen that when the extension is closed the next time opening the share dialog in Safari shows my extension with no icon. This happens on my testing iPhone 5s and iPhone 6 with iOS 9.3.
I think that the missing data is because of the system could not read the extension's JavaScript file, but why could this happen?
If you read the documentation for:
loadItemForTypeIdentifier(_:options:completionHandler:)
You'll see that:
The type information for the first parameter of your completionHandler
block should be set to the class of the expected type. For example,
when requesting text data, you might set the type of the first
parameter to NSString or NSAttributedString. An item provider can
perform simple type conversions of the data to the class you specify,
such as from NSURL to NSData or NSFileWrapper, or from NSData to
UIImage (in iOS) or NSImage (in OS X). If the data could not be
retrieved or coerced to the specified class, an error is passed to the
completion block’s.
Try this code to see what you recieve:
[item loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList options:nil completionHandler:^(id item, NSError *error) {
// here the error is sometimes not nil and thus the _baseURI ends up nil
_baseURI = [item[NSExtensionJavaScriptPreprocessingResultsKey] objectForKey:#"baseURI"];
}];
Note that item is not set to NSDictionary.

Data is not being transferred from iphone to iWatch (AppleWatch) on real devices

I have an iOS app and I programmed a extension for it on appleWatch.
I'm sending data (NSDictionary) to the appleWatch extension using transferUserInfo method. Everything works in the simulator but when I'm trying to run the application on real devices, it seems that the iWatch is not receiving anything though the iPhone is sending the data (I found this cause I debugged the sender side).
I have configured the WCSession on both sides. I have conformed the WCSessionDelegate in the class where I'm supposed to receive the data.
I'm using session:didReceiveUserInfo: method to receive the data in ExtensionDelegate but still like I said everything works fine in the simulator but nothing is being transferred on real devices.
Does anyone has a clue to what the problem is?
here's the code:
in the sender side:
inside my class MensaViewController.m
- (void)viewDidLoad
if ([WCSession isSupported]) {
session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
NSLog(#"A dish is being sent");
[session transferUserInfo:dishDictionary];
}
dishDictionary is declared inside viewDidLoad method and it contains data.
on the receiver side (Watch Extension)
I configure the WCSession and receive data in ExtensionDelegate.m like this:
- (void)applicationDidBecomeActive {
if ([WCSession isSupported]) {
session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
NSLog(#"Session activated in iWatch");
}
}
and I have this method to receive the data:
- (void)session:session didReceiveUserInfo:(NSDictionary<NSString *,id> *)userInfo{
NSLog(#"Received data from the iPhone");
NSArray<NSString*> *dictionaryKeys = userInfo.allKeys;
for(int i = 0; i < dictionaryKeys.count; i++){
Boolean equalsMensaString = [dictionaryKeys[i] isEqualToString:#"beilagen"];
if(equalsMensaString)
[self handleTransferredDish:userInfo];
Boolean equalsNewsString = [dictionaryKeys[i] isEqualToString:#"article"];
if(equalsNewsString)
[self handleTransferredNews:userInfo];
Boolean equalsEventString = [dictionaryKeys[i] isEqualToString:#"description"];
if(equalsEventString)
[self handleTransferredEvents:userInfo];
}
}
If you take a look at sources function description, you can see the description of the method you are using for transferring data:
public func transferUserInfo(userInfo: [String : AnyObject]) -> WCSessionUserInfoTransfer
The system will enqueue the user info dictionary and transfer it to
the counterpart app at an opportune time. The transfer of user info
will continue after the sending app has exited. The counterpart app
will receive a delegate callback on next launch if the file has
successfully arrived. The userInfo dictionary can only accept the
property list types.
So..There's no guarantee that the system will send this userInfo while you are actually using the App.
Use the following method instead:
public func sendMessage(message: [String : AnyObject], replyHandler: (([String : AnyObject]) -> Void)?, errorHandler: ((NSError) -> Void)?)
Clients can use this method to send messages to the counterpart app. Clients wishing to receive a reply to a particular
message should pass in a replyHandler block. If the message cannot be
sent or if the reply could not be received, the errorHandler block
will be invoked with an error. If both a replyHandler and an
errorHandler are specified, then exactly one of them will be invoked.
Messages can only be sent while the sending app is running. If the
sending app exits before the message is dispatched the send will fail.
If the counterpart app is not running the counterpart app will be
launched upon receiving the message (iOS counterpart app only). The
message dictionary can only accept the property list types.
and for receiving:
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void)
Notice That:
This send can fail if the counterpart app is not open or the Session is not paired and active. So, in the case you can not miss any data you should use updateApplicationContext or transferUserInfo (I actually prefer updateApplicationContext)
session.sendMessage(messageData, replyHandler: { (replyData) -> Void in
replyHandler?(replyData)
}, errorHandler: { (error) -> Void in
print("error: code:\(error.code) - \(error.localizedDescription)")
errorHandler?(error)
do {
try session.updateApplicationContext(messageData)
} catch {
print("There was an error trying to update watchkit app on the background")
}
})
And make sure you receive this cases with proper implementation of
func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject])
I know I'm too late to answer my own question. We were using Apple Watch 2 and IPhone 5 or 6 (don't remember after 2 years!), what happened was magical as everything started to work as the batterie of the apple watch was really low (in 5-10% range). We didn't have any problem even after charging the apple watch. End!

WatchOS2 WCSession sendMessage doesn't wake iPhone on background

This is being tested on both Simulator and real physical device iphone5s. I tried to use WCSession sendMessage to communicate from WatchOS2 extension to iPhone iOS9 code. It works well when iphone app is running either in the foreground and background mode.
But If I kill the iPhone app (not running app at all), then I always got errorHandler timeout. So Watch cannot communicate with iPhone anymore.
"Error Domain=WCErrorDomain Code=7012 "Message reply took too long."
UserInfo={NSLocalizedDescription=Message reply took too long.,
NSLocalizedFailureReason=Reply timeout occured.}".
I think it supposed to wake iPhone app in the background.
Any idea what to work around this problem or fix it? Thank you!
It is important that you activate the WCSession in your AppDelegate didFinishLaunchingWithOptions method. Also you have to set the WCSessionDelegate there. If you do it somewhere else, the code might not be executed when the system starts the killed app in the background.
Also, you are supposed to send the reply via the replyHandler. If you try to send someway else, the system waits for a reply that never comes. Hence the timeout error.
Here is an example that wakes up the app if it is killed:
In the WatchExtension:
Setup the session. Typically in your ExtensionDelegate:
func applicationDidFinishLaunching() {
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
And then send the message when you need something from the app:
if WCSession.defaultSession().reachable {
let messageDict = ["message": "hello iPhone!"]
WCSession.defaultSession().sendMessage(messageDict, replyHandler: { (replyDict) -> Void in
print(replyDict)
}, errorHandler: { (error) -> Void in
print(error)
}
}
In the iPhone App:
Same session setup, but this time also set the delegate:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
...
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
And then implement the delegate method to send the reply to the watch:
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
replyHandler(["message": "Hello Watch!"])
}
This works whenever there is a connection between the Watch and the iPhone. If the app is not running, the system starts it in the background.
I don't know if the system waits long enough until you received your data from iCloud, but this example definitely wakes up the app.
After hours of trying and hint from #jeron. I finally figured out the problem myself.
In my session:didReceiveMessage delegate method, I have two calls. 1.replyHandler call. 2. I have an async process running (RXPromise) in my case, It nested quite a few RXPromise callbacks to fetch various data from cloud service. I didn't pay attention to it, because it is supposed to call and return right away. But now that I commented out RXPromise block all together, it can wake up iOS app in the background every time.
Finally I figure out the trouble make is because after RXPromise call, it is not guaranty to be landed back to main thread anymore. And I believe session:didReceiveMessage has to be return on the main thread. I didn't see this mentioned anywhere on the Apple documentation.
Final solution:
- (void)session:(WCSession *)session
didReceiveMessage:(NSDictionary<NSString *, id> *)message
replyHandler:(void (^)(NSDictionary<NSString *, id> *_Nonnull))replyHandler {
replyHandler(#{ #"schedule" : #"OK" });
dispatch_async(dispatch_get_main_queue(), ^{
Nested RXPromise calls.....
});
}
Well, you can use transferUserInfo in order to queue the calls. Using sendMessage will result in errors when app is killed

How to call a method defined on the iPhone from the Apple Watch

Is there any way to call a defined method in a class on the iPhone from the Watchkit extension?
From my understanding currently one of the ways to communicate locally between Watch kit and the iPhone is by using NSUserDefaults, but are there other ways?
A simple example would be great.
You can add the class to both targets (main iOS app and WatchKit extention) and use methods in WatchKit Extention directly.
Conveniently add classes (preferable utilities or categories) with few dependencies.
If file is already added to project, you can delete it (remove reference) and add it once again with including to multiple targets.
For example, my category NSString+color in project works correctly in iOS app and Watch App.
Upd: You can also do it in right panel (Utilities). See the link bellow: cs621620.vk.me/v621620973/13f86/9XYDH1HL5BI.jpg
There are two main ways to 'communicate' between your WatchKit Extension and iOS application depending on the what you are trying to accomplish.
1. openParentApplication:reply:
This will open your iOS application in the background and allow you to perform logic from your iOS code and send a response back to your Extension. For example -
[InterfaceController openParentApplication:#{ #"command": #"foo" }
reply:^(NSDictionary *replyInfo, NSError *error) {
self.item = replyInfo[#"bar"];
}];
Checkout the Framework Reference -https://developer.apple.com/library/prerelease/ios/documentation/WatchKit/Reference/WKInterfaceController_class/index.html#//apple_ref/occ/clm/WKInterfaceController/openParentApplication:reply:
MMWormhole is a library that you can use to manage these communications
2. Shared Container
If you just need access to the same data that your iOS application has access to you can implement shared containers across both targets.
This could range from just using accessing a shared NSUserDefaults, as you've mentioned or all the way up to using Core Data and accessing a shared persistence stack across both iOS and WatchKit.
Programming Reference - https://developer.apple.com/library/ios/technotes/tn2408/_index.html
3. Singleton
Perhaps you just need to access shared logic from your WatchKit Extension but don't need the complexity of the above two options. As long as you're not persisting any data across both targets you could create a Singleton class that you can call from your Extension to perform the methods you need.
According to Apple's WatchKit Programming Guide you can communicate with the iOS app on the iPhone by using openParentApplication and pass a dictionary. The parent application handles this call via handleWatchKitExtensionsRequest in its AppDelegate. From there you can call other methods depending on the passed parameters.
handleWatchKitExtensionRequest then calls the reply method to pass back parameters to the WatchKit Extension:
Watchkit Extension:
// Call the parent application from Apple Watch
// values to pass
let parentValues = [
"value1" : "Test 1",
"value2" : "Test 2"
]
WKInterfaceController.openParentApplication(parentValues, reply: { (replyValues, error) -> Void in
println(replyValues["retVal1"])
println(replyValues["retVal2"])
})
iOS App:
// in AppDelegate.swift
func application(application: UIApplication!, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!, reply: (([NSObject : AnyObject]!) -> Void)!) {
// retrieved parameters from Apple Watch
println(userInfo["value1"])
println(userInfo["value2"])
// pass back values to Apple Watch
var retValues = Dictionary<String,String>()
retValues["retVal1"] = "return Test 1"
retValues["retVal2"] = "return Test 2"
reply(retValues)
}
Note that this seems to be broken in Xcode 6.2 Beta 3.
For WatchOS2 openParentApplication is deprecated. The replacement for it is WCSession in Watch Connectivity Framework
First, initialize WCSession in both watch(ExtensionDelegate)& iOS(AppDelegate) with following code
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
Send a notification from watch to Phone using
session.sendMessage(msg, replyHandler: { (responses) -> Void in
print(responses)
}) { (err) -> Void in
print(err)
}
Handle the message in AppDelegate using
func session(_ session: WCSession,
didReceiveMessage message: [String : AnyObject],
replyHandler replyHandler: ([String : AnyObject]) -> Void)
{
//do something according to the message dictionary
responseMessage = ["key" : "value"] //values for the replyHandler
replyHandler(responseMessage)
}

Resources