I'd like to add to my Watch app functionality which send to iPhone app a Local Notification (while iPhone app is on the background or iPhone is locked).
I know how to create Local Notification itself.
What Im asking for is way, how to trigger background process (which contains also Local Notification) on iPhone by (for example) tapping on button on Apple Watch.
WKInterfaceController.openParentApplication is the official way to communicate with the iPhone. Documentation.
You pass parameters in the userInfo dictionary and retrieve results via the reply block.
On the iPhone the request is handled by appDelegate's handleWatchKitExtensionRequest method. Documentation
Code in my InterfaceController.swift:
#IBAction func btn() {
sendMessageToParentApp("Button tapped")
}
// METHODS #2:
func sendMessageToParentApp (input:String) {
let dictionary = ["message":input]
WKInterfaceController.openParentApplication(dictionary, reply: { (replyDictionary, error) -> Void in
if let castedResponseDictionary = replyDictionary as? [String:String], responseMessage = castedResponseDictionary["message"] {
println(responseMessage)
self.lbl.setText(responseMessage)
}
})
}
Next i made new method in my AppDelegate.swift:
func application(application: UIApplication, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]?, reply: (([NSObject : AnyObject]!) -> Void)!) {
if let infoDictionary = userInfo as? [String:String], message = infoDictionary["message"] {
let response = "iPhone has seen this message." // odešle se string obsahující message (tedy ten String)
let responseDictionary = ["message":response] // tohle zase vyrobí slovník "message":String
NSNotificationCenter.defaultCenter().postNotificationName(notificationWatch, object: nil)
reply(responseDictionary)
}
}
As you can see I use Notification to get iOS app know that button has been tapped. In ViewController.swift I have Notification Observer and function which is executed every time observer catch notification that user tapped on button on watch ("notificationWatch" is global variable with notification key). Hope this will help to anybody.
Related
I'm making an app for Apple Watch that needs to wake the iPhone's counterpart app which loads a site via a WKWebView, takes a snapshot, and sends the image back.
It works perfectly when the iPhone app is on-screen, intermittently when it's running in the background, but not at all when the app is completely closed.
Is there any way to get the iPhone app to wake up in the background with WCSession's sendMessage? I've read that it's meant to but I haven't been able to get it working. Is it because the iPhone app doesn't send a reply to the initial message sent by the watch (the file that the iPhone sends back has to wait for the WKWebView to finish loading, so it can't be sent back in replyHandler)? Is there a plist setting I forgot to toggle?
The current workflow of this code is as follows:
On the Apple Watch, the user taps a button which triggers the already activated WCSession's sendMessage function in ExtensionDelegate.
The iPhone app receives it using the WCSession that it activated in AppDelegate.
In didRecieve, the iPhone app feeds a URL into a WKWebView and starts loading it.
In WKWebView's didFinish function, it takes a snapshot of the site and sends it back to the watch with transferFile.
The watch receives the snapshot and passes it back to the right ViewController.
All of these steps have been tested and verified to work while both apps are on-screen, but as soon as the iPhone enters the background or has its counterpart app closed, this workflow becomes very unstable.
The relevant code is below:
After the user presses the button, the ViewController fires a notification to ExtensionDelegate with the information to transmit over WCSession.
ExtensionDelegate (sending the message):
#objc func transmit(_ notification: Notification) {
// The paired iPhone has to be connected via Bluetooth.
if let session = session, session.isReachable {
session.sendMessage(["SWTransmission": notification.userInfo as Any],
replyHandler: { replyData in
// handle reply from iPhone app here
print(replyData)
}, errorHandler: { error in
// catch any errors here
print(error)
})
} else {
// when the iPhone is not connected via Bluetooth
}
}
The iPhone app (should, but doesn't) wakes up and activates the WCSession:
fileprivate let session: WCSession? = WCSession.isSupported() ? WCSession.default : nil
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
session?.delegate = self
session?.activate()
webView.navigationDelegate = self
webView.scrollView.contentInsetAdjustmentBehavior = .never
return true
}
The iPhone app receives the message in AppDelegate, and activates the WKWebView. Note that there isn't a configured reply. Could this be the cause of my issue?
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
DispatchQueue.main.async { [self] in
let dictionary = message["SWTransmission"] as! [String: Any]
let link = URL(string: dictionary["URL"] as! String)!
let request = URLRequest(url: link)
webView.frame = CGRect(x: 0, y: 0, width: Int(((dictionary["width"] as! Double) * 1.5)), height: dictionary["height"] as! Int)
webView.load(request)
}
}
[Still in AppDelegate] After the site is loaded, didFinish (should) gets activated, where it takes a snapshot and sends the file back to the watch via transferFile.
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.takeSnapshot(with: nil) { [self] (image, error) in
let filename = getDocumentsDirectory().appendingPathComponent("webImage.jpg")
if let data = image!.jpegData(compressionQuality: 0.8) {
try? data.write(to: filename)
}
self.session?.transferFile(filename, metadata: nil)
}
}
The Apple Watch receives the file in ExtensionDelegate and sends it back to the relevant ViewController:
func session(_ session: WCSession, didReceive file: WCSessionFile) {
DispatchQueue.main.async { [self] in
do {
NotificationCenter.default.post(name: NSNotification.Name("openSite"), object: nil, userInfo: ["imageURL": file.fileURL] as [String: Any])
} catch {
print(error)
}
}
}
Thank you very much for your help!
I do not have experience working on WatchOS, but I took a look at the documentation for WCSession and it seems that what you are observing exactly matches the expected behaviour.
You mention
"It works perfectly when the iPhone app is on-screen, intermittently when it's running in the background, but not at all when the app is completely closed"
The Apple documentation for WCSession states
When both session objects are active, the two processes can communicate immediately by sending messages back and forth. When only one session is active, the active session may still send updates and transfer files, but those transfers happen opportunistically in the background.
These two align perfectly.
When the app is on-screen, both session objects are apparently active, and as per your observation under this scenario you see the communication happening everytime.
When the app is in background, the session object on the app side appears to be not active, and the transfer would take place 'opportunistically', which is in-line with you observation that the communication occurs intermittently.
When the the app is completely closed, it cannot be launched by the system under any circumstance as far as I know, and this is also in-line with your observation that in-this situation the communication never happens.
Unless you've already read through the WCSession documentation, I would suggest that you go through it. I see that you are checking for WCSession's isReachable property, however, another important property that is mentioned on the documentation page is activationState. It may be worth checking the value of this property before initiating the communication.
I need to update orders from my app while app is in background.
Ok, I am using OneSignal, I can get message on didReceiveRemoteNotification and inside it, I call Alamofire to check on my api what I need to update.
The problem is when the code get to the point: Alamofire.request(url).responseJSON {(response) in it doesnt go inside, just when I open the app I can get the result.
I would like it to get the new data on background and notify users after updating, so they can click on the notification to see whats is new.
I read that Alamofire runs on a background thread by default, but the network request goes on Main thread.
So, I tried: this and this, both don't work.
I tried URLSessionConfiguration but I got Error code -999 cancelled.
So, I added sessionManager.session.finishTasksAndInvalidate() in the end of my response. The error stops, but the code still don't go inside Alamofire request.
Some of my code - didReceiveRemoteNotification on my AppDelegate:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
if let custom = userInfo["custom"] as? NSDictionary {
if let a = custom["a"] as? NSDictionary {
if let update = a["update"] as? NSString {
if update.isEqual(to: "Pedido") {
let strdataPedido = PedidoDAO().getMostRecentDtPedido()
if self.defaults.getDownloadInicialPedido(){
if strdataPedido != "" {
//let task = self.beginBackgroundTask()
self.loadOrdersFromLastSyncByApi(strdataPedido)
//self.endBackgroundTask(task)
}
}
}
}
}
}
loadOrdersFromLastSyncByApi function on my AppDelegate:
func loadOrdersFromLastSyncByApi(_ lastSync: String) {
let parceiroId = defaults.getParceiroId()
PedidoAPI().loadOrdersForLastSync(parceiroId, lastSync){ (dados) in
if let dadosPedidoModel = dados as? [PedidoModel] {
//do what needs to do to save new data
}
PedidoAPI().loadOrdersForLastSync function:
func loadOrdersForLastSync(_ parceiroId: String, _ lastSync: String, _ onComplete: #escaping(Any?) -> Void) {
let url = Test.basePath + "/api/orders/parceiro/\(parceiroId)/\(lastSync)"
//let queue = DispatchQueue(label: "com.test.br", qos: .background, attributes: .concurrent)
let task = self.beginBackgroundTask()
let queue = DispatchQueue.global(qos: .background)
queue.async {
Alamofire.request(url).responseJSON {(response) in
//This result its fired just when I open my app, I would like it to make everything on background
switch (response.result) {
//do what needs to send new data
}
self.endBackgroundTask(task)
Any help please?
Thanks for your question. So to clarify, I understood your question as the following:
You want to update users when there has been a change in their orders while the app is in background.
Follow-up Question:
Is there a reason you want to make the request from the client? Also, what data is coming in from OneSignal that you couldn't just handle on your server?
Answer:
You should handle any requests to a third party (Alamofire) on the server and then use our API to send a notification to the user with new info from the request response. I think that would be the best approach.
Lately, I am working on a project is related to Watch/iPhone communication again. But my code works sometimes and doesn’t work sometimes which is kind of weird to me because I think the code should either work or not. It cannot be 50/50. Therefore, I have no idea what goes wrong.
setup WCSession on iPhone:
class WatchCommunicationController: NSObject, WCSessionDelegate {
var session : WCSession?
override init(){
// super class init
super.init()
// if WCSession is supported
if WCSession.isSupported() { // it is supported
// get default session
session = WCSession.defaultSession()
// set delegate
session!.delegate = self
// activate session
session!.activateSession()
} else {
print("iPhone does not support WCSession")
}
}
... ...
}
similar WCSession setup on Watch:
class PhoneCommunicationController: NSObject, WCSessionDelegate {
var session : WCSession?
override init(){
// super class init
super.init()
// if WCSession is supported
if WCSession.isSupported() { // it is supported
// get default session
session = WCSession.defaultSession()
// set delegate
session!.delegate = self
// activate session
session!.activateSession()
} else {
print("Watch does not support WCSession")
}
}
... ...
}
send out message on Watch:
func sendGesture(gesture : GKGesture){
// if WCSession is reachable
if session!.reachable { // it is reachable
// create the interactive message with gesture
let message : [String : AnyObject]
message = [
"Type":"Gesture",
"Content":gesture.rawValue
]
// send message
session!.sendMessage(message, replyHandler: nil, errorHandler: nil)
print("Watch send gesture \(gesture)")
} else{ // it is not reachable
print("WCSession is not reachable")
}
}
related enum:
enum GKGesture: Int {
case Push = 0, Left, Right, Up, Down
}
receive message on iPhone:
func session(session: WCSession, didReceiveMessage message: [String : AnyObject]) {
//retrieve info
let type = message["Type"] as! String
let content = message["Content"]
switch type {
case "Gesture":
handleGesture(GKGesture(rawValue: content as! Int)!)
default:
print("Received message \(message) is invalid with type of \(type)")
}
}
func handleGesture(gesture : GKGesture){
print("iPhone receives gesture \(gesture)")
var notificationName = ""
switch gesture {
case .Up:
notificationName = "GestureUp"
case .Down:
notificationName = "GestureDown"
case .Left:
notificationName = "GestureLeft"
case .Right:
notificationName = "GestureRight"
case .Push:
notificationName = "GesturePush"
}
NSNotificationCenter.defaultCenter().postNotificationName(notificationName, object: nil)
}
somehow I can’t debug my Watch app on Xcode, the debug session just won’t attach. I don’t know why. Therefore, I debug one-sided with just the iPhone.
sometimes I got "receives gesture” print out, and sometimes not. And the same for getting the notification.
I don't know if Int would be wrapped around to NSNumber while being transfer within WCSession. If it would be, then that must be why when I use Int as the base class of the enum it won't work and works when String is the base class.
Connectivity Known Issue Your app may crash when using NSNumber and
NSDate objects with the WCSession API.
Workaround: Convert an NSNumber or NSDate object to a string before
calling WCSession APIs. Do the opposite conversion on the receiving
side.
Watch OS 2 Beta 4 release note
My guess is your call to sendMessage is returning an error in the cases where it fails, but you haven't implemented the error handler!! For now while you are getting up and running you can get away with just printing the error, but if this is shipping code you really ought to handle the appropriate errors:
// send message
session.sendMessage(message, replyHandler: nil, errorHandler: { (error) -> Void in
print("Watch send gesture \(gesture) failed with error \(error)")
})
print("Watch send gesture \(gesture)")
Your flow is correct but the difficulty is to understand how to debug:
Debug Watch:
Run the iPhone target and when it is done hit the Stop button.
Open the iOS app inside the simulator (run it manually from the simulator and not from Xcode) and let it hang there.
Switch to the Watch target (yourAppName WatchKit App), put the relevant breakpoint and run it.
The iOS app will be put automatically in the background and then you will be able to use sendMessage method (at the Watch target) to send whatever you need and if you have a replayHandler in your iOS app you will even receive the relevant messages inside the sendMessage at your Watch target (i.e InterfaceController)
Small Swift example:
Sending a Dictionary from Watch to iOS app:
if WCSession.defaultSession().reachable == true {
let requestValues = ["Send" : "From iWatch to iPhone"]
let session = WCSession.defaultSession()
session.sendMessage(requestValues,
replyHandler: { (replayDic: [String : AnyObject]) -> Void in
print(replayDic["Send"])
}, errorHandler: { (error: NSError) -> Void in
print(error.description)
})
}
else
{
print("WCSession isn't reachable from iWatch to iPhone")
}
Receiving the message from the Watch and sending a replay from the iOS app:
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
print(message.values)
var replyValues = Dictionary<String, AnyObject>()
replyValues["Send"] = "Received from iphone"
// Using the block to send back a message to the Watch
replyHandler(replyValues)
}
Debug iPhone:
The exact opposite of debug watch
Also, the answer by #sharpBaga has an important consideration.
Can someone please talk me through adding a button to a long-look local notification? I'm new to both the watch kit and notifications.
The long-look is functioning correctly. I'm setting up a UILocalNotification in my host app, setting the alertBody, category, userInfo, etc. and sending it off.
In my notification controller I'm setting everything up in didReceiveLocalNotification and it's working fine.
It seems, from my research, that I'm supposed to somehow add the button to the notification and use the method handleActionWithIdentifier: forLocalNotification but I'm unclear exactly how to do this.
I'm writing in Objective-C. Thanks
Here is notification payload I'm using:
{
"aps": {
"alert": {
"body": "Hi! How you doing? This is a test message. Press 'Reply' button to reply. Thanks!",
"title": "Petya"
},
"category": "newMessage"
},
"WatchKit Simulator Actions": [
{
"title": "Reply",
"identifier": "replyPressed"
}
],
}
Create custom notification subclass and override following method and don't forget to set that subclass in corresponding storyboard controller:
override func didReceiveRemoteNotification(remoteNotification: [NSObject : AnyObject], withCompletion completionHandler: (WKUserNotificationInterfaceType) -> Void) {
if let aps = remoteNotification["aps"] as? NSDictionary {
if let alert = aps["alert"] as? NSDictionary {
if let title = alert["title"] as? String {
titleLabel.setText(title)
}
if let body = alert["body"] as? String {
bodyLabel.setText(body)
}
}
}
completionHandler(WKUserNotificationInterfaceType.Custom)
}
After that in your notification custom button with title 'Reply' will appear. And when you press it it will launch watch app main interface controller and call handleActionWithIdentifier:localNotification: or handleActionWithIdentifier:remoteNotification:, depends on kind of notification you received. You have to override it like that:
override func handleActionWithIdentifier(identifier: String?, forRemoteNotification remoteNotification: [NSObject : AnyObject]) {
println("identifier: \(identifier)")
println("remoteNotification: \(remoteNotification)")
if identifier == "replyPressed" {
if let aps = remoteNotification["aps"] as? NSDictionary {
if let alert = aps["alert"] as? NSDictionary {
if let title = alert["title"] as? NSString {
let context = Context()
context.object = title
pushControllerWithName(kTCChatRoomControllerIdentifier, context: context)
}
}
}
}
}
P.S. Context is my own class for passing data between WKInterfaceController's subclasses
Hope this will help you!)
The Notification Essentials section of the Apple Watch Programming Guide describes how to add action buttons to Long-Look notifications for local notifications.
Basically, you need to create a UIMutableUserNotificationAction for every button that you want to add to your Long-Look interface, along with a category for your local notification. This is done in the App Delegate of your iOS app, when you register for notifications. Listing 15-1 in that guide shows exactly how to do it. Then, in the storyboard for your Apple Watch app, set your notification's category to the category you just created. The next section of the guide, "Responding to Taps in Action Buttons," should hopefully tell you everything you need to know in order to either launch your WatchKit app, or execute a task in the background on the iPhone.
I am making an Apple Watch app. One of the buttons will open the iphone app connected to the watch app.
What code do I use to do this?
I don't know what to even try?
Note: I am using swift for this project.
WatchKit doesn't include the ability to open the host iOS app in the foreground. The best you can do is open it in the background using openParentApplication:reply:.
If you need the user to do something in your iOS app, consider making use of Handoff.
It is not possible to activate an inactive iPhone app from the Watch. It is, however, possible to call the iPhone app to perform a task or to ask for data. See here: Calling parent application from Watch app
You can only open the iPhone app in the background by the following method:
Swift:
openParentApplication([ParentApp], reply:[Reply])
Objective-C:
openParentApplication:reply:
There is no ability to open the parent app in the foreground.
Note: To send data to iOS app in the background, use the first method.
Note: According to bgilham,
If you need the user to do something in your iOS app, consider making use of Handoff.
If you need to open your parent app in the foreground, use Handoff!
https://developer.apple.com/handoff/
Example:
Somewhere shared for both:
static let sharedUserActivityType = "com.yourcompany.yourapp.youraction"
static let sharedIdentifierKey = "identifier"
on your Watch:
updateUserActivity(sharedUserActivityType, userInfo: [sharedIdentifierKey : 123456], webpageURL: nil)
on your iPhone in App Delegate:
func application(application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool {
if (userActivityType == sharedUserActivityType) {
return true
}
return false
}
func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]!) -> Void) -> Bool {
if (userActivity.activityType == sharedUserActivityType) {
if let userInfo = userActivity.userInfo as? [String : AnyObject] {
if let identifier = userInfo[sharedIdentifierKey] as? Int {
//Do something
let alert = UIAlertView(title: "Handoff", message: "Handoff has been triggered for identifier \(identifier)" , delegate: nil, cancelButtonTitle: "Thanks for the info!")
alert.show()
return true
}
}
}
return false
}
And finally (this step is important!!!): In your Info.plist(s)