|
Bogdan Timofte
authored
2 weeks ago
|
1
|
# Issue: UIObservationTrackingFeedbackLoop on "Designed for iPad" macOS with NavigationBarContentView
|
|
|
2
|
|
|
|
3
|
## Description
|
|
|
4
|
- Aplicația USB Meter (SwiftUI "Designed for iPad") se bloaca instant la deschiderea `MeterView` pe macOS Apple Silicon, cu eroare repetata `UIObservationTrackingFeedbackLoopDetected` in `NavigationBarContentView.updateProperties`.
|
|
|
5
|
- Freezeul apare **inainte** ca datele BLE să ajunga (instant la deschidere), diferit de problema Catalyst (care era blocata pe navigare).
|
|
|
6
|
- Condiții: macOS Apple Silicon cu "Designed for iPad" mode (UIKit hosted SwiftUI). Pe Catalyst, NU apare (AppKit toolbar remplace NavigationBarContentView).
|
|
|
7
|
|
|
|
8
|
## Impact
|
|
|
9
|
- Imposibil de deschis meter view pe "Designed for iPad" macOS
|
|
|
10
|
- Aplicatia devine complet neresponsiva, CPU la 100%
|
|
|
11
|
- Error stack: `NavigationBarContentView` intra in ciclu infinit de observare SwiftUI (sa se schimbe ceva constant)
|
|
|
12
|
|
|
|
13
|
## Root Cause Analysis
|
|
|
14
|
|
|
|
15
|
### Ipoteza initiala (INCORECTA): Update frequency pe model
|
|
|
16
|
- Prim diagnostic: "poate BLE packet-uri (~1Hz) cu 15+ @Published pe Meter triggerez body re-evaluate, care triggerez NavigationBar update, care cicleaza la infinit"
|
|
|
17
|
- **CONCLUZIUNE incorecta**: problema nu este frecventa datelor; eroarea apare instant, inainte ca Bluetooth sa se conecteze
|
|
|
18
|
|
|
|
19
|
### Adevarat root cause (GASIT): UIKit NavigationBarContentView bug
|
|
|
20
|
- NavigationBarContentView (UIKit hosted component) are implement basata pe @Observable tracking
|
|
|
21
|
- Cand SwiftUI seteaza `.navigationBarTitle()` + `.toolbar {}` pe o view push-uit in split NavigationView, UIKit intra in infinite feedback loop pe observation changes
|
|
|
22
|
- Bug specific la "Designed for iPad" pe macOS; pe Catalyst NU apare (AppKit toolbar inlocuieste NavigationBarContentView)
|
|
|
23
|
- Eroarea apare **inainte** ca bind pe meter data; doar configurarea vue modifiers triggerez loop-ul
|
|
|
24
|
|
|
|
25
|
## Investigatie si attempts
|
|
|
26
|
|
|
|
27
|
### 1. Model-layer deduplication (PARTIAL, nu era fix principal)
|
|
|
28
|
Implemented:
|
|
|
29
|
- `BluetoothSerial.swift`: RSSI averaging (sliding window 3 = ~9s), singura @Published ramasa
|
|
|
30
|
- `Meter.swift`: Eliminat @Published de la ~20 proprietati per-packet (voltage, current, power, temps, etc.). Adaugat `setIfChanged()` helper cu dedup. Singur `objectWillChange.send()` la final de `parseData()` doar daca ceva s-a schimbat. Guard pe `operationalState` oscillation
|
|
|
31
|
- `Measurements.swift`: `objectWillChange.send()` doar cand punct e adaugat (nu pe accumulation)
|
|
|
32
|
- **Rezultat**: Zero improvement pe problema Designed-for-iPad. Insa sunt optimizari bune pentru defense-in-depth.
|
|
|
33
|
|
|
|
34
|
### 2. Toolbar migration (DEPRECATED API FIX, nu era problema)
|
|
|
35
|
- Inlocuit `.navigationBarItems(trailing:)` cu `.toolbar { ToolbarItemGroup }` in MeterView
|
|
|
36
|
- Adaugat @State buffers (`navBarTitle`, `navBarShowRSSI`, `navBarRSSI`) ca shield
|
|
|
37
|
- **Rezultat**: Zero improvement. Nu era problema API veche; problema era conceptuala (UIKit nav bar pe Mac).
|
|
|
38
|
|
|
|
39
|
### 3. UIKit NavigationBarContentView bypass (SOLUTIE FINALA)
|
|
|
40
|
- Runtime detection: `ProcessInfo.processInfo.isiOSAppOnMac` (true doar pe "Designed for iPad" pe Mac)
|
|
|
41
|
- **MeterView.swift**:
|
|
|
42
|
- Adaugat `static let isMacIPadApp: Bool = ProcessInfo.processInfo.isiOSAppOnMac`
|
|
|
43
|
- Body rewritten: VStack cu conditional `macNavigationHeader` (custom back button via `dismiss()`, title, RSSI, settings)
|
|
|
44
|
- `.navigationBarHidden(true)` cand `isMacIPadApp || landscape`
|
|
|
45
|
- Creat `IOSOnlyNavBar` ViewModifier (@ViewBuilder if/else): applies `.navigationBarTitle()` + `.toolbar {}` DOAR cand `!isMacIPadApp`, passeaza content nemodificat pe Mac
|
|
|
46
|
- **MeterSettingsView.swift**: Acelasi pattern
|
|
|
47
|
- Custom `macSettingsHeader` cu back button, title, RSSI
|
|
|
48
|
- `IOSOnlySettingsNavBar` ViewModifier pentru conditional nav bar modifiers
|
|
|
49
|
- Pe real iPad: toti modifiers se aplica normal (nav bar standard UIKit)
|
|
|
50
|
|
|
|
51
|
## Solutie finala aplicata
|
|
|
52
|
|
|
|
53
|
### Fișiere modificate
|
|
|
54
|
1. **MeterView.swift** (728 → 765 lines)
|
|
|
55
|
- Adaugat `@Environment(\.dismiss)` si `isMacIPadApp` static const
|
|
|
56
|
- Rewritten body cu conditional `macNavigationHeader` (HStack cu back button, title, RSSI, settings NavigationLink)
|
|
|
57
|
- Adaugat `IOSOnlyNavBar` ViewModifier private struct cu @ViewBuilder if/else
|
|
|
58
|
|
|
|
59
|
2. **MeterSettingsView.swift**
|
|
|
60
|
- Adaugat `@Environment(\.dismiss)` si `isMacIPadApp` static const
|
|
|
61
|
- Body rewritten cu VStack + conditional `macSettingsHeader`
|
|
|
62
|
- Adaugat `IOSOnlySettingsNavBar` ViewModifier private struct
|
|
|
63
|
|
|
|
64
|
3. **Model layer** (executat anterior, nu direct pentru fix), dar relevant:
|
|
|
65
|
- BluetoothSerial.swift: RSSI averaging
|
|
|
66
|
- Meter.swift: @Published dedup
|
|
|
67
|
- Measurements.swift: objectWillChange fix
|
|
|
68
|
|
|
|
69
|
## Test status
|
|
|
70
|
- ✅ Compileaza fara erori
|
|
|
71
|
- ✅ "Designed for iPad" mode pe macOS: MeterView si MeterSettingsView deschid fara blocaj
|
|
|
72
|
- ⚠️ Real iPad: NU am iPad disponibil sa verific ca problema nu apare (nu am device pentru test)
|
|
|
73
|
- UIKit nav bar modifiers se aplica normal pe real iPad (path aplicarii e diferit din UIKit sandbox)
|
|
|
74
|
- Expect sa fie OK dar nu am certitudine
|
|
|
75
|
|
|
|
76
|
## Commit-uri
|
|
|
77
|
- [recent] - UIKit NavBar bypass pe "Designed for iPad", MeterView + MeterSettingsView custom headers
|
|
|
78
|
|
|
|
79
|
## Verificare
|
|
|
80
|
- pe macOS Apple Silicon + "Designed for iPad": deschide app, selecteaza meter, apasa sa o deschizi
|
|
|
81
|
- MeterView trebuie sa deschida fara freeze
|
|
|
82
|
- MeterSettingsView (pe gear icon) trebuie sa deschida fara freeze
|
|
|
83
|
- Back button sa revina la lista
|
|
|
84
|
|
|
|
85
|
## Lessons learned
|
|
|
86
|
- Nu toate feedback loop-urile ObservableObject sunt cauzate de frecventa datelor
|
|
|
87
|
- UIKit pe macOS "Designed for iPad" are bugs nedocumentate in observation tracking cand SwiftUI configureaza nav bar
|
|
|
88
|
- Catalyst (AppKit) nu are problema (toolbar separat)
|
|
|
89
|
- Workaround: bypass-ul ca sa nu setam nav bar modifiers pe Mac
|
|
|
90
|
|
|
|
91
|
## Notes pentru viitor
|
|
|
92
|
- Aceasta solutie functioneaza pe "Designed for iPad" + Catalyst
|
|
|
93
|
- Nu am testat pe real iPad (nu am device); expect sa fie OK din arhitectura (modifiers se aplica)
|
|
|
94
|
- Daca problema apare pe real iPad in viitor: mai multi custom headers nu scala; s-ar putea reveni la NavigationView + sheet-based nav (cum am facut pe Catalyst in issue #001)
|