Oh no, we were so focused on getting the manufacturer data right that we forgot to fill in the plist key. In other words, it works like a charm, we can now set up all our existing and future products with ASK π₯³ Thanks for your help.
Post
Replies
Boosts
Views
Activity
I filed feedbacks for different advertisements used by 3 different products:
FB14052487
FB14051395
FB14051767
Bluetooth and ASK logging profiles are installed and I attached the sysdiagnose to each.
About the lenght of the advertisements: I assume we're using extended advertising, I'll check with the hardware people and update you.
The missing discoverability flag seem like a bug. I can try to have this fixed, but unfortunately we cannot fix this for already released products.
This advertisement contains the "General Discoverable" flag and is also not discovered by ASK:
0x02010A1107B24CE98BC8AC6381BE45D21B5CB41ECE08FFD2OA003F1300000909**************0000000000000000000000000000000000000000000000
The not properly configured ASDiscoveryDescriptor was caused by pasting the wrong code to the message. I was using your suggestion, unfortunately without success.
The info.plist looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAccessorySetupBluetoothServices</key>
<array>
<string>CE1EB45C-1BD2-45BE-8163-ACC88BE94CB2</string>
</array>
<key>NSAccessorySetupKitSupports</key>
<array>
<string>Bluetooth</string>
</array>
</dict>
</plist>
Hello! We used LightBlue on Android to capture the complete BLE advertisement for two products we try to pair with AccessorySetupKit. I masked the local name of product 2, because it's not released yet.
Product 1 has this BLE advertisement:
0x0B097461747773325F626C6508FFD20A00350000001107B24CE98BC8AC6381BE45D21B5CB41ECE0000000000000000000000000000000000000000000000
The manufacturer data as seen from CoreBluetooth (CBAdvertisementDataManufacturerDataKey) is 0xd20a0035000000
This is the desciptor we are using:
let descriptor1 = ASDiscoveryDescriptor()
descriptor1.bluetoothServiceUUID = CBUUID(string: "CE1EB45C-1BD2-45BE-8163-ACC88BE94CB2")
descriptor1.bluetoothManufacturerDataBlob = Data([0x00, 0x35, 0x00, 0x00, 0x00])
descriptor1.bluetoothManufacturerDataMask = Data([0xff, 0xff, 0xff, 0xff, 0xff])
descriptor1.bluetoothCompanyIdentifier = .init(0x0ad2)
Product 2 has this BLE advertisement:
0x02010A1107B24CE98BC8AC6381BE45D21B5CB41ECE08FFOAD2003F1300000909**************0000000000000000000000000000000000000000000000
The manufacturer data as seen from CoreBluetooth (CBAdvertisementDataManufacturerDataKey) is 0xd20affd20a0000000000
This is the desciptor we are using:
let descriptor2 = ASDiscoveryDescriptor()
descriptor2.bluetoothServiceUUID = CBUUID(string: "CE1EB45C-1BD2-45BE-8163-ACC88BE94CB2")
descriptor2.bluetoothManufacturerDataBlob = Data([0xFF, 0xD2, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00])
descriptor2.bluetoothManufacturerDataMask = Data([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
descriptor2.bluetoothCompanyIdentifier = .init(0x0ad2)
Both products can be found with CoreBluetooth, however, both are not visible when using the AccessorySetupKit. Is there anything unexpected the advertisement? Do we need to change anything in the desciptors to match the advertisements?
I'm sorry, but I have to ask again to make sure I understand the issue. When we use CoreBluetooth's CBCentralManager to scan for peripherals, we'll see this data in the advertisementData dictionary (CBAdvertisementDataManufacturerDataKey):
0xd2 0x0a 0xb1 0xb2 0xb3 0xb4 0xb5 0xb6 0xb7 0xb8
I understand that AccessorySetupKit expects a different byte order and that the ASDiscoveryDescriptor should be set up as follows
let descriptor = ASDiscoveryDescriptor()
descriptor.bluetoothServiceUUID = CBUUID(string: "ce1eb45c-1bd2-45be-8163-acc88be94cb2")
descriptor.bluetoothCompanyIdentifier = .init(0x0ad2)
descriptor.bluetoothManufacturerDataBlob = Data([0xb2, 0xb1, 0xb4, 0xb3, 0xb6, 0xb5, 0xb8, 0xb7]) // ????
descriptor.bluetoothManufacturerDataMask = Data([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
Could you please confirm if this setup is correct? If not, I would greatly appreciate any corrections or guidance you could provide.
I find the differing byte orders between the advertisementData dictionary and the ASDiscoveryDescriptor a bit confusing. If the above setup is correct, I will proceed to file feedback with logs, as we are currently unable to detect our accessory using AccessorySetupKit.
Ok, probably this is a bug in SwiftUI. We created an example that allows reproducing the crash on an iOS 17 device.
We filed feedback FB13238006 about this. Feel free to duplicate when you're affected by this.
import SwiftUI
/// SwiftUI Toolbar crasher
/// =======================
///
/// Steps to reproduce
/// ------------------
/// * launch this app on a physical device running iOS 17 (any stable or beta release).
/// * Tap the center button ("Go to view 2")
/// * Tap the center button ("Go to view 3")
/// * Tap the center button ("Go to view 4")
/// * Tap the center button ("Go to view 5")
/// * Tap the center button ("Pop to root π£")
///
/// Expected result
/// ---------------
/// * The app navigates back to the root screen.
///
/// Actual result
/// -------------
/// * The app crashes in 30-50% of the attempts.
///
///
class ToolbarCrasher: ObservableObject {
@Published var navTree: NavigationTree? = nil
}
enum NavigationTree {
case one(One?)
var one: One? {
switch self {
case let .one(one): one
}
}
var isOne: Bool {
switch self {
case .one: true
}
}
enum One {
case two(Two?)
var two: Two? {
switch self {
case let .two(two): two
}
}
var isTwo: Bool {
switch self {
case .two: true
}
}
enum Two {
case three(Three?)
var three: Three? {
switch self {
case let .three(three): three
}
}
var isThree: Bool {
switch self {
case .three: true
}
}
enum Three {
case four
var isFour: Bool {
switch self {
case .four: true
}
}
}
}
}
}
struct ContentView: View {
@StateObject var toolbarCrasher: ToolbarCrasher = .init()
var body: some View {
NavigationStack {
VStack {
Text("Root View")
Button(
action: { toolbarCrasher.navTree = .one(nil) },
label: { Text("Go to view 2") }
)
}
.navigationDestination(
isPresented: .init(
get: { toolbarCrasher.navTree?.isOne ?? false },
set: { isForward in if isForward { toolbarCrasher.navTree = nil } }
),
destination: {
ContentView2(toolbarCrasher: toolbarCrasher)
}
)
.padding()
.toolbar {
ToolbarItem(placement: .topBarTrailing, content: {
Button(
action: { toolbarCrasher.navTree = nil },
label: { Text("Pop to root π£") }
)
})
}
}
}
}
struct ContentView2: View {
@ObservedObject var toolbarCrasher: ToolbarCrasher
var body: some View {
VStack {
Text("View 2")
Button(
action: { toolbarCrasher.navTree = .one(.two(nil)) },
label: { Text("Go to view 3") }
)
}
.toolbar {
ToolbarItem(placement: .topBarTrailing, content: {
Button(
action: { toolbarCrasher.navTree = nil },
label: { Text("Pop to root π£") }
)
})
}
.navigationDestination(
isPresented: .init(
get: { toolbarCrasher.navTree?.one?.isTwo ?? false },
set: { isForward in if isForward { toolbarCrasher.navTree = .one(nil) } }
),
destination: {
ContentView3(toolbarCrasher: toolbarCrasher)
}
)
.padding()
}
}
struct ContentView3: View {
@ObservedObject var toolbarCrasher: ToolbarCrasher
var body: some View {
VStack {
Text("View 3")
Button(
action: { toolbarCrasher.navTree = .one(.two(.three(nil))) },
label: { Text("Go to view 4") }
)
}
.toolbar {
ToolbarItem(placement: .topBarTrailing, content: {
Button(
action: { toolbarCrasher.navTree = nil },
label: { Text("Pop to root π£") }
)
})
}
.navigationDestination(
isPresented: .init(
get: { toolbarCrasher.navTree?.one?.two?.isThree ?? false },
set: { isForward in if isForward { toolbarCrasher.navTree = .one(.two(nil)) } }
),
destination: {
ContentView4(state: toolbarCrasher)
}
)
.padding()
}
}
struct ContentView4: View {
@ObservedObject var state: ToolbarCrasher
var body: some View {
VStack {
Text("View 4")
Button(
action: { state.navTree = .one(.two(.three(.four))) },
label: { Text("Go to view 5") }
)
}
.toolbar {
ToolbarItem(placement: .topBarTrailing, content: {
Button(
action: { state.navTree = nil },
label: { Text("Pop to root π£") }
)
})
}
.navigationDestination(
isPresented: .init(
get: { state.navTree?.one?.two?.three?.isFour ?? false },
set: { isForward in if isForward { state.navTree = .one(.two(.three(nil))) } }
),
destination: {
ContentView5(toolbarCrasher: state)
}
)
.padding()
}
}
struct ContentView5: View {
@ObservedObject var toolbarCrasher: ToolbarCrasher
var body: some View {
Text("View 5")
Button(
action: { toolbarCrasher.navTree = nil },
label: { Text("Pop to root π£") }
)
}
}
#Preview {
ContentView()
}
@olonsky Thanks for the example. I think the root cause is that in our code bases the navigation is changed in two different runloops, giving SwiftUI conflicting information about the desired navigation state. The fix is to make sure that the navigation for all views is derived from a single value (nested enum or an array) that is changed in a single step.
btw: I don't understand why you need to defer changing the path using DispatchQueue.main.async. By removing this hack, the crash is gone.
.onReceive(coordinator.$path) { value in
// DispatchQueue.main.async {
path = value
// }
}