Very slow communication between apple watch and iPhone - swift

I have wrote a very basic Xcode project that contains 3 targets:
- iOS target
- WatchKit app
- WatchKit extension
First of all, i do not understand why Xcode creates a second target (extension) for WatchKit app ? It seems that WatchKit app contains storyboard, and WatchKit extension contains swift code (controllers). Is there a particular reason for Xcode to design and split 2 targets instead of one single ?
Look at this very basic piece of code:
iOS controller:
override func viewDidLoad()
{
super.viewDidLoad()
if WCSession.isSupported()
{
let session = WCSession.default()
session.delegate = self
session.activate()
}
}
#IBAction func on_btn_tap(_ sender: Any)
{
if WCSession.isSupported()
{
let session = WCSession.default()
session.sendMessage(["mykey": "myvalue"], replyHandler: { (response) -> Void in
NSLog("OK")
}, errorHandler: { (error) -> Void in
NSLog("Error)
})
}
}
On watch extension (InterfaceController.swift):
override func awake(withContext context: Any?)
{
super.awake(withContext: context)
if WCSession.isSupported()
{
let session = WCSession.default()
session.delegate = self
session.activate()
}
}
extension InterfaceController: WCSessionDelegate
{
func session(_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?)
{
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void)
{
self.btn.setBackgroundColor(UIColor.yellow)
}
}
As you certainly understand, i have a button on my iOS App. When i tap on this button, i send a message to Watch App and this app will change a button color.
There is a delay of about 5-6 seconds between the button tap on the iPhone and the color change. Do you know why ?
In the other communication side (watch to iPhone), it is worst (10-15 seconds)
Thanks

Since you are updating your UI you need to wrap it in a DispatchQueue, like this:
DispatchQueue.main.async {
self.btn.setBackgroundColor(UIColor.yellow)
}
These delegate callbacks are not on the main thread and you should never update your UI from any other thread than the main thread. Wrapping it like this results in much faster updating of your UI and safer code.

Related

WatchOS Complication not updating automatically

I'm trying to get my watchOS complication to automatically update every few minutes. In the ComplicationCOntroller.swift, I setup the complication in the getCurrentTimelineEntry() function, and it works the first time, but only updates the value displayed in the complication if I launch the WatchOS app. Can I set my complication to automatically update in the background? Even if it's a set time interval, e.g. "update in 3 minutes". Here's my ExtensionDelegate:
import WatchKit
import WatchConnectivity
class ExtensionDelegate: NSObject, WKExtensionDelegate, WCSessionDelegate {
let session = WCSession.default
let defaults = UserDefaults.standard
let server = CLKComplicationServer.sharedInstance()
override init() {
super.init()
WKInterfaceDevice.current().isBatteryMonitoringEnabled = true
session.delegate = self
session.activate()
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("Session started on \(WKInterfaceDevice.current().name)")
defaults.set("32", forKey: "PhoneBatteryLevel")
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
print("Received application context: \(applicationContext["PhoneBatteryLevel"]!)")
defaults.set(applicationContext["PhoneBatteryLevel"], forKey: "PhoneBatteryLevel")
for complication in server.activeComplications! {
server.reloadTimeline(for: complication)
print("Reloading complications...")
}
}
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
}
func applicationDidBecomeActive() {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillResignActive() {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, etc.
}
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
// Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
for task in backgroundTasks {
// Use a switch statement to check the task type
switch task {
case let backgroundTask as WKApplicationRefreshBackgroundTask:
// Be sure to complete the background task once you’re done.
backgroundTask.setTaskCompletedWithSnapshot(false)
case let snapshotTask as WKSnapshotRefreshBackgroundTask:
// Snapshot tasks have a unique completion call, make sure to set your expiration date
snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
// Be sure to complete the connectivity task once you’re done.
connectivityTask.setTaskCompletedWithSnapshot(false)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once you’re done.
urlSessionTask.setTaskCompletedWithSnapshot(false)
case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask:
// Be sure to complete the relevant-shortcut task once you're done.
relevantShortcutTask.setTaskCompletedWithSnapshot(false)
case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask:
// Be sure to complete the intent-did-run task once you're done.
intentDidRunTask.setTaskCompletedWithSnapshot(false)
default:
// make sure to complete unhandled task types
task.setTaskCompletedWithSnapshot(false)
}
}
}
}
And the relevant ComplicationController code:
func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: #escaping (CLKComplicationTimelineEntry?) -> Void) {
switch complication.family {
case .modularSmall:
let template = CLKComplicationTemplateModularSmallSimpleText()
template.textProvider = CLKSimpleTextProvider(text: "\(defaults.integer(forKey: "PhoneBatteryLevel"))")
template.tintColor = UIColor.yellow
let entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
handler(entry)
case .modularLarge:
let template = CLKComplicationTemplateModularLargeStandardBody()
template.headerTextProvider = CLKSimpleTextProvider(text: "Battery Levels")
template.headerTextProvider.tintColor = UIColor.green
template.body1TextProvider = CLKSimpleTextProvider(text: "iPhone: \(defaults.integer(forKey: "PhoneBatteryLevel"))%")
template.body2TextProvider = CLKSimpleTextProvider(text: "Watch: \(getWatchBatteryLevel())%")
let entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
handler(entry)
default:
return
}
}

Sending csv file from Apple Watch to Phone - nothing happening

I have this function in the Watchkit Extension's InterfaceController...
func createAndSendCSV(data: Array<String>) {
var csvText = "time, data\n"
let fileName = "\(startTime).csv"
let path = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
for row in data {
csvText += row
}
do {
try csvText.write(to: path!, atomically: true, encoding: String.Encoding.utf8)
WCSession.default.transferFile(path!, metadata: ["time": startTime])
} catch {
print("Failed to create file")
print("\(error)")
}
}
that is called when the user stops the app on the watch. I know the function is triggered because I can add a print statement in the do catch showing that it was successful.
On ViewController I have the below which isn't much beyond making sure the session is activated and then trying to work with the file that was sent. I don't see any errors but I also don't see my print test message.
I'm trying to use a CSV file to transfer data as I can't send via sendMessage due to size of message. Anyone know why it might not be working?
import UIKit
import WatchConnectivity
class ViewController: UIViewController, WCSessionDelegate {
override func viewDidLoad() {
super.viewDidLoad()
if (WCSession.isSupported()) {
let session = WCSession.default
session.delegate = self
session.activate()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func session(_ session: WCSession, didReceive file: WCSessionFile) {
DispatchQueue.main.async() {
do {
print("test")
let contents = try String(contentsOf: file.fileURL, encoding: .utf8)
self.sendData(data: contents)
} catch {
print("File Read Error for file \(String(describing: file.metadata))")
}
}
}
func sendData(data: String) {
print(data)
}
// Not used but needs to exist
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
}
func sessionDidBecomeInactive(_ session: WCSession) {
}
func sessionDidDeactivate(_ session: WCSession) {
}
}
The method you are using transferFile(_:metadata:) runs asynchronously and gets throttled by the system to accommodate Apple Watch’s limits on performance and power. You should be able to check the status of the
outstandingFileTransfers property to ensure you are setting up the transfer correctly, but beyond that you may need to rethink how you are sharing data between the two devices.

Cannot keep WatchOS 3 data synced with Firebase?

With the removal of WatchOS3 from being able to directly connect to the Firebase database, I am having issues keeping the watch app up to date with the data if the phone is locked or the app is not active.
I am currently having the watch request updates from the phone in the "awake", which works. However any changes/additions to the data in Firebase does not fire the update on the phone running in the background to notify the watch. If the phone app is open, communication is passed along flawlessly.
I am scoured the Apple docs, but I am not seeing what I am missing to keep the phone / Firebase connection active while in the background.
Has anyone had better luck with this or have a recommendation on a more robust solution?
Current Watch Code/Flow:
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
print("RECEIVED MESSAGE from Phone")
// REMOVED: This is where I parse json return and reloads the view data
replyHandler( [ "Pages" : "Good to Go" ] )
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print ("Watch Activation Complete")
}
override func awake(withContext context: Any?) {
super.awake(withContext: context)
let currentDate = Date()
sendMessageToPhone(command: dataCommands.sendData, value: currentDate.description)
}
func sendMessageToPhone(command: dataCommands, value: String) {
print("SENDING MESSAGE from Watch")
let data = [command.rawValue : value]
session.sendMessage(data, replyHandler: {(data: [String : Any]) -> Void in
print("Phone Got Message")
}, errorHandler: {(error ) -> Void in
print("Phone did not get message.")
self.messageErrorHandler(error: error as NSError)
})
}

watchOS2 app and iPhone app communication

In the watchOS1, we had a method “openParentApplication”. This method communicated with the phone application even when it wasn’t running in foreground or background and fetched a reply immediately. I need something similar for watchOS2. I want my watch application to communicate immediately with the phone app even if my iPhone application is not running. Methods like updateApplicationContext:error:, sendMessage:replyHandler:errorHandler: and transferUserInfo: are not helpful in this scenario.
Please can someone suggest me a better approach to achieve this?
Actually sendMessage:replyHandler:errorHandler: is doing exactly what you are asking for. As long as your watch is connected to your phone it immediately gets a response to the message. This is working when the app is in the foreground, in the background or not running at all.
Here is how you set it up:
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. So, basically it just works like openParentApplication(_:reply:)

WCSession Failing to Activate

I am having a problem with the WatchKit Connectivity Session failing to activate when I call the session.activateSession() method. This is the code I am using to set up the session.
if (WCSession.isSupported()) {
session = WCSession.defaultSession()
session.delegate = self // conforms to WCSessionDelegate
session.activateSession()
print("Session has been activated")
}
However, I have placed a breakpoint on the print line and when I inspect the session object, it says the sessionActivated property is still false, even after calling activateSession. I don't appear to be getting any sort of bug when I call activate session, so I assume it should have worked, but this does not seem to be the case.
Furthermore, if I try and use the sendMessage method on the session object later in my code like this -
let message = ["request": "fireLocalNotification"]
session.sendMessage(
message, replyHandler: { (replyMessage) -> Void in }) { (error) -> Void in
print(error.localizedDescription)
}
I receive an error code "The operation couldn’t be completed. (WCErrorDomain error 7004.)" which I looked up which means "WCErrorCodeSessionNotActivated." This is yet another reason why I think the activateSession method isn't calling correctly. I have even tried running the activateSession method the line directly before I send the message, but I still receive the error. If anyone could help explain what is going on, that would be wonderful, thank you! :)
You should activate the WatchConnectivity session on both the WatchKit Extension and the iOS app target. For example you might do it in the InterfaceController's
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
if WCSession.isSupported() {
let wcsession = WCSession.defaultSession()
wcsession.delegate = self
wcsession.activateSession()
wcsession.sendMessage(["update": "list"], replyHandler: { (dict) -> Void in
print("InterfaceController session response: \(dict)")
}, errorHandler: { (error) -> Void in
print("InterfaceController session error: \(error)")
})
}
}
and in the AppDelegate
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if WCSession.isSupported() {
let wcsession = WCSession.defaultSession()
wcsession.delegate = self
wcsession.activateSession()
}
return true
}
What I have noticed in several examples is that people tend to set a delegate only in the class which handles requests, e.g. if the watch was to send a message to the iOS app a delegate would only be set in the iOS app. This is WRONG. As the WatchConnectivity clearly states, you MUST set the delegate in both circumstances, otherwise you'll get the 7004 error.
Since "activateSession()" changed to "activate()" in Xcode 8 ,You need to add and extension for your class to delegate the session ( WCSessionDelegate ), and extend it with the function:
func session(_ session: WCSession, activationDidCompleteWith
activationState: WCSessionActivationState, error: Error?)
In order to ensure that the asynchronous method "activate" finishes successfully.
In your case:
extension YourInterfaceControllerClass : WCSessionDelegate {
func session(_ session: WCSession,
didReceiveMessage message: [String : Any],
replyHandler: #escaping ([String : Any]) -> Void)
{
//this function is mandatory and can be empty
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?)
{
// Your code to be executed after asynchronous activation is completed:
let message = ["request": "fireLocalNotification"]
session.sendMessage(
message, replyHandler: { (replyMessage) -> Void in }) { (error) -> Void in
print(error.localizedDescription)
}
//....
}
}
Are you using big number values?
NSDictionary *userInfo = #{
#"a1":#(1000000000), // 1000000000
#"a2":#(10000000000), // 1e+10
#"a3":#(100000000000), // crash!
};
[[WCSession defaultSession] transferUserInfo:userInfo];
On the above code, the value of key "a3" is dangerous, it causes Apple Watch crash.
Once you send the list, it remains in Apple Watch until reinstall the watch app.
(This crash occurs on a device, not on a simulator)