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?
Related
I have a fairly simple app that parses a RSS feed and shows it's content in a table view. It's available on the App Store. I have Crashlytics crash reporting integrated. I recently received two reports. These are a little difficult to decipher.
This has occurred in an iPhone 6 running iOS 10.2.1.
This is from an iPhone 5 running iOS 10.2.1.
Even though it says it's crashing due to privacy violations, I'm not accessing any services that requires permission in my app.
Also searching on com.apple.root.default-qos lead me to believe that this may have something to do with background threads. The only place where I use a background thread is to parse the RSS feed data.
DispatchQueue.global(qos: .background).async {
guard let data = try? Data(contentsOf: URL) else {
return
}
do {
let xmlDoc = try AEXMLDocument(xml: data)
if let items = xmlDoc.root["channel"]["item"].all {
self.posts.removeAll()
for item in items {
let title = item["title"].value ?? ""
// ...
self.posts.append(jobPost)
}
DispatchQueue.main.async {
self.saveposts(self.posts)
self.posts.sort { $0.publishDate > $1.publishDate }
self.tableView.reloadData()
UIApplication.shared.toggleNetworkActivityIndicator(show: false)
self.toggleUI(enable: true)
if self.refreshControl.isRefreshing { self.refreshControl.endRefreshing() }
}
}
} catch let error as NSError {
print("RSS parsing failed: \(error)")
self.showErrorAlert(error)
UIApplication.shared.toggleNetworkActivityIndicator(show: false)
self.toggleUI(enable: true)
if self.refreshControl.isRefreshing { self.refreshControl.endRefreshing() }
}
}
I tested this code on my iPhone 5 running iOS 9.3.5 and simulators running iOS 10.2 but no crash occurred.
Is there any other way to track down this problem?
I would double check all your permissions. In my case, starting with iOS10 you need permissions to save stuff to the user's camera roll. In my app, I was showing a default share sheet and whenever a user selected "save photo" the app crashed with one of these very not helpful error messages. I added
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Allow you to save charts and graphs from the app to your phone.</string>
to my info.plist, clean & run. And everything the problem was solved.
I have a UNNotificationServiceExtension written in Swift. All it does:
Set notification title
Set notification body
Load image & call contentHandler ()
Here's a shorted version of what am I doing:
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: #escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
self.bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent
bestAttemptContent!.title = GetNotificationTitle(userInfo)
bestAttemptContent!.body = GetNotificationBody(userInfo)
if let imageUrl = <get image url> {
let imageLoaderTask = URLSession.shared.dataTask(with: URL.init(string: imageUrl)!) { (newsImageData, newsImageUrl, newsImageError) -> Void in
if newsImageError == nil && newsImageData != nil {
let documentDirectory = self.GetDocumentDirectory()
if documentDirectory != nil {
let newsFileURL = documentDirectory?.appendingPathComponent("news_image").appendingPathExtension("png")
do {
try newsImageData?.write(to: newsFileURL!)
let attachment = try UNNotificationAttachment(identifier: "newsImage",
url: newsFileURL!,
options: nil)
self.bestAttemptContent?.attachments = [ attachment, ]
self.contentHandler!(self.bestAttemptContent!)
return
} catch {}
}
}
self.contentHandler!(self.bestAttemptContent!)
}
imageLoaderTask.resume()
} else {
self.contentHandler!(self.bestAttemptContent!)
}
}
In 95% cases - it works just fine. However, occasionally notification arrives without any changes (i.e. title, body remained the same, no image has been attached).
I know that:
It's not timeout: serviceExtensionTimeWillExpire is not called
It doesn't look like UNNotificationServiceExtension crashes: I've added plenty of NSLog() calls to check Device Logs - self.contentHandler!(self.bestAttemptContent!) fires
It happens more often on my iPhone rather than on iPad
I haven't found any single clue in Device Logs regarding the issue
Does anyone faced this issue? Any workarounds? Thoughts?
I built a UNNotificationServiceExtension when the feature was first announced. Two ideas:
The underlying URLSession data task is failing to fetch the remote media due to a system issue. Grab a sysdiagnose and look for errors in libnetwork.dylib.
The service extension is its own separate binary and process. It's possible that the system did not launch the process, or could not open up a link between your application process and the service extension. I'd also check a sysdaignose for anything that says the mach port of the process is nil.
I am trying to integrate Firebase into an iMessage extension.
As a test, I am setting up Firebase and trying to save a local file to Firebase Storage in the viewDidAppear method. The Firebase Real-time Database works fine in the code below, only the storage part does not.
The exact same code works when done in a normal app (i.e. not a iMessage extension).
I get the following error message:
Error Domain=FIRStorageErrorDomain Code=-13000
"An unknown error occurred, please check the server response."
UserInfo={ResponseErrorDomain=NSURLErrorDomain, object=test.jpg,
bucket=myapp.appspot.com, ResponseErrorCode=-995,
`NSLocalizedDescription=An unknown error occurred, please check the server response.
I am doing the following:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
FIRApp.configure()
FIRAuth.auth()?.signInAnonymously { (user, error) in
guard let fileURL = Bundle.main.url(forResource: "test", withExtension:"jpg") else { return }
let storageRef = FIRStorage.storage().reference().child("test.jpg")
storageRef.putFile(fileURL, metadata: nil) { (metaData, error) in //produces error
if error != nil {
print(error.debugDescription)
}
}
FIRDatabase.database().reference().updateChildValues(["someKey" : "someValue"]) // works fine
}
}
I have a suspicion that iMessage extensions may get limited access to the file system (since they live in a different sandbox than the normal app), and thus getting the file wouldn't work. In this case putData works, but putFile doesn't. Solution: always upload and download in memory (putData and dataWithMaxSize:) vs the file system (putFile and writeFile).
I'm trying to make iOS app to communicate with watch, but i get inconsistent behaviour all the time - either the communication is too slow, or none of the data gets transferred at all.
Besides, i don't see any "Phone disabled" screen when the watchKit runs (which causes a crash, because i need to get data from the phone first).
This is what i have in regards to establishing the WCSession in the iPhone app
App Delegate
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if NSClassFromString("WCSession") != nil {
if #available(iOS 9.0, *) {
if(WCSession.isSupported()){
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
if session.paired {
print("Watch connected")
} else {
print("No watch")
}
}
} else {
}}
if NSClassFromString("WCSession") != nil {
if(WCSession.isSupported()){
session.sendMessage(["b":"delegateSaysHi"], replyHandler: nil, errorHandler: nil)
}}
}
MainViewController
(viewDidLoad)
if NSClassFromString("WCSession") != nil {
if(WCSession.isSupported()){
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
if session.paired {
print("Watch connected")
} else {
print("No watch")
}
}}
MainViewController (Method for transferring bunch of data from iOS app to watchKit app)
func transferData(){
do {
let dataArray = ["somedata": array2d1]
try WCSession.defaultSession().updateApplicationContext(dataArray)
let dataArray1 = ["somedata1": array2d2]
try WCSession.defaultSession().updateApplicationContext(dataArray1)
let dataArray2 = ["somedata2": array2d3]
try WCSession.defaultSession().updateApplicationContext(dataArray2)
let dataArray3 = ["somedata3": array2d4]
try WCSession.defaultSession().updateApplicationContext(dataArray3)
// and up to 12
}
catch {
print("Something wrong happened")
}
}
And this is for watchKit app
App Delegate
func applicationDidFinishLaunching() {
if(WCSession.isSupported()){
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
}
}
func applicationDidBecomeActive() {
if(WCSession.isSupported()){
self.session.sendMessage(["b":"peek"], replyHandler: nil, errorHandler: nil)
}
InterfaceController (awakeWithContext)
if(WCSession.defaultSession().reachable){
self.session.sendMessage(["b":"peek"], replyHandler: nil, errorHandler: nil)
}
Method for receiving ApplicationContext data
func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
if let retrievedArray1 = applicationContext["somedata"] as? [[String]] {
self.watchAppArray = retrievedArray1
}
if let retrievedArray2 = applicationContext["somedata2"] as? [[String]] {
self.watchAppArray = retrievedArray1
// and so on for 12 arrays sent from phone
}
}
}}
Any advices on clearing out the situation are very welcome!
Thank you.
Multiple delegates/activations:
You're repeatedly setting up, delegating, and activating sessions in different parts of your app. You keep changing your delegate, so code in one part of the app will no longer be used after you delegated handling to a different part of your app.
You should use a single session/delegate throughout your app. One solution is to setup a WCSession singleton which would be available app-wide. Here's a guide which walks you through that process.
Only the most recent application context would get sent:
By trying to queue up multiple application context requests, the earlier ones would no longer be in the queue when the system gets around to transmitting it, as the system would have already replaced the preceding context with the later one. So only the last (dataArray3) would ever get transmitted.
Use the updateApplicationContext:error: method to communicate recent state information to the counterpart. When the counterpart wakes, it can use this information to update its own state. ... This method overwrites the previous data dictionary, so use this method when your app needs only the most recent data values.
If all of the arrays represent the recent state of your application, you want to transmit them together in a single dictionary.
var dataArray = [String: AnyObject]()
dataArray["somedata"] = array2d1
dataArray["somedata1"] = array2d2
dataArray["somedata2"] = array2d3
dataArray["somedata3"] = array2d4
do {
try session.updateApplicationContext(dataArray)
}
catch {
print(error)
}
It may also help to add some error handling to your sendMessage code, as the paired device may not always be reachable.
Slow communication:
As for the communication being too slow, there are two issues at hand.
Transfers may not happen immediately.
When only one session is active, the active session may still send updates and transfer files, but those transfers happen opportunistically in the background.
Remember that background transfers are not be delivered immediately. The system sends data as quickly as possible but transfers are not instantaneous, and the system may delay transfers slightly to improve power usage. Also, sending a large data file requires a commensurate amount of time to transmit the data to the other device and process it on the receiving side.
The more data you send, the longer it takes to transmit/receive it all.
When sending messages, send only the data that your app needs. All transfers involve sending data wireless to the counterpart app, which consumes power. Rather than sending all of your data every time, send only the items that have changed.
You can control how much data you send, as well as whether the data is sent interactively or in the background. If the watch is reachable, you could use sendMessage for immediate communication. If it's not reachable, you could fall back on a background method.
How can I check from watchOS 2 if application on iPhone is opened or not?
I want to send a message with NSUserDefaults from watch to iPhone via sendMessage (to be able to update interface on phone when message received) when both applications are running and I want to send NSUserDefaults even if only watchOS 2 app is running.
From what I read I found this:
/** The counterpart app must be reachable for a send message to succeed. */
#property (nonatomic, readonly, getter=isReachable) BOOL reachable;
It's always reachable from what I check.
Reachable means the apple watch and iPhone are connected via bluetooth or wifi. It doesn't necessarily mean the iPhone app is running. If reachable is true, when you try to sendMessage from the apple watch it will launch the iPhone app in the background. You need to assign the WKSession delegate as soon as possible because the delegates methods (sendMessage) will fire soon. I think what you are saying you want to do is call sendMessage if you can, and it not use the transferUserInfo method instead. To do this, first on your apple watch:
func applicationDidFinishLaunching() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
// NOTE: This should be your custom message dictionary
// You don't necessarily call the following code in
// applicationDidFinishLaunching, but it is here for
// the simplicity of the example. Call this when you want to send a message.
let message = [String:AnyObject]()
// To send your message.
// You could check reachable here, but it could change between reading the
// value and sending the message. Instead just try to send the data and if it
// fails queue it to be sent when the connection is re-established.
session.sendMessage(message, replyHandler: { (response) -> Void in
// iOS app got the message successfully
}, errorHandler: { (error) -> Void in
// iOS app failed to get message. Send it in the background
session.transferUserInfo(message)
})
}
Then, in your iOS app:
// Do this here so it is setup as early as possible so
// we don't miss any delegate method calls
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
self.watchKitSetup()
return true
}
func watchKitSetup() {
// Stop if watch connectivity is not supported (such as on iPad)
if (WCSession.isSupported()) {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
// Handle the message from the apple watch...
dispatch_async(dispatch_get_main_queue()) {
// Update UI on the main thread if necessary
}
}
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
// Handle the message from the apple watch...
dispatch_async(dispatch_get_main_queue()) {
// Update UI on the main thread if necessary
}
}
You probably want to use the application context of WatchConnectivity:
have a look at WCSession.updateApplicationContext( )
It sends the most important configuration info to the counterpart as soon as the counterpart is reachable, even if the counterpart is not reachable at the time of sending. If you call updateApplicationContext multiple times, only the latest is sent.
For much deeper info watch the WWDC 2015 session about WatchConnectivity: https://developer.apple.com/videos/wwdc/2015/?id=713
It describes more means to send data, but I think the application context fits best for you.
The session also details how to find out if the counterpart is reachable, but I think you don't need that for your use case.