struct ContentView : View {
@State private var name: String = ""
var body: some View {
VStack {
Text("Hello \(name)")
TextField("Name...", text: self.$name) {
// Called when the user tap the return button
// see `onCommit` on TextField initializer.
UIApplication.shared.endEditing()
}
}
}
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(
rootView: contentView.onTapGesture { window.endEditing(true)}
)
self.window = window
window.makeKeyAndVisible()
}
}
class AnyGestureRecognizer: UIGestureRecognizer {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if let touchedView = touches.first?.view, touchedView is UIControl {
state = .cancelled
} else if let touchedView = touches.first?.view as? UITextView, touchedView.isEditable {
state = .cancelled
} else {
state = .began
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
state = .ended
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
state = .cancelled
}
}
In SceneDelegate.swift in the func scene, add next code:
let tapGesture = AnyGestureRecognizer(target: window, action:#selector(UIView.endEditing))
tapGesture.requiresExclusiveTouchType = false
tapGesture.cancelsTouchesInView = false
tapGesture.delegate = self //I don't use window as delegate to minimize possible side effects
window?.addGestureRecognizer(tapGesture)
Implement UIGestureRecognizerDelegate to allow simultaneous touches.
Now any keyboard on any view will be closed on touch or drag outside.
P.S. If you want to close only specific TextFields - then add and remove gesture recognizer to the window whenever called callback of TextField onEditingChanged
struct SwiftUIView: View {
@State private var textFieldId: String = UUID().uuidString // To hidekeyboard when tapped outside textFields
@State var fieldValue = ""
var body: some View {
VStack {
TextField("placeholder", text: $fieldValue)
.id(textFieldId)
.onTapGesture {} // So that outer tap gesture has no effect on field
// any more views
}
.onTapGesture { // whenever tapped within VStack
textFieldId = UUID().uuidString
//^ this will remake the textfields hence loosing keyboard focus!
}
}
}
struct HideKeyboardGestureModifier: ViewModifier {
@Environment(\.keyboardIsShown) var keyboardIsShown
func body(content: Content) -> some View {
content
.gesture(TapGesture().onEnded {
UIApplication.shared.resignCurrentResponder()
}, including: keyboardIsShown ? .all : .none)
}
}
extension UIApplication {
func resignCurrentResponder() {
sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
extension View {
/// Assigns a tap gesture that dismisses the first responder only when the keyboard is visible to the KeyboardIsShown EnvironmentKey
func dismissKeyboardOnTap() -> some View {
modifier(HideKeyboardGestureModifier())
}
/// Shortcut to close in a function call
func resignCurrentResponder() {
UIApplication.shared.resignCurrentResponder()
}
}
环境关键词
extension EnvironmentValues {
var keyboardIsShown: Bool {
get { return self[KeyboardIsShownEVK] }
set { self[KeyboardIsShownEVK] = newValue }
}
}
private struct KeyboardIsShownEVK: EnvironmentKey {
static let defaultValue: Bool = false
}
class AnyGestureRecognizer: UIGestureRecognizer {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if let touchedView = touches.first?.view, touchedView is UIControl {
state = .cancelled
} else if let touchedView = touches.first?.view as? UITextView, touchedView.isEditable {
state = .cancelled
} else {
// Check if it is a subview of editable UITextView
if var touchedView = touches.first?.view {
while let superview = touchedView.superview {
if let view = superview as? UITextView, view.isEditable {
state = .cancelled
return
} else {
touchedView = superview
}
}
}
state = .began
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
state = .ended
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
state = .cancelled
}
}