如何在使用SwiftUI时隐藏键盘?

191

如何使用 SwiftUI 针对以下情况隐藏 keyboard

情况 1

我有一个TextField,需要在用户点击“返回”按钮时隐藏 keyboard

情况 2

我有一个TextField,需要在用户点击外部区域时隐藏 keyboard

我可以通过使用 SwiftUI 来实现这个功能吗?

注意:

我没有问关于 UITextField 的问题。 我想通过使用 SwifUI.TextField 来完成此操作。


37
请仔细再次阅读我的问题!@DannyBuonocore - Hitesh Surani
12
@DannyBuonocore 这不是提到的问题的副本。这个问题是关于SwiftUI的,而另一个是普通的UIKit。 - Johnykutty
1
@DannyBuonocore 请查看https://developer.apple.com/documentation/swiftui,了解UIKit和SwiftUI之间的区别。谢谢。 - Hitesh Surani
我在这里添加了我的解决方案(https://dev59.com/HlMI5IYBdhLWcg3wUp42#59872410),希望能对你有所帮助。 - Victor Kushnerov
大多数解决方案都无法按预期工作,因为它们会禁用其他控件选项卡上的所需反应。可在此处找到一个有效的解决方案:https://forums.developer.apple.com/thread/127196 - Hardy
显示剩余2条评论
36个回答

153

您可以通过向共享应用程序发送动作来强制第一响应者辞职:

extension UIApplication {
    func endEditing() {
        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

现在您可以使用此方法在任何时候关闭键盘:
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()
            }
        }
    }
}

如果您想通过点击来关闭键盘,您可以创建一个全屏的白色视图并添加一个触发endEditing(_:)方法的点击操作:

```html 如果您想通过点击来关闭键盘,您可以创建一个全屏的白色视图并添加一个触发endEditing(_:)方法的点击操作: ```
struct Background<Content: View>: View {
    private var content: Content

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content()
    }

    var body: some View {
        Color.white
        .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
        .overlay(content)
    }
}

struct ContentView : View {
    @State private var name: String = ""

    var body: some View {
        Background {
            VStack {
                Text("Hello \(self.name)")
                TextField("Name...", text: self.$name) {
                    self.endEditing()
                }
            }
        }.onTapGesture {
            self.endEditing()
        }
    }

    private func endEditing() {
        UIApplication.shared.endEditing()
    }
}

2
.keyWindow现已弃用,请参见Lorenzo Santini的答案 - LinusGeffarth
4
.tapAction 已更名为 .onTapGesture - LinusGeffarth
1
有没有办法在没有白色背景的情况下实现这个,我正在使用间距,并且需要在间距上检测点击手势。此外,白色背景策略在较新的iPhone上会产生问题,在现在有额外屏幕空间的情况下。感谢任何帮助! - Joseph Astrahan
我发布了一个答案,对你的设计进行了改进。如果你想要的话,可以对你的答案进行编辑,我不在意获得荣誉。 - Joseph Astrahan
2
也许值得注意的是 UIApplication 是 UIKit 的一部分,因此需要 import UIKit - Alienbash
显示剩余3条评论

143

iOS 15+

(键盘上方的“完成”按钮)

从iOS 15开始,我们现在可以使用@FocusState来控制应该聚焦哪个字段(请参见此答案以查看更多示例)。

我们还可以直接在键盘上方添加ToolbarItem

当两者结合在一起时,我们可以在键盘正上方添加一个Done按钮。这是一个简单的演示:

enter image description here

struct ContentView: View {
    private enum Field: Int, CaseIterable {
        case username, password
    }

    @State private var username: String = ""
    @State private var password: String = ""

    @FocusState private var focusedField: Field?

    var body: some View {
        NavigationView {
            Form {
                TextField("Username", text: $username)
                    .focused($focusedField, equals: .username)
                SecureField("Password", text: $password)
                    .focused($focusedField, equals: .password)
            }
            .toolbar {
                ToolbarItem(placement: .keyboard) {
                    Button("Done") {
                        focusedField = nil
                    }
                }
            }
        }
    }
}

iOS 14+

(点击任意位置隐藏键盘)

这里提供了一个更新的解决方案,适用于SwiftUI 2 / iOS 14(最初由Mikhail在这里提出)。

它不使用AppDelegate或者SceneDelegate,因为如果你使用SwiftUI生命周期,这些内容是缺失的:

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onAppear(perform: UIApplication.shared.addTapGestureRecognizer)
        }
    }
}

extension UIApplication {
    func addTapGestureRecognizer() {
        guard let window = windows.first else { return }
        let tapGesture = UITapGestureRecognizer(target: window, action: #selector(UIView.endEditing))
        tapGesture.requiresExclusiveTouchType = false
        tapGesture.cancelsTouchesInView = false
        tapGesture.delegate = self
        window.addGestureRecognizer(tapGesture)
    }
}

extension UIApplication: UIGestureRecognizerDelegate {
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true // set to `false` if you don't want to detect tap during other gestures
    }
}

如果您想检测其他手势(不仅限于点击手势),可以像Mikhail在回答中所述使用AnyGestureRecognizer
let tapGesture = AnyGestureRecognizer(target: window, action: #selector(UIView.endEditing))

以下是如何检测同时手势(除了长按手势)的示例代码:
extension UIApplication: UIGestureRecognizerDelegate {
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return !otherGestureRecognizer.isKind(of: UILongPressGestureRecognizer.self)
    }
}

8
应该将其置于顶部,因为考虑了新的SwiftUI生命周期。 - carlosobedgomez
2
这个很好用。但是如果我在文本框中双击,键盘会消失而不是选择文本。有什么办法可以允许双击选择文本吗? - Gary
4
为了回答我的问题,我将其设置为真,并将tapGesture设置为Mikhail在他的答案中创建的AnyGestureRecognizer(...)而不是UITapGestureRecognizer(...)。这样就可以在文本字段内选择文本的同时,还可以使用各种手势隐藏文本字段外的键盘。 - Gary
2
很遗憾,iOS 15的解决方案不允许您点击键盘外部来关闭它。 - Roland Lariotte
7
假设您使用iOS系统,您可以使用guard let window = (connectedScenes.first as? UIWindowScene)?.windows.first else { return }来消除警告。这样的话,它与原解决方案的行为完全相同。 - pawello2222
显示剩余14条评论

83

经过多次尝试,我找到了一个解决方案,目前不会阻止任何控件 - 将手势识别器添加到 UIWindow

  1. If you want to close keyboard only on Tap outside (without handling drags) - then it's enough to use just UITapGestureRecognizer and just copy step 3:
  2. Create custom gesture recognizer class that works with any touches:

    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
        }
    }
    
  3. 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)  
    
  4. Implement UIGestureRecognizerDelegate to allow simultaneous touches.

    extension SceneDelegate: UIGestureRecognizerDelegate {
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            return true
        }
    }
    
现在,任何视图上的任何键盘都将在触摸或拖动视图外部时关闭。
顺便提一句,如果您希望仅关闭特定的TextField,则可以在TextField的回调函数onEditingChanged中添加和删除手势识别器到窗口。请注意保留html标记。

4
这个答案应该排在最前面。当视图中存在其他控件时,其他答案会失败。 - imthath
2
非常棒的答案。运行得非常流畅。@Mikhail,我实际上很想知道您如何专门为某些文本字段删除手势识别器(我使用标签构建了自动完成功能,因此每次我点击列表中的元素时,我不希望这个特定的文本字段失去焦点)。 - Pasta
1
太棒了!我想知道在没有scenedelegate的情况下,这将如何在iOS 14中实现? - Dom
3
@DominiqueMiller 我为iOS 14 这里调整了这个解决方案。 - pawello2222
1
@pawello2222 谢谢! - Dom
显示剩余13条评论

37

在使用NavigationView中的TextField时,我遇到了这个问题。以下是我的解决方案。它将在开始滚动时关闭键盘。

NavigationView {
    Form {
        Section {
            TextField("Receipt amount", text: $receiptAmount)
            .keyboardType(.decimalPad)
           }
        }
     }
     .gesture(DragGesture().onChanged{_ in UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)})

2
这将导致onDelete(滑动删除)出现奇怪的行为。 - Tarek Hallak
这很不错,但水龙头呢? - DanielZanchi

33

@RyanTCB的回答很好; 这里有一些改进措施,使其更加易于使用并避免潜在崩溃:

struct DismissingKeyboard: ViewModifier {
    func body(content: Content) -> some View {
        content
            .onTapGesture {
                let keyWindow = UIApplication.shared.connectedScenes
                        .filter({$0.activationState == .foregroundActive})
                        .map({$0 as? UIWindowScene})
                        .compactMap({$0})
                        .first?.windows
                        .filter({$0.isKeyWindow}).first
                keyWindow?.endEditing(true)                    
        }
    }
}

“修复”只是将keyWindow!.endEditing(true)正确替换为keyWindow?.endEditing(true)(是的,你可能会争辩这不可能发生)。

更有趣的是如何使用它。例如,假设您有一个包含多个可编辑字段的表单。只需像这样包装:

Form {
    .
    .
    .
}
.modifier(DismissingKeyboard())

现在,轻触任何不显示键盘的控件将执行相应的关闭操作。

(已在 beta 7 中测试)


9
嗯——点击其他控件不再起作用。事件被吞噬了。 - Yarm
我无法复制那个问题 - 我使用截至11/1的最新Apple版本仍然可以正常工作。它之前是正常工作的,然后突然停止工作了吗? - Feldur
@Albert - 这是正确的;要使用这种方法,您必须将装饰有DismissingKeyboard()的项目细分到适用于应该关闭的元素的更精细的级别,并避免DatePicker。 - Feldur
@np2314 你能发一下出现问题的代码吗?我用这种技术检查了我的应用程序,截至今天(2/3),它是正常工作的。 - Feldur
这段代码将 cancelsTouchesInView 设为 true。这样,您就无法点击视图上的按钮了。 - Roland Lariotte
显示剩余2条评论

30

我找到了一种不需要访问keyWindow属性就可以取消键盘的方法;实际上,使用这种方法编译器会返回一个警告。

UIApplication.shared.keyWindow?.endEditing(true)

'keyWindow'在iOS 13.0已被弃用:不应该在支持多个场景的应用程序中使用,因为它返回所有连接场景中的关键窗口。

相反,我使用了这段代码:

UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)

1
这是一个完美的答案。非常简洁易懂。只需将此代码放入层次结构顶部视图的.onTapGesture{}修饰符中,问题就解决了。谢谢。 - NSSpeedForce

18

纯SwiftUI(iOS 15)

iOS 15(Xcode 13)中的SwiftUI已经原生支持使用新的@FocusState属性包装器以编程方式聚焦TextField

要消除键盘,只需将视图的focusedField设置为nil即可。自iOS14以来,返回键会自动关闭键盘。

文档: https://developer.apple.com/documentation/swiftui/focusstate/

struct MyView: View {

    enum Field: Hashable {
        case myField
    }

    @State private var text: String = ""
    @FocusState private var focusedField: Field?

    var body: some View {
        TextField("Type here", text: $text)
            .focused($focusedField, equals: .myField)

        Button("Dismiss") {
            focusedField = nil
        }
    }
}

纯SwiftUI(适用于iOS 14及以下版本)

您可以完全避免与UIKit的交互,并在纯SwiftUI中实现它。只需为您的TextField添加一个.id(<your id>)修饰符,并在需要关闭键盘时更改其值(例如在滑动、视图点击、按钮操作等情况下)。

示例实现:

struct MyView: View {
    @State private var text: String = ""
    @State private var textFieldId: String = UUID().uuidString

    var body: some View {
        VStack {
            TextField("Type here", text: $text)
                .id(textFieldId)

            Spacer()

            Button("Dismiss", action: { textFieldId = UUID().uuidString })
        }
    }
}

请注意,我只在最新的Xcode 12 beta中进行了测试,但它应该可以在旧版本(甚至是Xcode 11)中正常工作,没有任何问题。


太棒了,简单易行的解决方案!我使用这种技术来隐藏键盘,每当用户点击文本框外部的任何地方时都会触发。请参见 https://dev59.com/HlMI5IYBdhLWcg3wUp42#65798558 - Hasaan Ali
在 iOS 的 @Focused 版本中,你如何关闭切换或选择器表单字段的键盘? - Galen Smith
是的,它可以在iOS15+上运行。但是它相当冗长。一旦你需要额外的样板代码。 - Robson Tenório
尝试将其包装在列表中,这是行不通的。 - undefined

16

自iOS 15起,您可以使用@FocusState

struct ContentView: View {
    
    @Binding var text: String
    
    private enum Field: Int {
        case yourTextEdit
    }

    @FocusState private var focusedField: Field?

    var body: some View {
        VStack {
            TextEditor(text: $speech.text.bound)
                .padding(Edge.Set.horizontal, 18)
                .focused($focusedField, equals: .yourTextEdit)
        }.onTapGesture {
            if (focusedField != nil) {
                focusedField = nil
            }
        }
    }
}

适用于+iOS15的合适解决方案! - Alessandro Pace
是的,它可以在iOS15+上工作。但是它相当冗长。一旦你需要额外的样板文件。 - Robson Tenório

16

在“SceneDelegate.swift”文件中,只需添加 .onTapGesture { window.endEditing(true)} 即可。

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()
        }
    }

这对于使用键盘的每个视图在您的应用程序中来说已经足够了...


5
这带来了另一个问题——我在表单中同时使用文本框和一个选择器,但它变得无响应。我在这个主题中找不到解决方法,但您的答案是一个好的解决方案,可以通过轻点其他地方来关闭键盘——如果您不使用选择器的话。 - Nalov
你好。我的代码:var body: some View { NavigationView { Form { Section { TextField("输入", text: $c) } Section { Picker("名称", selection: $sel) { ForEach(0..<200) { Text("\(self.array[$0])%") } } .onTapGesture { UIApplication.shared.endEditing() } } } } }当点击其他区域时,键盘会消失,但是选择器无响应。我没有找到解决方法。 - Nalov
2
再次问候,目前我有两个解决方案:第一个是使用本地键盘,在按下返回按钮时关闭键盘;第二个是稍微更改点击处理方式(也称为“костыль”)- window.rootViewController = UIHostingController(rootView: contentView.onTapGesture(count: 2, perform: { window.endEditing(true) }) ) 希望这能帮到你... - Dim Novo
你好。谢谢。第二种方法解决了它。我正在使用数字键盘,所以用户只能输入数字,它没有回车键。通过点击来取消是我一直在寻找的。 - Nalov
这将导致无法浏览列表。 - Cui Mingda
结合 @DimNovo 的答案,这个代码很好用(可能解决了选择器的问题,我没有尝试):window.rootViewController = UIHostingController(rootView: contentView.onTapGesture { UIApplication.shared.sendAction(#selector(UIView.resignFirstResponder), to: nil, from: nil, for: nil) }) - Cinn

15

我的解决方案是如何在用户点击外部时隐藏软件键盘。 您需要使用contentShapeonLongPressGesture来检测整个视图容器。使用onTapGesture来避免阻塞TextField上的焦点。您可以使用onTapGesture而不是onLongPressGesture,但NavigationBar项目将无法使用。

extension View {
    func endEditing() {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

struct KeyboardAvoiderDemo: View {
    @State var text = ""
    var body: some View {
        VStack {
            TextField("Demo", text: self.$text)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .contentShape(Rectangle())
        .onTapGesture {}
        .onLongPressGesture(
            pressing: { isPressed in if isPressed { self.endEditing() } },
            perform: {})
    }
}

这个很好用,我稍微有些不同的使用方式,必须确保它在主线程上被调用。 - keegan3d

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接