Let's say I have SwiftUI app that starts playing audio from chosen url in list:
struct ContentView: View {
@ObservedObject var viewModel: ContentViewModel
var body: some View {
List(PlayerLinks.links, id: \.self) { link in
Button(link) {
viewModel.play(from: link)
}
}
}
}
class ContentViewModel: ObservableObject {
let player = Player()
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(notificationAction),
name: .newLinkChosen,
object: nil
)
}
func play(from url: String) {
player.play(from: url)
}
@objc func notificationAction(_ notification: Notification) {
if let userInfo = notification.userInfo,
let newURL = userInfo["newLink"] as? String {
self.play(from: newURL)
}
}
}
class Player {
var player: AVPlayer?
init() {
do {
try AVAudioSession.sharedInstance()
.setCategory(
AVAudioSession.Category.playback,
mode: AVAudioSession.Mode.default,
options: []
)
} catch let error as NSError {
print(error.localizedDescription)
}
}
func play(from url: String) {
let playerItem = AVPlayerItem(url: URL(string: url)!)
player = AVPlayer(playerItem: playerItem)
player?.play()
}
}
And this app has watchOS companion app, that also has a list of stations:
struct ContentWatchView: View {
@ObservedObject var viewModel: ContentWatchViewModel
var body: some View {
List(PlayerLinks.links, id: \.self) { link in
Button(link) {
viewModel.onRowSelected(url: link)
}
}
}
}
class ContentWatchViewModel: ObservableObject {
var connectivityManager: WatchConnectivityManager
init(connectivityManager: WatchConnectivityManager) {
self.connectivityManager = connectivityManager
}
func onRowSelected(url: String) {
connectivityManager.sendPlayerLinkToIOS(url)
}
}
By tapping row in watchOS app, I need to start AVPlayer playing on iOS device.
For this I have implemented WatchConnectivityManager
that handles communication between watchOS and iOS apps:
class WatchConnectivityManager: NSObject, ObservableObject, WCSessionDelegate {
private let session: WCSession = WCSession.default
var isReachable = false
static var shared = WatchConnectivityManager()
override init() {
super.init()
if WCSession.isSupported() {
session.delegate = self
session.activate()
}
}
func session(
_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?
) {
#if os(iOS)
print("ACTIVATED ON IOS")
#elseif os(watchOS)
print("ACTIVATED ON WATCHOS")
#endif
DispatchQueue.main.async {
self.isReachable = session.isReachable
}
}
func sessionReachabilityDidChange(_ session: WCSession) {
DispatchQueue.main.async {
self.isReachable = session.isReachable
}
}
#if os(iOS)
func sessionDidDeactivate(_ session: WCSession) {
session.activate()
}
func sessionDidBecomeInactive(_ session: WCSession) {
print("Session did become inactive: \(session.activationState.rawValue)")
}
func sessionWatchStateDidChange(_ session: WCSession) {
print("Session watch state did change: \(session.activationState.rawValue)")
}
#endif
// MARK: MESSAGE RECEIVER
func session(
_ session: WCSession,
didReceiveMessage message: [String : Any],
replyHandler: @escaping ([String : Any]) -> Void
) {
#if os(iOS)
if let action = message["action"] as? String,
action == "newPlayerLinkChosen",
let link = message["link"] as? String {
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .newLinkChosen,
object: nil,
userInfo: ["newLink": link]
)
replyHandler(["success": true])
}
} else {
replyHandler(["success": false])
}
#endif
}
// MARK: MESSAGE SENDERS
#if os(watchOS)
func sendPlayerLinkToIOS(_ link: String) {
let message = [
"action": "newPlayerLinkChosen",
"link": link
]
session.sendMessage(message) { replyHandler in
print(replyHandler)
} errorHandler: { error in
print(error.localizedDescription)
}
}
#endif
}
extension Notification.Name {
static let newLinkChosen = Notification.Name("NewLinkChosen")
}
sendPlayerLinkToIOS
func sends message with chosen link, which is received by MESSAGE RECEIVER
method, and then it posts a notification with chosen link to the NotificationCenter.default
. This notification is received by iOS ContentViewModel
and the player starts.
It is all working good when the iOS app is in foreground, however it's not working when we go to the Home Screen and choose some url from watch app again (the iOS app is not terminated).
Here are logs, if they're helpful for someone:
ACTIVATED ON IOS
2023-08-18 16:23:38.651377+0300 RemotePlayer[20479:6162721] [plugin] AddInstanceForFactory: No factory registered for id <CFUUID 0x6000022fce00> F8BB1C28-BAE8-11D6-9C31-00039315CD46
2023-08-18 16:23:52.046769+0300 RemotePlayer[20479:6163115] [AMCP] 4611 HALC_ProxyIOContext.cpp:783 HALC_ProxyIOContext::_StartIO(): Client running as an adaptive unboosted daemon
2023-08-18 16:23:52.047123+0300 RemotePlayer[20479:6163115] HALPlugIn.cpp:519 HALPlugIn::StartIOProc: got an error from the plug-in routine, Error: 1852797029 (nope)
2023-08-18 16:23:52.048409+0300 RemotePlayer[20479:6163115] [aqme] AQMEIO.cpp:211 error 1852797029
2023-08-18 16:23:52.049634+0300 RemotePlayer[20479:6163115] [aqme] MEDeviceStreamClient.cpp:431 AQME Default-InputOutput: client stopping after failed start: <CA_UISoundClientBase@0x149514830>; running count now 0
2023-08-18 16:23:52.050233+0300 RemotePlayer[20479:6163115] CA_UISoundClient.cpp:285 CA_UISoundClientBase::StartPlaying: AddRunningClient failed (status = 1852797029).
2023-08-18 16:23:53.738535+0300 RemotePlayer[20479:6163570] [AMCP] 59139 HALC_ProxyIOContext.cpp:783 HALC_ProxyIOContext::_StartIO(): Client running as an adaptive unboosted daemon
2023-08-18 16:23:53.741183+0300 RemotePlayer[20479:6163570] HALPlugIn.cpp:519 HALPlugIn::StartIOProc: got an error from the plug-in routine, Error: 1852797029 (nope)
2023-08-18 16:23:53.744653+0300 RemotePlayer[20479:6163570] [aqme] AQMEIO.cpp:211 error 1852797029
2023-08-18 16:23:53.745684+0300 RemotePlayer[20479:6163570] [aqme] MEDeviceStreamClient.cpp:431 AQME Default-InputOutput: client stopping after failed start: <AudioQueueObject@0x14a809000; Unknown figplayer; [20479]; play>; running count now 0
Just bought Apple Watch for myself and tested again. Player's working fine when app is in background and also when the app is not opened at all. For some reason, this is not working on emulators... So if u face similar issue on emulator, try it on real device, maybe it's not even an issue at all.