AVAudioSession always returns the same outputVolume - ios

I have an watchOS app that request the current volume from the iPhone parent app via:
session?.sendMessage(["getVolume":1], replyHandler: {
replyDict in
if let currentVolume = replyDict["currentVolume"] as? Float{
NSLog("Current volume received from phone with value: \(currentVolume)")
}
}, errorHandler: {
(error) -> Void in
NSLog("Error: :\(error)")
// iOS app failed to get message. Send it in the background
//self.session?.transferUserInfo(["getVolume":1])
})
The iPhone app handles it like this:
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
NSLog("Received a message")
if let _ = message["getVolume"] as? Int{
replyHandler(["currentVolume":AVAudioSession.sharedInstance().outputVolume])
}
else{
replyHandler([:])
}
}
This always returns the same outputVolume the phone had on the first request.
I investigated several things like if there is some kind of caching for the background request but it is always a new call that returns the new value.
Is there any workaround to get system volume with a different way or maybe a solution how to get this to work with AVAudioSession?

This isn't an issue with the WatchKit or watchOS per se, as I've experienced this in a regular iOS application.
I had to activate my app's audio session in order to observe the change in volume:
try? AVAudioSession.sharedInstance().setActive(true)

Related

Sometimes WatchConnectivity session on paired watch simulator is not reachable

I have an iOS app that communicates with the paired watch using WatchConnectivity. In most cases, it works without problems, on the simulators and on the devices.
The problem:
During development on the simulators, I get now and then the following communication error when I try to send a direct message from iOS to watchOS using WCSession.default.sendMessage(_:replyHandler:errorHandler:):
Error Domain=WCErrorDomain Code=7007
"WatchConnectivity session on paired device is not reachable."
I have read this related post, but it does not apply to my case, because my app does work normally.
My questions:
How can it be that the watch simulator becomes not reachable while the app is running on the iOS simulator?
Does it make sense just to retry sendMessage after a while?
Is there any workaround?
As a workaround, I modified the sendMessage function so that in case of this error the transfer is retried a number of times. Since then, all sendMessage transfers are executed successfully.
func sendMessage(_ message: [String: AnyObject],
replyHandler: (([String: Any]) -> Void)?,
errorHandler: ((Error) -> Void)?) {
guard let communicationReadySession = communicationReadySession else {
// watchOS: A session is always valid, so it will never come here.
print("Cannot send direct message: No reachable session")
let error = NSError.init(domain: kErrorDomainWatch,
code: kErrorCodeNoValidAndReachableSession,
userInfo: nil)
errorHandler?(error)
return
}
/* The following trySendingMessageToWatch sometimews fails with
Error Domain=WCErrorDomain Code=7007 "WatchConnectivity session on paired device is not reachable."
In this case, the transfer is retried a number of times.
*/
let maxNrRetries = 5
var availableRetries = maxNrRetries
func trySendingMessageToWatch(_ message: [String: AnyObject]) {
communicationReadySession.sendMessage(message,
replyHandler: replyHandler,
errorHandler: { error in
print("sending message to watch failed: error: \(error)")
let nsError = error as NSError
if nsError.domain == "WCErrorDomain" && nsError.code == 7007 && availableRetries > 0 {
availableRetries = availableRetries - 1
let randomDelay = Double.random(min: 0.3, max: 1.0)
DispatchQueue.main.asyncAfter(deadline: .now() + randomDelay, execute: {
trySendingMessageToWatch(message)
})
} else {
errorHandler?(error)
}
})
} // trySendingMessageToWatch
trySendingMessageToWatch(message)
} // sendMessage

How to access PFUser.Current() on watch app

I am using Parse and I want to be able to access the current user on my watch. There is a guide on sharing data between a host app and extensions but the function enableDataSharing(withApplicationGroupIdentifier:, containingApplication:) is marked as available. I guess I misunderstood and Watch Extensions are not considered App Extensions. Is there another way to access the current user on the Watch?
So after a lot of digging and pursuing multiple options I found out that Watch Apps are completely separate from iOS Apps and can no longer share KeyChain Access. This is probably why the built in Parse solution no longer works. Fortunately Watch Connectivity is pretty secure so I think it's safe to send the session token over from iOS and use PFUser.become to log in.
In the watch app:
guard WCSession.default.isReachable else {
print("Phone is not reachable")
return
}
WCSession.default.sendMessage(["Request": "SessionToken"],
replyHandler: (user, error) in {
guard let token = response["Response"] as? String else { return }
// there is a version of PFUser.become() with a success callback as well
PFUser.become(inBackground: token)
}
in iOS (class that conforms to WCSessionDelegate protocol):
public func session(_ session: WCSession,
didReceiveMessage message: [String: Any],
replyHandler: #escaping ([String: Any]) -> Void) {
// Respond to 'Request' message with sessionToken
if let _ = message["Request"], let sessionToken = PFUser.current()?.sessionToken {
replyHandler(["Response": sessionToken])
} else {
// Either user is not logged in, or this was not a 'Request' message
}
}

Can the watch app receive background info even if the app is in the background?

I cannot figure out how to make updateApplicationContext data arrive on the watch before the watch app is foregrounded. It seems to work only when the watch app is foregrounded.
How can the watch receive files while in the background?
This is what I've been trying to accomplish:
iOS code:
func sendDataToWatch() {
if WCSession.isSupported() {
do {
try WCSession.default.updateApplicationContext(["key":value])
} catch {
print("ERROR: \(error)")
}
}
}
Watch code:
func session(_ session: WCSession, didReceiveApplicationContext
applicationContext:[String : Any]) {
//handle data when it arrives
}
I noticed that the WatchConnectivity was provided with a handler function. Is this something I should set up to be able to handle background connectivity while the Watch App is backgrounded or not even launched?
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)
default:
// make sure to complete unhandled task types
task.setTaskCompletedWithSnapshot(false)
}
}
}
According to apple you can send data from iPhone to Apple Watch using SendMessage while session is reachable.
https://developer.apple.com/documentation/watchconnectivity/wcsession/1615687-sendmessage
Calling this method from your WatchKit extension while it is active
and running wakes up the corresponding iOS app in the background and
makes it reachable.
You can use below methods to send data from the iPhone to Apple Watch
Swift 2.2
let msg = ["IPrequest":"IsLogin"]
WCSession.defaultSession().sendMessage(msg, replyHandler: { (replyDict) in
print(replyDict)
}, errorHandler: { (error) in
print(error)
})
Received dictionary using below method
Swift 2.2
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void)
{
dispatch_async(dispatch_get_main_queue()) { () -> Void in
print("Response:\(message)")
}
}
I have implemented above solution in one of the my project.
Hope it will help you!

How to receive a watchOS file transfer on iOS in the background

I am using transferFile and I can successfully send and receive files, but in order to complete the transfer process, I need to open up the iPhone app.
In observing other apps, it appears that they are able to receive and act upon received data in the background (and send a push notification to the user, for example).
I am wondering how they did this.
You should send a message from the watch app to the phone using the sendMessage function of watch connectivity requesting the data. This will wake up the iphone app. Then in your didreceivemessage method on the phone you should use the filetransfer function to send your files to the watch.
To clarify when a message is sent using sendMessage this wakes the iphone application up in the background to receive the message to where it can respond with a file transfer. Hope this helps
You need to send a message first before sending the file transfer. Implement something like this on your watch side
func sendActivationMessage() {
if session.activationState == .activated && session.isReachable {
session.sendMessage(["Watch Message" : "Activate"], replyHandler: {
(reply) in
if reply["Phone Message"] as! String == "Activated" {
//This is where you should implement your file transfer
}
}, errorHandler: { (error) in
print("***** Error Did Occur: \(error) *****")
})
} else {
print("***** Activation Error *****")
}
}
Then in your didreceivemessage function on the phone side implement something like this
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
if let messageFromWatch = message["Watch Message"] {
let messageData = messageFromWatch as! String
//Message From Watch to Activate Watch Connectivity Session
if messageData == "Activate" {
replyHandler(["Phone Message" : "Activated"])
}
}

WatchConnectivity - using sendMessage

I am trying to establish connectivity between the Apple Watch (version 2.0.1) and my iPhone (running iOS 9.1) with the WatchConnectivity API in Swift.
I followed this tutorial and could not achieve messaging between the devices.
Messaging from the Apple Watch:
let applicationData = ["data":sampleData]
self.wcSession.sendMessage(applicationData, replyHandler: {(_: [String : AnyObject]) -> Void in
// handle reply from iPhone app here
}, errorHandler: {(error ) -> Void in
// catch any errors here
})
In my ViewController.swift:
// MARK: - WatchConnectivity Session
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
let sample:HKQuantitySample = (message["data"] as? HKQuantitySample)!
print("Sample messaged: \(sample)")
}
func sessionReachabilityDidChange(session: WCSession) {
print("session reachability changed: \(session.reachable)")
}
Both Watch app and the iOS app are foreground!!
I am not sure what is missing.
All of the WCSession APIs that take a dictionary as a parameter only accept dictionaries of property list types; this includes the sendMessage API you are using:
message / A dictionary of property list values that you want to send. You define the contents of the dictionary that your counterpart supports. This parameter must not be nil.
So HKSamples are not a property list type which is why this isn't working, although you are saying the error handler is not getting invoked which sounds very suspicious. Are you certain changing your code to this doesn't log anything?
self.wcSession.sendMessage(applicationData, replyHandler: {(_: [String : AnyObject]) -> Void in
// handle reply from iPhone app here
}, errorHandler: {(error ) -> Void in
print(error);
})

Resources