Issue: UIObservationTrackingFeedbackLoop on "Designed for iPad" macOS with NavigationBarContentView
Description
- 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.
- Freezeul apare inainte ca datele BLE să ajunga (instant la deschidere), diferit de problema Catalyst (care era blocata pe navigare).
- Condiții: macOS Apple Silicon cu "Designed for iPad" mode (UIKit hosted SwiftUI). Pe Catalyst, NU apare (AppKit toolbar remplace NavigationBarContentView).
Impact
- Imposibil de deschis meter view pe "Designed for iPad" macOS
- Aplicatia devine complet neresponsiva, CPU la 100%
- Error stack:
NavigationBarContentView intra in ciclu infinit de observare SwiftUI (sa se schimbe ceva constant)
Root Cause Analysis
Ipoteza initiala (INCORECTA): Update frequency pe model
- Prim diagnostic: "poate BLE packet-uri (~1Hz) cu 15+ @Published pe Meter triggerez body re-evaluate, care triggerez NavigationBar update, care cicleaza la infinit"
- CONCLUZIUNE incorecta: problema nu este frecventa datelor; eroarea apare instant, inainte ca Bluetooth sa se conecteze
Adevarat root cause (GASIT): UIKit NavigationBarContentView bug
- NavigationBarContentView (UIKit hosted component) are implement basata pe @Observable tracking
- Cand SwiftUI seteaza
.navigationBarTitle() + .toolbar {} pe o view push-uit in split NavigationView, UIKit intra in infinite feedback loop pe observation changes
- Bug specific la "Designed for iPad" pe macOS; pe Catalyst NU apare (AppKit toolbar inlocuieste NavigationBarContentView)
- Eroarea apare inainte ca bind pe meter data; doar configurarea vue modifiers triggerez loop-ul
Investigatie si attempts
1. Model-layer deduplication (PARTIAL, nu era fix principal)
Implemented:
- BluetoothSerial.swift: RSSI averaging (sliding window 3 = ~9s), singura @Published ramasa
- 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
- Measurements.swift: objectWillChange.send() doar cand punct e adaugat (nu pe accumulation)
- Rezultat: Zero improvement pe problema Designed-for-iPad. Insa sunt optimizari bune pentru defense-in-depth.
2. Toolbar migration (DEPRECATED API FIX, nu era problema)
- Inlocuit
.navigationBarItems(trailing:) cu .toolbar { ToolbarItemGroup } in MeterView
- Adaugat @State buffers (
navBarTitle, navBarShowRSSI, navBarRSSI) ca shield
- Rezultat: Zero improvement. Nu era problema API veche; problema era conceptuala (UIKit nav bar pe Mac).
3. UIKit NavigationBarContentView bypass (SOLUTIE FINALA)
- Runtime detection:
ProcessInfo.processInfo.isiOSAppOnMac (true doar pe "Designed for iPad" pe Mac)
- MeterView.swift:
- Adaugat
static let isMacIPadApp: Bool = ProcessInfo.processInfo.isiOSAppOnMac
- Body rewritten: VStack cu conditional
macNavigationHeader (custom back button via dismiss(), title, RSSI, settings)
.navigationBarHidden(true) cand isMacIPadApp || landscape
- Creat
IOSOnlyNavBar ViewModifier (@ViewBuilder if/else): applies .navigationBarTitle() + .toolbar {} DOAR cand !isMacIPadApp, passeaza content nemodificat pe Mac
- MeterSettingsView.swift: Acelasi pattern
- Custom
macSettingsHeader cu back button, title, RSSI
IOSOnlySettingsNavBar ViewModifier pentru conditional nav bar modifiers
- Pe real iPad: toti modifiers se aplica normal (nav bar standard UIKit)
Solutie finala aplicata
Fișiere modificate
MeterView.swift (728 → 765 lines)
- Adaugat
@Environment(\.dismiss) si isMacIPadApp static const
- Rewritten body cu conditional
macNavigationHeader (HStack cu back button, title, RSSI, settings NavigationLink)
- Adaugat
IOSOnlyNavBar ViewModifier private struct cu @ViewBuilder if/else
MeterSettingsView.swift
- Adaugat
@Environment(\.dismiss) si isMacIPadApp static const
- Body rewritten cu VStack + conditional
macSettingsHeader
- Adaugat
IOSOnlySettingsNavBar ViewModifier private struct
Model layer (executat anterior, nu direct pentru fix), dar relevant:
- BluetoothSerial.swift: RSSI averaging
- Meter.swift: @Published dedup
- Measurements.swift: objectWillChange fix
Test status
- ✅ Compileaza fara erori
- ✅ "Designed for iPad" mode pe macOS: MeterView si MeterSettingsView deschid fara blocaj
- ⚠️ Real iPad: NU am iPad disponibil sa verific ca problema nu apare (nu am device pentru test)
- UIKit nav bar modifiers se aplica normal pe real iPad (path aplicarii e diferit din UIKit sandbox)
- Expect sa fie OK dar nu am certitudine
Commit-uri
- [recent] - UIKit NavBar bypass pe "Designed for iPad", MeterView + MeterSettingsView custom headers
Verificare
- pe macOS Apple Silicon + "Designed for iPad": deschide app, selecteaza meter, apasa sa o deschizi
- MeterView trebuie sa deschida fara freeze
- MeterSettingsView (pe gear icon) trebuie sa deschida fara freeze
- Back button sa revina la lista
Lessons learned
- Nu toate feedback loop-urile ObservableObject sunt cauzate de frecventa datelor
- UIKit pe macOS "Designed for iPad" are bugs nedocumentate in observation tracking cand SwiftUI configureaza nav bar
- Catalyst (AppKit) nu are problema (toolbar separat)
- Workaround: bypass-ul ca sa nu setam nav bar modifiers pe Mac
Notes pentru viitor
- Aceasta solutie functioneaza pe "Designed for iPad" + Catalyst
- Nu am testat pe real iPad (nu am device); expect sa fie OK din arhitectura (modifiers se aplica)
- 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)