如何在使用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个回答

12

iOS15中这个功能运行得非常完美。

VStack {
    // Some content
}
.onTapGesture {
    // Hide Keyboard
    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
.gesture(
    DragGesture(minimumDistance: 0, coordinateSpace: .local).onEnded({ gesture in
        // Hide keyboard on swipe down
        if gesture.translation.height > 0 {
            UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
        }
}))

您的TextField上不需要任何其他操作,下拉和点击都可以隐藏它。我使用的方式是,在我的主NavigationView上添加此代码,然后它下面的所有内容都将起作用。唯一的例外是,任何Sheet都需要添加这个代码,因为它作用于不同的状态。


我使用了iOS 14 (swiftUI 2)中的以下示例,使用了@main和2个扩展。你是说我必须放弃所有这些代码才能在iOS 15中实现相同的功能吗?没有一个简单的解决方案可以在任何地方轻松关闭键盘吗? - Galen Smith
@GalenSmith 不,我的意思是我在iOS15中测试了我发布的解决方案。但是在iOS14、13等版本中,只需进行一些名称上的小改动即可使用。我认为特别是.onTapGesture是不同的。 - Joe Scotto

12

将此修饰符添加到您希望检测用户轻拍的视图中

.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)

        }

.onTapGesture 在大多数情况下都能正常工作,但是当我在一个包含导航链接的列表中添加 onTapGesture 时,这些链接将不再被触发。 - Mike Birkhoff

11

我更喜欢使用.onLongPressGesture(minimumDuration: 0),这样当另一个TextView被激活时不会导致键盘闪烁(在.onTapGesture中的副作用)。隐藏键盘的代码可以是可重用的函数。

.onTapGesture(count: 2){} // UI is unresponsive without this line. Why?
.onLongPressGesture(minimumDuration: 0, maximumDistance: 0, pressing: nil, perform: hide_keyboard)

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

仍然使用这种方法会有闪烁。 - Daniel Ryan
这个很有效,我略有不同地使用了它,并且必须确保它在主线程上调用。 - keegan3d

10

由于keyWindow已经被弃用。

extension View {
    func endEditing(_ force: Bool) {
        UIApplication.shared.windows.forEach { $0.endEditing(force)}
    }
}

1
force 参数未被使用。应该是 { $0.endEditing(force)} - Davide

9

更新了答案,使用 Swift 5.7 版本:

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

然后在需要的地方使用它,例如作为按钮动作:

Button(action: {
    // do stuff
    UIApplication.shared.dismissKeyboard()
}, label: { Text("MyButton") })

3
.keyWindow现已弃用。请参考Lorenzo Santini的回答 - LinusGeffarth
它在iOS 13+上已经被弃用。 - Ahmadreza

8

扩展josefdolezal上面的答案,您可以像下面这样在用户点击文本框之外的任何地方时隐藏键盘

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!
            }
        }
    }

7

键盘的Return

除了关于在文本字段之外轻敲的所有答案之外,您可能还想在用户点击键盘上的返回键时关闭键盘:

定义这个全局函数:

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

并在 onCommit 参数中添加 use it:

TextField("title", text: $text, onCommit:  {
    resignFirstResponder()
})

好处

  • 你可以在任何地方调用它
  • 它不依赖于UIKit或SwiftUI(可用于Mac应用程序)
  • 即使在iOS 13中也可以使用

演示

演示


简单而干净,但如果我们使用手机键盘呢? - Ahmadreza
文本编辑器对象有类似的东西吗? - Mike Keller
TextEditor 中,回车键实际上是将新行添加到编辑区域中,覆盖这个功能并不是一个好的做法。你可以使用竖直的 TextField 代替。 - Mojtaba Hosseini

6

我发现一些非常好用的东西

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

然后将其添加到视图结构中:

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

那么

struct YourView: View {
    var body: some View {
       ParentView {
           //...
       }.contentShape(Rectangle()) //<---- This is key!
        .onTapGesture {endEditing()} 
     }
 }
    

3
这段代码禁用了视图上的其他触摸操作。 - Roland Lariotte

6

扩展@Feldur的回答(基于@RyanTCB的答案),这里是一个更加表达和强大的解决方案,允许您在除onTapGesture之外的其他手势上消除键盘,您可以在函数调用中指定想要的手势。

用法

// MARK: - View
extension RestoreAccountInputMnemonicScreen: View {
    var body: some View {
        List(viewModel.inputWords) { inputMnemonicWord in
            InputMnemonicCell(mnemonicInput: inputMnemonicWord)
        }
        .dismissKeyboard(on: [.tap, .drag])
    }
}

或者使用All.gestures(只是Gestures.allCases的简化形式)

.dismissKeyboard(on: All.gestures)

代码

enum All {
    static let gestures = all(of: Gestures.self)

    private static func all<CI>(of _: CI.Type) -> CI.AllCases where CI: CaseIterable {
        return CI.allCases
    }
}

enum Gestures: Hashable, CaseIterable {
    case tap, longPress, drag, magnification, rotation
}

protocol ValueGesture: Gesture where Value: Equatable {
    func onChanged(_ action: @escaping (Value) -> Void) -> _ChangedGesture<Self>
}
extension LongPressGesture: ValueGesture {}
extension DragGesture: ValueGesture {}
extension MagnificationGesture: ValueGesture {}
extension RotationGesture: ValueGesture {}

extension Gestures {
    @discardableResult
    func apply<V>(to view: V, perform voidAction: @escaping () -> Void) -> AnyView where V: View {

        func highPrio<G>(
             gesture: G
        ) -> AnyView where G: ValueGesture {
            view.highPriorityGesture(
                gesture.onChanged { value in
                    _ = value
                    voidAction()
                }
            ).eraseToAny()
        }

        switch self {
        case .tap:
            // not `highPriorityGesture` since tapping is a common gesture, e.g. wanna allow users
            // to easily tap on a TextField in another cell in the case of a list of TextFields / Form
            return view.gesture(TapGesture().onEnded(voidAction)).eraseToAny()
        case .longPress: return highPrio(gesture: LongPressGesture())
        case .drag: return highPrio(gesture: DragGesture())
        case .magnification: return highPrio(gesture: MagnificationGesture())
        case .rotation: return highPrio(gesture: RotationGesture())
        }

    }
}

struct DismissingKeyboard: ViewModifier {

    var gestures: [Gestures] = Gestures.allCases

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

        return gestures.reduce(content.eraseToAny()) { $1.apply(to: $0, perform: action) }
    }
}

extension View {
    dynamic func dismissKeyboard(on gestures: [Gestures] = Gestures.allCases) -> some View {
        return ModifiedContent(content: self, modifier: DismissingKeyboard(gestures: gestures))
    }
}

警告

请注意,如果您使用所有手势,它们可能会相互冲突,并且我没有想出任何简洁的解决方案来解决这个问题。


eraseToAny() 的意思是什么? - Ravindra_Bhati
eraseToAnyView - Peacemoon

5

这对于带有文本字段的表单视图不起作用。表单不会显示。 - Nils

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