I have iOS application which provides data for my watchface. The watch face has a very simple flow that periodically requests new data.
My implementation of iOS is based on official example.
This is basically core of my application. It handles connection and sending/receiving messages.
import Combine
import UIKit
import ConnectIQ
import Foundation
final class GarminService {
// This must match the value in `Info.plist`.
private static let urlScheme = "com.lukas.garmincompanion"
static let shared = GarminService()
private let manager = Manager()
private let messageSubject = PassthroughSubject<Any, Never>()
private var lifetimeCancellables: Set<AnyCancellable> = []
private init() {
ConnectIQ.shared?.initialize(
withUrlScheme: Self.urlScheme,
uiOverrideDelegate: nil
)
DeviceManager.shared.restoreDevicesFromFileSystem()
registerForStoredDevices()
manager.messageHandler = { [weak self] messageData in
self?.messageSubject.send(messageData)
Task {
let data = await CalendarReader.shared.prepareJsonData()
print("recevied data")
await self?.broadcast(dto: data!)
}
}
}
func observeMessages() -> AnyPublisher<Any, Never> {
messageSubject.eraseToAnyPublisher()
}
@discardableResult
func handle(url: URL) -> Bool {
guard url.scheme == Self.urlScheme,
let devices = ConnectIQ.shared?.parseDeviceSelectionResponse(
from: url
) as? [IQDevice]
else { return false }
DeviceManager.shared.saveDevicesToFileSystem(devices)
registerForStoredDevices()
return true
}
private func registerForStoredDevices() {
for device in DeviceManager.shared.savedDevices {
ConnectIQ.shared?.register(forDeviceEvents: device, delegate: manager)
}
}
func broadcast(dto: Any) async {
await manager.broadcast(dto: dto)
}
}
extension GarminService {
fileprivate final class Manager: NSObject, IQDeviceEventDelegate,
IQAppMessageDelegate
{
private static let watchAppUuid = UUID(
uuidString: "xxx"
)
@Published
private(set) var apps: [UUID: IQApp] = [:]
var messageHandler: ((Any) -> Void)?
func deviceStatusChanged(_ device: IQDevice!, status: IQDeviceStatus) {
switch status {
case .connected:
let app = IQApp(
uuid: Self.watchAppUuid,
store: nil,
device: device
)
apps[device.uuid] = app
ConnectIQ.shared?.register(forAppMessages: app, delegate: self)
// IMPORTANT: Sending a message right after connecting sends the messages to the void.
// I have no idea why it doesn't work, but feel free to shrink the delay. I've found that 100ms works reliably.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
ConnectIQ.shared?.sendMessage(
"Hello there.",
to: app,
progress: nil,
completion: {
print($0)
}
)
}
case .bluetoothNotReady, .invalidDevice, .notConnected, .notFound:
apps.removeValue(forKey: device.uuid)
@unknown default:
print("Unhandled case '\(status.rawValue)'.")
}
}
func receivedMessage(_ message: Any!, from app: IQApp!) {
print(
"Received message from ConnectIQ: \(message.debugDescription)"
)
guard let message else { return }
messageHandler?(message)
}
func broadcast(dto: Any) async {
let wrapped = NSDictionary(dictionary: ["message": dto])
for app in apps.values {
await ConnectIQ.shared?.sendMessage(
wrapped,
to: app,
progress: nil
)
print("Sent \(wrapped) to \(app)")
}
}
// deinit {
// ConnectIQ.shared?.unregister(forAllDeviceEvents: self)
// ConnectIQ.shared?.unregister(forAllAppMessages: self)
// }
}
}
extension ConnectIQ {
static var shared: ConnectIQ? {
sharedInstance()
}
}
Everything works perfectly for about one or two hours. After that, the iOS application suspends and stops responding. I have to start it manually and redirect to the Garmin Connect application again.
I have Background fetch, Background processing and Uses bluetooth LE accessories enabled.
I did not find a way how to keep connection with my watches alive for a long time like with Garmin connect app. Does anyone have experience with that?
Thanks