From 413cd90297155601e8f291c404ad304d007e3942 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Mon, 24 Aug 2026 11:24:31 +0200 Subject: [PATCH] Render the trailing switch on iOS list items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `trailingSwitch()` set trailing_type=switch, but the iOS renderer's switch case was an EmptyView stub ("handled at a higher level" — no such level exists), so the row silently rendered nothing. Android has always drawn a Material Switch here. Render a real SwiftUI Toggle with local state and the same echo-prevention sync as NativeUIToggleRenderer, reporting through the row's on_trailing_change callback with a bool payload — exactly what Android sends. --- resources/ios/NativeUIListItemRenderer.swift | 54 +++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/resources/ios/NativeUIListItemRenderer.swift b/resources/ios/NativeUIListItemRenderer.swift index b9f7ec2..e9bc940 100644 --- a/resources/ios/NativeUIListItemRenderer.swift +++ b/resources/ios/NativeUIListItemRenderer.swift @@ -296,7 +296,10 @@ struct NativeUIListItemRenderer: View { .accessibilityLabel(effectiveTrailingA11y) } case "switch": - EmptyView() // Switch requires state management - handled at a higher level + // Real Toggle with local state + echo prevention (same pattern as + // NativeUIToggleRenderer). Android has always rendered a Material + // Switch for this trailing type; this brings iOS to parity. + ListItemTrailingSwitch(node: node, serverValue: checked, changeCb: changeCb) case "checkbox": selectionControl( glyph: checked ? "checkmark.square.fill" : "square", @@ -335,3 +338,52 @@ private func listItemMenuItem(_ item: NativeUINode) -> some View { .tint(isDestructive ? .red : nil) } } + +/// Trailing switch for `trailingSwitch()` rows. Holds its own on/off state so +/// the thumb animates instantly on tap, syncs from the server value with the +/// same echo-prevention as `NativeUIToggleRenderer`, and reports changes over +/// the row's `on_trailing_change` callback (bool payload — identical to what +/// the Android renderer has always sent). +private struct ListItemTrailingSwitch: View { + let node: NativeUINode + let serverValue: Bool + let changeCb: Int + + @ObservedObject private var themeStore = NativeUITheme.shared + @Environment(\.colorScheme) private var colorScheme + + @State private var isOn: Bool = false + @State private var lastSentValue: Bool = false + @State private var initialized: Bool = false + + var body: some View { + let theme = themeStore.resolve(for: colorScheme) + + Toggle("", isOn: $isOn) + .labelsHidden() + .tint(theme.primary) + .disabled(node.props.getBool("disabled") || changeCb == 0) + .onAppear { + if !initialized { + isOn = serverValue + lastSentValue = serverValue + initialized = true + } + } + .onChange(of: serverValue) { _, new in + // Ignore server pushes that echo our last commit; accept + // genuine programmatic updates. + if new != lastSentValue { + isOn = new + lastSentValue = new + } + } + .onChange(of: isOn) { _, new in + guard new != lastSentValue else { return } + lastSentValue = new + if changeCb != 0 { + NativeUIBridge.sendToggleChangeEvent(changeCb, nodeId: node.id, value: new) + } + } + } +}