118 lines
4.3 KiB
Swift
118 lines
4.3 KiB
Swift
import Foundation
|
|
import WatchConnectivity
|
|
import Combine
|
|
|
|
// MARK: - Лёгкая модель трека для передачи на часы
|
|
struct WatchTrackInfo: Codable {
|
|
let id: String // используем имя файла как стабильный идентификатор
|
|
let title: String
|
|
let artist: String
|
|
}
|
|
|
|
// MARK: - Связь iPhone <-> Apple Watch
|
|
// Добавь этот файл в таргет "music player" (основное приложение).
|
|
class PhoneConnectivityManager: NSObject, WCSessionDelegate, ObservableObject {
|
|
static let shared = PhoneConnectivityManager()
|
|
|
|
private var cancellables = Set<AnyCancellable>()
|
|
|
|
private override init() {
|
|
super.init()
|
|
activateSession()
|
|
// Откладываем обращение к MusicManager.shared на следующий "тик" —
|
|
// иначе при первом запуске получится циклическая инициализация
|
|
// двух синглтонов друг через друга и приложение упадёт.
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.observeMusicManager()
|
|
}
|
|
}
|
|
|
|
private func activateSession() {
|
|
guard WCSession.isSupported() else { return }
|
|
let session = WCSession.default
|
|
session.delegate = self
|
|
session.activate()
|
|
}
|
|
|
|
// MARK: - Отправка состояния плеера на часы
|
|
|
|
private func observeMusicManager() {
|
|
let manager = MusicManager.shared
|
|
Publishers.CombineLatest4(
|
|
manager.$tracks,
|
|
manager.$currentTrackIndex,
|
|
manager.$isPlaying,
|
|
manager.$currentTitle
|
|
)
|
|
.debounce(for: .milliseconds(250), scheduler: DispatchQueue.main)
|
|
.sink { [weak self] _ in
|
|
self?.pushStateToWatch()
|
|
}
|
|
.store(in: &cancellables)
|
|
}
|
|
|
|
func pushStateToWatch() {
|
|
guard WCSession.default.activationState == .activated else { return }
|
|
let manager = MusicManager.shared
|
|
|
|
let trackInfos = manager.tracks.map {
|
|
WatchTrackInfo(id: $0.url.lastPathComponent, title: $0.title, artist: $0.artist)
|
|
}
|
|
guard let tracksData = try? JSONEncoder().encode(trackInfos) else { return }
|
|
|
|
let context: [String: Any] = [
|
|
"tracks": tracksData,
|
|
"isPlaying": manager.isPlaying,
|
|
"currentTitle": manager.currentTitle,
|
|
"currentArtist": manager.currentArtist,
|
|
"currentId": manager.currentTrack?.url.lastPathComponent ?? ""
|
|
]
|
|
|
|
try? WCSession.default.updateApplicationContext(context)
|
|
}
|
|
|
|
// MARK: - WCSessionDelegate
|
|
|
|
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
|
DispatchQueue.main.async { self.pushStateToWatch() }
|
|
}
|
|
|
|
func sessionDidBecomeInactive(_ session: WCSession) {}
|
|
func sessionDidDeactivate(_ session: WCSession) { WCSession.default.activate() }
|
|
|
|
// Команды, пришедшие пока приложение на телефоне активно
|
|
func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
|
|
handleCommand(message)
|
|
}
|
|
|
|
// Команды, пришедшие "в очереди" (если телефон был недоступен)
|
|
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
|
|
handleCommand(userInfo)
|
|
}
|
|
|
|
// MARK: - Обработка команд с часов
|
|
|
|
private func handleCommand(_ command: [String: Any]) {
|
|
guard let action = command["action"] as? String else { return }
|
|
DispatchQueue.main.async {
|
|
let manager = MusicManager.shared
|
|
switch action {
|
|
case "playPause":
|
|
manager.playPause()
|
|
case "next":
|
|
manager.nextTrack()
|
|
case "previous":
|
|
manager.prevTrack()
|
|
case "playTrack":
|
|
if let id = command["id"] as? String,
|
|
let index = manager.currentList.firstIndex(where: { $0.url.lastPathComponent == id }) {
|
|
manager.playTrack(at: index)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
self.pushStateToWatch()
|
|
}
|
|
}
|
|
}
|