USB-Meter / Documentation / Issues / 002_2026-03-24_18-45_designed-for-ipad-nav-feedback-loop.md
1 contributor
94 lines | 6.029kb

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

  1. 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
  2. MeterSettingsView.swift

    • Adaugat @Environment(\.dismiss) si isMacIPadApp static const
    • Body rewritten cu VStack + conditional macSettingsHeader
    • Adaugat IOSOnlySettingsNavBar ViewModifier private struct
  3. 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)