From 066a977ddc46483401c181ad883aff1932330f2f Mon Sep 17 00:00:00 2001 From: Vitor Silveira Date: Mon, 23 Jun 2025 20:57:35 -0300 Subject: [PATCH 1/4] #188: Add support to swiping to dismiss keyboard for UIKitBackend --- .../Sources/NotesExample/ContentView.swift | 9 ++-- .../Environment/EnvironmentValues.swift | 3 ++ .../Values/ScrollDismissesKeyboardMode.swift | 20 +++++++++ .../ScrollDismissesKeyboardModifier.swift | 41 +++++++++++++++++++ .../UIKitBackend/UIKitBackend+Control.swift | 10 +++++ 5 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 Sources/SwiftCrossUI/Values/ScrollDismissesKeyboardMode.swift create mode 100644 Sources/SwiftCrossUI/Views/Modifiers/ScrollDismissesKeyboardModifier.swift diff --git a/Examples/Sources/NotesExample/ContentView.swift b/Examples/Sources/NotesExample/ContentView.swift index 05f74d7c5..d3647b0e3 100644 --- a/Examples/Sources/NotesExample/ContentView.swift +++ b/Examples/Sources/NotesExample/ContentView.swift @@ -38,10 +38,10 @@ struct ContentView: View { var textEditorBackground: Color { switch colorScheme { - case .light: - Color(0.8, 0.8, 0.8) - case .dark: - Color(0.18, 0.18, 0.18) + case .light: + Color(0.8, 0.8, 0.8) + case .dark: + Color(0.18, 0.18, 0.18) } } @@ -142,6 +142,7 @@ struct ContentView: View { .padding() .background(textEditorBackground) .cornerRadius(4) + .scrollDismissesKeyboard(.interactively) } } .padding() diff --git a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift index 3d709f9aa..0f5111a98 100644 --- a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift +++ b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift @@ -77,6 +77,8 @@ public struct EnvironmentValues { /// Whether user interaction is enabled. Set by ``View/disabled(_:)``. public var isEnabled: Bool + + public var scrollDismissesKeyboardMode: ScrollDismissesKeyboardMode /// Called by view graph nodes when they resize due to an internal state /// change and end up changing size. Each view graph node sets its own @@ -196,6 +198,7 @@ public struct EnvironmentValues { listStyle = .default toggleStyle = .button isEnabled = true + scrollDismissesKeyboardMode = .never } /// Returns a copy of the environment with the specified property set to the diff --git a/Sources/SwiftCrossUI/Values/ScrollDismissesKeyboardMode.swift b/Sources/SwiftCrossUI/Values/ScrollDismissesKeyboardMode.swift new file mode 100644 index 000000000..5fbc061f1 --- /dev/null +++ b/Sources/SwiftCrossUI/Values/ScrollDismissesKeyboardMode.swift @@ -0,0 +1,20 @@ +/// The ways that scrollable content can interact with the software keyboard. +/// +/// Use this type in a call to the ``View/scrollDismissesKeyboard(_:)`` +/// modifier to specify the dismissal behavior of scrollable views. +public enum ScrollDismissesKeyboardMode: Sendable { + /// Dismiss the keyboard as soon as scrolling starts. + case immediately + + /// Enable people to interactively dismiss the keyboard as part of the + /// scroll operation. + /// + /// The software keyboard's position tracks the gesture that drives the + /// scroll operation if the gesture crosses into the keyboard's area of the + /// display. People can dismiss the keyboard by scrolling it off the + /// display, or reverse the direction of the scroll to cancel the dismissal. + case interactively + + /// Never dismiss the keyboard automatically as a result of scrolling. + case never +} diff --git a/Sources/SwiftCrossUI/Views/Modifiers/ScrollDismissesKeyboardModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/ScrollDismissesKeyboardModifier.swift new file mode 100644 index 000000000..e239efb19 --- /dev/null +++ b/Sources/SwiftCrossUI/Views/Modifiers/ScrollDismissesKeyboardModifier.swift @@ -0,0 +1,41 @@ +extension View { + /// Configures the behavior in which scrollable content interacts with + /// the software keyboard. + /// + /// You use this modifier to customize how scrollable content interacts + /// with the software keyboard. For example, you can specify a value of + /// ``ScrollDismissesKeyboardMode/immediately`` to indicate that you + /// would like scrollable content to immediately dismiss the keyboard if + /// present when a scroll drag gesture begins. + /// + /// @State private var text = "" + /// + /// ScrollView { + /// TextField("Prompt", text: $text) + /// ForEach(0 ..< 50) { index in + /// Text("\(index)") + /// .padding() + /// } + /// } + /// .scrollDismissesKeyboard(.immediately) + /// + /// You can also use this modifier to customize the keyboard dismissal + /// behavior for other kinds of scrollable views, like a ``List`` or a + /// ``TextEditor``. + /// + /// By default, scrollable content never automatically dismisses the keyboard. + /// Pass a different value of ``ScrollDismissesKeyboardMode`` to change this behavior. + /// For example, ``ScrollDismissesKeyboardMode/interactively`` allows the keyboard to dismiss + /// as the user scrolls. Note that ``TextEditor`` may still use a different + /// default to preserve expected editing behavior. + /// + /// - Parameter mode: The keyboard dismissal mode that scrollable content + /// uses. + /// + /// - Returns: A view that uses the specified keyboard dismissal mode. + public func scrollDismissesKeyboard(_ mode: ScrollDismissesKeyboardMode) -> some View { + EnvironmentModifier(self) { environment in + environment.with(\.scrollDismissesKeyboardMode, mode) + } + } +} diff --git a/Sources/UIKitBackend/UIKitBackend+Control.swift b/Sources/UIKitBackend/UIKitBackend+Control.swift index 0b813a8dc..cbd13da40 100644 --- a/Sources/UIKitBackend/UIKitBackend+Control.swift +++ b/Sources/UIKitBackend/UIKitBackend+Control.swift @@ -318,6 +318,16 @@ extension UIKitBackend { textEditorWidget.child.inputAccessoryView = nil } #endif + + textEditorWidget.child.alwaysBounceVertical = environment.scrollDismissesKeyboardMode != .never + textEditorWidget.child.keyboardDismissMode = switch environment.scrollDismissesKeyboardMode { + case .immediately: + textEditorWidget.child.inputAccessoryView == nil ? .onDrag : .onDragWithAccessory + case .interactively: + textEditorWidget.child.inputAccessoryView == nil ? .interactive : .interactiveWithAccessory + case .never: + .none + } } public func setContent(ofTextEditor textEditor: Widget, to content: String) { From 6b4dfcf33ed1ebfe27ea300d0157cdb3b5690da5 Mon Sep 17 00:00:00 2001 From: Vitor Silveira Date: Mon, 23 Jun 2025 21:59:11 -0300 Subject: [PATCH 2/4] #188: Make the method only executable on iOS --- .../UIKitBackend/UIKitBackend+Control.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/UIKitBackend/UIKitBackend+Control.swift b/Sources/UIKitBackend/UIKitBackend+Control.swift index cbd13da40..c2cdd8eab 100644 --- a/Sources/UIKitBackend/UIKitBackend+Control.swift +++ b/Sources/UIKitBackend/UIKitBackend+Control.swift @@ -317,17 +317,17 @@ extension UIKitBackend { } else { textEditorWidget.child.inputAccessoryView = nil } - #endif - textEditorWidget.child.alwaysBounceVertical = environment.scrollDismissesKeyboardMode != .never - textEditorWidget.child.keyboardDismissMode = switch environment.scrollDismissesKeyboardMode { - case .immediately: - textEditorWidget.child.inputAccessoryView == nil ? .onDrag : .onDragWithAccessory - case .interactively: - textEditorWidget.child.inputAccessoryView == nil ? .interactive : .interactiveWithAccessory - case .never: - .none - } + textEditorWidget.child.alwaysBounceVertical = environment.scrollDismissesKeyboardMode != .never + textEditorWidget.child.keyboardDismissMode = switch environment.scrollDismissesKeyboardMode { + case .immediately: + textEditorWidget.child.inputAccessoryView == nil ? .onDrag : .onDragWithAccessory + case .interactively: + textEditorWidget.child.inputAccessoryView == nil ? .interactive : .interactiveWithAccessory + case .never: + .none + } + #endif } public func setContent(ofTextEditor textEditor: Widget, to content: String) { From e5b77895738585b25c8b3d377e907815a843a781 Mon Sep 17 00:00:00 2001 From: Vitor Silveira Date: Tue, 24 Jun 2025 13:52:46 -0300 Subject: [PATCH 3/4] #188: Add support to swiping to dismiss keyboard for ScrollView --- .../Sources/NotesExample/ContentView.swift | 8 ++++---- Sources/AppKitBackend/AppKitBackend.swift | 4 ++++ Sources/CursesBackend/CursesBackend.swift | 2 ++ Sources/Gtk3Backend/Gtk3Backend.swift | 4 ++++ Sources/GtkBackend/GtkBackend.swift | 4 ++++ Sources/LVGLBackend/LVGLBackend.swift | 2 ++ Sources/QtBackend/QtBackend.swift | 2 ++ Sources/SwiftCrossUI/Backend/AppBackend.swift | 18 ++++++++++++++++++ .../Environment/EnvironmentValues.swift | 2 +- .../ScrollDismissesKeyboardModifier.swift | 6 +++--- Sources/SwiftCrossUI/Views/ScrollView.swift | 1 + .../UIKitBackend/UIKitBackend+Container.swift | 18 ++++++++++++++++++ .../UIKitBackend/UIKitBackend+Control.swift | 10 +++++----- Sources/WinUIBackend/WinUIBackend.swift | 4 ++++ 14 files changed, 72 insertions(+), 13 deletions(-) diff --git a/Examples/Sources/NotesExample/ContentView.swift b/Examples/Sources/NotesExample/ContentView.swift index d3647b0e3..469fe7e7f 100644 --- a/Examples/Sources/NotesExample/ContentView.swift +++ b/Examples/Sources/NotesExample/ContentView.swift @@ -38,10 +38,10 @@ struct ContentView: View { var textEditorBackground: Color { switch colorScheme { - case .light: - Color(0.8, 0.8, 0.8) - case .dark: - Color(0.18, 0.18, 0.18) + case .light: + Color(0.8, 0.8, 0.8) + case .dark: + Color(0.18, 0.18, 0.18) } } diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index 62f9e4b46..62dec277a 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -861,6 +861,10 @@ public final class AppKitBackend: AppBackend { return scrollView } + public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) { + let scrollView = scrollView as! NSScrollView + } + public func setScrollBarPresence( ofScrollContainer scrollView: Widget, hasVerticalScrollBar: Bool, diff --git a/Sources/CursesBackend/CursesBackend.swift b/Sources/CursesBackend/CursesBackend.swift index 311914aaa..407567859 100644 --- a/Sources/CursesBackend/CursesBackend.swift +++ b/Sources/CursesBackend/CursesBackend.swift @@ -84,6 +84,8 @@ public final class CursesBackend: AppBackend { public func setSpacing(ofHStack container: Widget, to spacing: Int) {} + public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) {} + public func createTextView() -> Widget { let label = Label("") label.width = Dim.fill() diff --git a/Sources/Gtk3Backend/Gtk3Backend.swift b/Sources/Gtk3Backend/Gtk3Backend.swift index 9b8c6411d..84ff924ef 100644 --- a/Sources/Gtk3Backend/Gtk3Backend.swift +++ b/Sources/Gtk3Backend/Gtk3Backend.swift @@ -539,6 +539,10 @@ public final class Gtk3Backend: AppBackend { return scrollView } + public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) { + let scrollView = scrollView as! ScrolledWindow + } + public func setScrollBarPresence( ofScrollContainer scrollView: Widget, hasVerticalScrollBar: Bool, diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index 094186f91..4d969ff52 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -503,6 +503,10 @@ public final class GtkBackend: AppBackend { return scrollView } + public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) { + let scrollView = scrollView as! ScrolledWindow + } + public func setScrollBarPresence( ofScrollContainer scrollView: Widget, hasVerticalScrollBar: Bool, diff --git a/Sources/LVGLBackend/LVGLBackend.swift b/Sources/LVGLBackend/LVGLBackend.swift index 918f8df45..ada701df3 100644 --- a/Sources/LVGLBackend/LVGLBackend.swift +++ b/Sources/LVGLBackend/LVGLBackend.swift @@ -156,6 +156,8 @@ public final class LVGLBackend: AppBackend { } } + public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) {} + public func createTextView() -> Widget { return Widget { parent in let label = LVLabel(with: parent) diff --git a/Sources/QtBackend/QtBackend.swift b/Sources/QtBackend/QtBackend.swift index e4a35826c..59ed595bf 100644 --- a/Sources/QtBackend/QtBackend.swift +++ b/Sources/QtBackend/QtBackend.swift @@ -108,6 +108,8 @@ public struct QtBackend: AppBackend { } } + public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) {} + public func setSpacing(ofHStack widget: Widget, to spacing: Int) { (widget.layout as! QHBoxLayout).spacing = Int32(spacing) } diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index 634db2f9c..31c88252b 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -264,6 +264,20 @@ public protocol AppBackend { /// Creates a scrollable single-child container wrapping the given widget. func createScrollContainer(for child: Widget) -> Widget + /// Updates a scroll container with environment-specific values. + /// + /// This method is primarily used on iOS to apply environment changes + /// that affect the scroll view’s behavior, such as keyboard dismissal mode. + /// It allows the backend to update UIKit-specific properties (e.g. `keyboardDismissMode`) + /// when the environment changes. + /// + /// - Parameters: + /// - scrollView: The scroll container widget previously created by `createScrollContainer(for:)`. + /// - environment: The current `EnvironmentValues` to apply. + func updateScrollContainer( + _ scrollView: Widget, + environment: EnvironmentValues + ) /// Sets the presence of scroll bars along each axis of a scroll container. func setScrollBarPresence( ofScrollContainer scrollView: Widget, @@ -741,6 +755,10 @@ extension AppBackend { todo() } + public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) { + todo() + } + public func setScrollBarPresence( ofScrollContainer scrollView: Widget, hasVerticalScrollBar: Bool, diff --git a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift index 0f5111a98..d58fce004 100644 --- a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift +++ b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift @@ -198,7 +198,7 @@ public struct EnvironmentValues { listStyle = .default toggleStyle = .button isEnabled = true - scrollDismissesKeyboardMode = .never + scrollDismissesKeyboardMode = .interactively } /// Returns a copy of the environment with the specified property set to the diff --git a/Sources/SwiftCrossUI/Views/Modifiers/ScrollDismissesKeyboardModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/ScrollDismissesKeyboardModifier.swift index e239efb19..0655a8e90 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/ScrollDismissesKeyboardModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/ScrollDismissesKeyboardModifier.swift @@ -23,10 +23,10 @@ extension View { /// behavior for other kinds of scrollable views, like a ``List`` or a /// ``TextEditor``. /// - /// By default, scrollable content never automatically dismisses the keyboard. + /// By default, scrollable content dismisses the keyboard interactively as the user scrolls. /// Pass a different value of ``ScrollDismissesKeyboardMode`` to change this behavior. - /// For example, ``ScrollDismissesKeyboardMode/interactively`` allows the keyboard to dismiss - /// as the user scrolls. Note that ``TextEditor`` may still use a different + /// For example, use ``ScrollDismissesKeyboardMode/never`` to prevent the keyboard from + /// dismissing automatically. Note that ``TextEditor`` may still use a different /// default to preserve expected editing behavior. /// /// - Parameter mode: The keyboard dismissal mode that scrollable content diff --git a/Sources/SwiftCrossUI/Views/ScrollView.swift b/Sources/SwiftCrossUI/Views/ScrollView.swift index d375c1fa8..b3d7f1bc5 100644 --- a/Sources/SwiftCrossUI/Views/ScrollView.swift +++ b/Sources/SwiftCrossUI/Views/ScrollView.swift @@ -143,6 +143,7 @@ public struct ScrollView: TypeSafeView, View { hasVerticalScrollBar: hasVerticalScrollBar, hasHorizontalScrollBar: hasHorizontalScrollBar ) + backend.updateScrollContainer(widget, environment: environment) } else { finalResult = childResult } diff --git a/Sources/UIKitBackend/UIKitBackend+Container.swift b/Sources/UIKitBackend/UIKitBackend+Container.swift index 5375a24aa..a31d3b1bd 100644 --- a/Sources/UIKitBackend/UIKitBackend+Container.swift +++ b/Sources/UIKitBackend/UIKitBackend+Container.swift @@ -56,6 +56,19 @@ final class ScrollWidget: ContainerWidget { scrollView.showsVerticalScrollIndicator = hasVerticalScrollBar scrollView.showsHorizontalScrollIndicator = hasHorizontalScrollBar } + + public func updateScrollContainer(environment: EnvironmentValues) { + #if os(iOS) + scrollView.keyboardDismissMode = switch environment.scrollDismissesKeyboardMode { + case .immediately: + .onDrag + case .interactively: + .interactive + case .never: + .none + } + #endif + } } extension UIKitBackend { @@ -121,6 +134,11 @@ extension UIKitBackend { ScrollWidget(child: child) } + public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) { + let scrollViewWidget = scrollView as! ScrollWidget + scrollViewWidget.updateScrollContainer(environment: environment) + } + public func setScrollBarPresence( ofScrollContainer scrollView: Widget, hasVerticalScrollBar: Bool, diff --git a/Sources/UIKitBackend/UIKitBackend+Control.swift b/Sources/UIKitBackend/UIKitBackend+Control.swift index c2cdd8eab..3026b7cb2 100644 --- a/Sources/UIKitBackend/UIKitBackend+Control.swift +++ b/Sources/UIKitBackend/UIKitBackend+Control.swift @@ -320,11 +320,11 @@ extension UIKitBackend { textEditorWidget.child.alwaysBounceVertical = environment.scrollDismissesKeyboardMode != .never textEditorWidget.child.keyboardDismissMode = switch environment.scrollDismissesKeyboardMode { - case .immediately: - textEditorWidget.child.inputAccessoryView == nil ? .onDrag : .onDragWithAccessory - case .interactively: - textEditorWidget.child.inputAccessoryView == nil ? .interactive : .interactiveWithAccessory - case .never: + case .immediately: + textEditorWidget.child.inputAccessoryView == nil ? .onDrag : .onDragWithAccessory + case .interactively: + textEditorWidget.child.inputAccessoryView == nil ? .interactive : .interactiveWithAccessory + case .never: .none } #endif diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index af3ed5314..a8e3edda1 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -616,6 +616,10 @@ public final class WinUIBackend: AppBackend { return scrollViewer } + public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) { + let scrollView = scrollView as! WinUI.ScrollViewer + } + public func setScrollBarPresence( ofScrollContainer scrollView: Widget, hasVerticalScrollBar: Bool, From a651758fcc7279e220b79ac90f68517cb418f1b8 Mon Sep 17 00:00:00 2001 From: Vitor Silveira Date: Tue, 24 Jun 2025 22:37:37 -0300 Subject: [PATCH 4/4] #188: Add .automatic case, remove unused variables, and apply minor adjustments --- Examples/Sources/NotesExample/ContentView.swift | 1 - Sources/AppKitBackend/AppKitBackend.swift | 4 +--- Sources/Gtk3Backend/Gtk3Backend.swift | 4 +--- Sources/GtkBackend/GtkBackend.swift | 4 +--- Sources/SwiftCrossUI/Backend/AppBackend.swift | 2 -- Sources/SwiftCrossUI/Environment/EnvironmentValues.swift | 5 +++-- .../SwiftCrossUI/Values/ScrollDismissesKeyboardMode.swift | 8 ++++++++ Sources/UIKitBackend/UIKitBackend+Container.swift | 2 ++ Sources/UIKitBackend/UIKitBackend+Control.swift | 2 ++ Sources/WinUIBackend/WinUIBackend.swift | 4 +--- 10 files changed, 19 insertions(+), 17 deletions(-) diff --git a/Examples/Sources/NotesExample/ContentView.swift b/Examples/Sources/NotesExample/ContentView.swift index 469fe7e7f..05f74d7c5 100644 --- a/Examples/Sources/NotesExample/ContentView.swift +++ b/Examples/Sources/NotesExample/ContentView.swift @@ -142,7 +142,6 @@ struct ContentView: View { .padding() .background(textEditorBackground) .cornerRadius(4) - .scrollDismissesKeyboard(.interactively) } } .padding() diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index 62dec277a..c18a11c73 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -861,9 +861,7 @@ public final class AppKitBackend: AppBackend { return scrollView } - public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) { - let scrollView = scrollView as! NSScrollView - } + public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) {} public func setScrollBarPresence( ofScrollContainer scrollView: Widget, diff --git a/Sources/Gtk3Backend/Gtk3Backend.swift b/Sources/Gtk3Backend/Gtk3Backend.swift index 84ff924ef..b80d76676 100644 --- a/Sources/Gtk3Backend/Gtk3Backend.swift +++ b/Sources/Gtk3Backend/Gtk3Backend.swift @@ -539,9 +539,7 @@ public final class Gtk3Backend: AppBackend { return scrollView } - public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) { - let scrollView = scrollView as! ScrolledWindow - } + public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) {} public func setScrollBarPresence( ofScrollContainer scrollView: Widget, diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index 4d969ff52..d15e4ab50 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -503,9 +503,7 @@ public final class GtkBackend: AppBackend { return scrollView } - public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) { - let scrollView = scrollView as! ScrolledWindow - } + public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) {} public func setScrollBarPresence( ofScrollContainer scrollView: Widget, diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index 31c88252b..e074fdf76 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -268,8 +268,6 @@ public protocol AppBackend { /// /// This method is primarily used on iOS to apply environment changes /// that affect the scroll view’s behavior, such as keyboard dismissal mode. - /// It allows the backend to update UIKit-specific properties (e.g. `keyboardDismissMode`) - /// when the environment changes. /// /// - Parameters: /// - scrollView: The scroll container widget previously created by `createScrollContainer(for:)`. diff --git a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift index d58fce004..929643f4a 100644 --- a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift +++ b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift @@ -77,7 +77,8 @@ public struct EnvironmentValues { /// Whether user interaction is enabled. Set by ``View/disabled(_:)``. public var isEnabled: Bool - + + /// The way that scrollable content interacts with the software keyboard. public var scrollDismissesKeyboardMode: ScrollDismissesKeyboardMode /// Called by view graph nodes when they resize due to an internal state @@ -198,7 +199,7 @@ public struct EnvironmentValues { listStyle = .default toggleStyle = .button isEnabled = true - scrollDismissesKeyboardMode = .interactively + scrollDismissesKeyboardMode = .automatic } /// Returns a copy of the environment with the specified property set to the diff --git a/Sources/SwiftCrossUI/Values/ScrollDismissesKeyboardMode.swift b/Sources/SwiftCrossUI/Values/ScrollDismissesKeyboardMode.swift index 5fbc061f1..1dd7aef24 100644 --- a/Sources/SwiftCrossUI/Values/ScrollDismissesKeyboardMode.swift +++ b/Sources/SwiftCrossUI/Values/ScrollDismissesKeyboardMode.swift @@ -3,6 +3,14 @@ /// Use this type in a call to the ``View/scrollDismissesKeyboard(_:)`` /// modifier to specify the dismissal behavior of scrollable views. public enum ScrollDismissesKeyboardMode: Sendable { + /// Determine the mode automatically based on the surrounding context. + /// + /// Currently, this behaves the same as ``ScrollDismissesKeyboardMode/interactively``. + /// In the future, it may adapt dynamically based on the context, similar to how + /// SwiftUI's `.automatic` works (e.g., using `.interactively` in some views and + /// `.immediately` in others). Using this value avoids source-breaking changes later on. + case automatic + /// Dismiss the keyboard as soon as scrolling starts. case immediately diff --git a/Sources/UIKitBackend/UIKitBackend+Container.swift b/Sources/UIKitBackend/UIKitBackend+Container.swift index a31d3b1bd..4f28290ad 100644 --- a/Sources/UIKitBackend/UIKitBackend+Container.swift +++ b/Sources/UIKitBackend/UIKitBackend+Container.swift @@ -60,6 +60,8 @@ final class ScrollWidget: ContainerWidget { public func updateScrollContainer(environment: EnvironmentValues) { #if os(iOS) scrollView.keyboardDismissMode = switch environment.scrollDismissesKeyboardMode { + case .automatic: + .interactive case .immediately: .onDrag case .interactively: diff --git a/Sources/UIKitBackend/UIKitBackend+Control.swift b/Sources/UIKitBackend/UIKitBackend+Control.swift index 3026b7cb2..55e60d192 100644 --- a/Sources/UIKitBackend/UIKitBackend+Control.swift +++ b/Sources/UIKitBackend/UIKitBackend+Control.swift @@ -320,6 +320,8 @@ extension UIKitBackend { textEditorWidget.child.alwaysBounceVertical = environment.scrollDismissesKeyboardMode != .never textEditorWidget.child.keyboardDismissMode = switch environment.scrollDismissesKeyboardMode { + case .automatic: + textEditorWidget.child.inputAccessoryView == nil ? .interactive : .interactiveWithAccessory case .immediately: textEditorWidget.child.inputAccessoryView == nil ? .onDrag : .onDragWithAccessory case .interactively: diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index a8e3edda1..fe411f10d 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -616,9 +616,7 @@ public final class WinUIBackend: AppBackend { return scrollViewer } - public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) { - let scrollView = scrollView as! WinUI.ScrollViewer - } + public func updateScrollContainer(_ scrollView: Widget, environment: EnvironmentValues) {} public func setScrollBarPresence( ofScrollContainer scrollView: Widget,