Post

Replies

Boosts

Views

Activity

Reply to BLE Broadcast Cannot Relaunch User-Force-Quit App via AccessorySetupKit (iOS 26+)
Info.plist Configuration <key>NSAccessorySetupBluetoothNames</key> <array> <string>My-Device</string> </array> <key>NSAccessorySetupBluetoothServices</key> <array> <string>1111xxxx-xxxxx-xxxx-xxxx-xxxxxxxxxxxx</string> </array> <key>NSAccessorySetupKitSupports</key> <array> <string>Bluetooth</string> </array> <key>UIBackgroundModes</key> <array> <string>bluetooth-central</string> <string>remote-notification</string> </array> Relevant Code AppDelegate.swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { BluetoothService.share.activeSession() let center = UNUserNotificationCenter.current() center.delegate = self let options: UNAuthorizationOptions = [.badge, .alert, .sound] center.requestAuthorization(options: options) { granted, error in } UIApplication.shared.registerForRemoteNotifications() let notice = LocalNotification.defaultSound notice.body = "Launch App Success!" notice.play() return true } BluetoothService.swift public class BluetoothService: NSObject, CBCentralManagerDelegate { public static let share = BluetoothService() private let restoreIdentifier = "com.yourcompany.yourapp.bluetoothCentral" private lazy var centralManager = { let options: [String: Any] = [ CBCentralManagerOptionRestoreIdentifierKey: restoreIdentifier, CBCentralManagerOptionShowPowerAlertKey: true ] let centralManager = CBCentralManager(delegate: self, queue: nil, options: options) return centralManager }() public var options: [String: Any] = [CBCentralManagerScanOptionAllowDuplicatesKey: true] private var session: Any? private let bleName = "My-Device" private let serviceUUIDs = [CBUUID(string: "1111xxxx-xxxxx-xxxx-xxxx-xxxxxxxxxxxx")] private var tagTime: TimeInterval? override private init() { super.init() centralManager.delegate = self } public func activeSession() { if #available(iOS 18.0, *) { let session = ASAccessorySession() self.session = session session.activate(on: .main) { [weak self] event in guard let self = self else { return } self.handleSessionEvent(event) } } else { self.start() } } @available(iOS 18.0, *) private func handleSessionEvent(_ event: ASAccessoryEvent) { switch event.eventType { case .activated: guard let session = self.session as? ASAccessorySession else { return } if session.accessories.isEmpty { self.showPicker() } else { self.start() } case .accessoryAdded: self.start() default: break } } @available(iOS 18.0, *) public func showPicker() { guard let session = session as? ASAccessorySession else { return } var pickerItems: [ASPickerDisplayItem] = [] let image = UIImage(systemName: "lanyardcard") ?? UIImage() for uuid in serviceUUIDs { let descriptor = ASDiscoveryDescriptor() descriptor.bluetoothServiceUUID = uuid let displayItem = ASPickerDisplayItem( name: self.bleName, productImage: image, descriptor: descriptor ) pickerItems.append(displayItem) } session.showPicker(for: pickerItems) { [weak self] error in if let error = error { print("\(error)") } else { self?.start() } } } public func start() { if centralManager.isScanning { return } centralManager.scanForPeripherals(withServices: serviceUUIDs, options: options) let notice = LocalNotification.defaultSound notice.title = "ble start" notice.play() } // MARK: - CBCentralManagerDelegate public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { let restoredServices = (dict[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID]) ?? self.serviceUUIDs let restoredOptions = (dict[CBCentralManagerRestoredStateScanOptionsKey] as? [String : Any]) ?? self.options let notice = LocalNotification.defaultSound notice.title = "willRestoreState" notice.body = "\(dict)" notice.play() central.scanForPeripherals(withServices: restoredServices, options: restoredOptions) } public func centralManagerDidUpdateState(_ central: CBCentralManager) { print(">>> CentralManagerDidUpdateState: \(central.state.rawValue)") if central.state == .poweredOn { self.start() } } public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { var name = advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? peripheral.name guard let name = name, name == self.bleName else { return } let current = Date().timeIntervalSince1970 if current - (self.tagTime ?? 0.0) < 3.0 { return } self.tagTime = current let notice = LocalNotification.defaultSound notice.title = name notice.play() } } Test Results Foreground: Scanning works normally. didDiscover is triggered continuously as advertising packets arrive. Background: Scanning works, but didDiscover is triggered only once when a peripheral is first discovered, even though the peripheral advertises continuously. User Force-Kill (Swiped Away): When the app is force-killed by the user, BLE events fail to wake/relaunch the app into the background. Questions Why is didDiscover called only once in the background? Our use case requires receiving multiple scan callbacks in the background. Is CBCentralManagerScanOptionAllowDuplicatesKey completely ignored by the system in the background even when targeting specific Service UUIDs? Is there any API or recommended strategy (e.g., within AccessorySetupKit) to receive repeated discovery callbacks in the background? 2. Can an app be woken up/relaunched after being force-closed by the user? When the user manually terminates the app via the App Switcher, CoreBluetooth state restoration does not seem to relaunch it. Does ASAccessorySession change or override this restriction, or is force-kill termination strictly enforced across iOS? Any insight or documentation reference would be greatly appreciated!
Topic: App & System Services SubTopic: Hardware Tags:
1d
Reply to BLE Broadcast Cannot Relaunch User-Force-Quit App via AccessorySetupKit (iOS 26+)
Info.plist Configuration <key>NSAccessorySetupBluetoothNames</key> <array> <string>My-Device</string> </array> <key>NSAccessorySetupBluetoothServices</key> <array> <string>1111xxxx-xxxxx-xxxx-xxxx-xxxxxxxxxxxx</string> </array> <key>NSAccessorySetupKitSupports</key> <array> <string>Bluetooth</string> </array> <key>UIBackgroundModes</key> <array> <string>bluetooth-central</string> <string>remote-notification</string> </array> Relevant Code AppDelegate.swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { BluetoothService.share.activeSession() let center = UNUserNotificationCenter.current() center.delegate = self let options: UNAuthorizationOptions = [.badge, .alert, .sound] center.requestAuthorization(options: options) { granted, error in } UIApplication.shared.registerForRemoteNotifications() let notice = LocalNotification.defaultSound notice.body = "Launch App Success!" notice.play() return true } BluetoothService.swift public class BluetoothService: NSObject, CBCentralManagerDelegate { public static let share = BluetoothService() private let restoreIdentifier = "com.yourcompany.yourapp.bluetoothCentral" private lazy var centralManager = { let options: [String: Any] = [ CBCentralManagerOptionRestoreIdentifierKey: restoreIdentifier, CBCentralManagerOptionShowPowerAlertKey: true ] let centralManager = CBCentralManager(delegate: self, queue: nil, options: options) return centralManager }() public var options: [String: Any] = [CBCentralManagerScanOptionAllowDuplicatesKey: true] private var session: Any? private let bleName = "My-Device" private let serviceUUIDs = [CBUUID(string: "1111xxxx-xxxxx-xxxx-xxxx-xxxxxxxxxxxx")] private var tagTime: TimeInterval? override private init() { super.init() centralManager.delegate = self } public func activeSession() { if #available(iOS 18.0, *) { let session = ASAccessorySession() self.session = session session.activate(on: .main) { [weak self] event in guard let self = self else { return } self.handleSessionEvent(event) } } else { self.start() } } @available(iOS 18.0, *) private func handleSessionEvent(_ event: ASAccessoryEvent) { switch event.eventType { case .activated: guard let session = self.session as? ASAccessorySession else { return } if session.accessories.isEmpty { self.showPicker() } else { self.start() } case .accessoryAdded: self.start() default: break } } @available(iOS 18.0, *) public func showPicker() { guard let session = session as? ASAccessorySession else { return } var pickerItems: [ASPickerDisplayItem] = [] let image = UIImage(systemName: "lanyardcard") ?? UIImage() for uuid in serviceUUIDs { let descriptor = ASDiscoveryDescriptor() descriptor.bluetoothServiceUUID = uuid let displayItem = ASPickerDisplayItem( name: self.bleName, productImage: image, descriptor: descriptor ) pickerItems.append(displayItem) } session.showPicker(for: pickerItems) { [weak self] error in if let error = error { print("\(error)") } else { self?.start() } } } public func start() { if centralManager.isScanning { return } centralManager.scanForPeripherals(withServices: serviceUUIDs, options: options) let notice = LocalNotification.defaultSound notice.title = "ble start" notice.play() } // MARK: - CBCentralManagerDelegate public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { let restoredServices = (dict[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID]) ?? self.serviceUUIDs let restoredOptions = (dict[CBCentralManagerRestoredStateScanOptionsKey] as? [String : Any]) ?? self.options let notice = LocalNotification.defaultSound notice.title = "willRestoreState" notice.body = "\(dict)" notice.play() central.scanForPeripherals(withServices: restoredServices, options: restoredOptions) } public func centralManagerDidUpdateState(_ central: CBCentralManager) { print(">>> CentralManagerDidUpdateState: \(central.state.rawValue)") if central.state == .poweredOn { self.start() } } public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { var name = advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? peripheral.name guard let name = name, name == self.bleName else { return } let current = Date().timeIntervalSince1970 if current - (self.tagTime ?? 0.0) < 3.0 { return } self.tagTime = current let notice = LocalNotification.defaultSound notice.title = name notice.play() } } Test Results Foreground: Scanning works normally. didDiscover is triggered continuously as advertising packets arrive. Background: Scanning works, but didDiscover is triggered only once when a peripheral is first discovered, even though the peripheral advertises continuously. User Force-Kill (Swiped Away): When the app is force-killed by the user, BLE events fail to wake/relaunch the app into the background. Questions Why is didDiscover called only once in the background? Our use case requires receiving multiple scan callbacks in the background. Is CBCentralManagerScanOptionAllowDuplicatesKey completely ignored by the system in the background even when targeting specific Service UUIDs? Is there any API or recommended strategy (e.g., within AccessorySetupKit) to receive repeated discovery callbacks in the background? 2. Can an app be woken up/relaunched after being force-closed by the user? When the user manually terminates the app via the App Switcher, CoreBluetooth state restoration does not seem to relaunch it. Does ASAccessorySession change or override this restriction, or is force-kill termination strictly enforced across iOS? Any insight or documentation reference would be greatly appreciated!
Topic: App & System Services SubTopic: Hardware Tags:
Replies
Boosts
Views
Activity
1d
Reply to App Review Rejection for Guidelines 3.1.1
Therefore, we believe using non-IAP payments is justified under Guideline 3.1.4 – Hardware-Specific Content: In limited circumstances, such as when features are dependent upon specific hardware to function, the app may unlock that functionality without using in-app purchase (e.g., an astronomy app that adds features when synced with a telescope).
Replies
Boosts
Views
Activity
Jul ’25