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

4

对我来说,最简单的解决方案是直接使用这里的库。

SwiftUI的支持有些有限,我通过在@main结构体中放置以下代码来使用它:

import IQKeyboardManagerSwift

@main
struct MyApp: App {
            
    init(){
        IQKeyboardManager.shared.enable = true
        IQKeyboardManager.shared.shouldResignOnTouchOutside = true
        
    }

    ...
}

我曾经忽略了推荐使用IQKeyboardManager的消息,因为我认为它只是“又一个库”。在与SwiftUI键盘进行了长时间的斗争后,我终于开始实现它。 - Liviu

4
首先在你的代码中添加扩展。
   extension UIApplication {
     func dismissKeyboard() {
       sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
       } }

使用它作为修饰符,在按下“完成”按钮时关闭键盘。
   .toolbar{
    ToolbarItemGroup(placement: .keyboard){
         Spacer()
         Button("Done"){
            UIApplication.shared.dismissKeyboard()
          }
      }
  }

3

关于我使用的点击“外部”的简单解决方案:

首先,在所有视图之前提供一个ZStack。 在其中,放置一个背景(使用您选择的颜色)并提供一个点击手势。 在手势调用中,调用我们上面看到的“sendAction”:

import SwiftUI

struct MyView: View {
    private var myBackgroundColor = Color.red
    @State var text = "text..."

var body: some View {
    ZStack {
        self.myBackgroundColor.edgesIgnoringSafeArea(.all)
            .onTapGesture(count: 1) {
                UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
        }
        TextField("", text: $text)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .padding()
    }
  }
}
extension UIApplication {
   func endEditing() {
       sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
  }
}

sample


3

根据 @Sajjon 所述的答案,这里提供了一种解决方案,可以让您根据自己的选择在点击、长按、拖动、放大和旋转手势时关闭键盘。

此解决方案适用于 XCode 11.4

使用方法以获得 @IMHiteshSurani 所要求的行为

struct MyView: View {
    @State var myText = ""

    var body: some View {
        VStack {
            DismissingKeyboardSpacer()

            HStack {
                TextField("My Text", text: $myText)

                Button("Return", action: {})
                    .dismissKeyboard(on: [.longPress])
            }

            DismissingKeyboardSpacer()
        }
    }
}

struct DismissingKeyboardSpacer: View {
    var body: some View {
        ZStack {
            Color.black.opacity(0.0001)

            Spacer()
        }
        .dismissKeyboard(on: Gestures.allCases)
    }
}

代码

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 {
            AnyView(view.highPriorityGesture(
                gesture.onChanged { _ in
                    voidAction()
                }
            ))
        }

        switch self {
        case .tap:
            return AnyView(view.gesture(TapGesture().onEnded(voidAction)))
        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(AnyView(content)) { $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))
    }
}

3

这种方法可以让你在间距中隐藏键盘

首先添加此功能(鸣谢:来自SwiftUI can't tap in Spacer of HStack的Casper Zandbergen)

extension Spacer {
    public func onTapGesture(count: Int = 1, perform action: @escaping () -> Void) -> some View {
        ZStack {
            Color.black.opacity(0.001).onTapGesture(count: count, perform: action)
            self
        }
    }
}

接下来添加以下两个函数(感谢rraphael,他来自于这个问题)

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

下面的函数将被添加到您的 View 类中,请参考 rraphael 的顶部答案获取更多详细信息。
private func endEditing() {
   UIApplication.shared.endEditing()
}

最后,你现在可以简单地调用...

Spacer().onTapGesture {
    self.endEditing()
}

现在,这将使得所有的空白区域都能够关闭键盘。不再需要一个大的白色背景视图了!

你可以假设地将这种扩展技术应用于任何需要支持TapGestures的控件,并与self.endEditing()组合调用onTapGesture函数,以在任何情况下关闭键盘。


我的问题是,当您让键盘消失时,如何触发文本字段的提交?目前,“提交”仅在您按下iOS键盘上的返回键时触发。 - Joseph Astrahan

2

一种更清晰的 SwiftUI 本地方式,可以通过轻触来关闭键盘,而不会阻塞任何复杂的表单等... 感谢 @user3441734 指出 GestureMask 是一种干净的方法。

  1. 监听 UIWindow.keyboardWillShowNotification / willHide

  2. 通过在根视图中设置 EnvironmentKey 来传递当前键盘状态

已在 iOS 14.5 下测试通过。

将关闭手势附加到表单上

Form { }
    .dismissKeyboardOnTap()

Setup monitor in root view

// Root view
    .environment(\.keyboardIsShown, keyboardIsShown)
    .onDisappear { dismantleKeyboarMonitors() }
    .onAppear { setupKeyboardMonitors() }

// Monitors

    @State private var keyboardIsShown = false
    @State private var keyboardHideMonitor: AnyCancellable? = nil
    @State private var keyboardShownMonitor: AnyCancellable? = nil
    
    func setupKeyboardMonitors() {
        keyboardShownMonitor = NotificationCenter.default
            .publisher(for: UIWindow.keyboardWillShowNotification)
            .sink { _ in if !keyboardIsShown { keyboardIsShown = true } }
        
        keyboardHideMonitor = NotificationCenter.default
            .publisher(for: UIWindow.keyboardWillHideNotification)
            .sink { _ in if keyboardIsShown { keyboardIsShown = false } }
    }
    
    func dismantleKeyboarMonitors() {
        keyboardHideMonitor?.cancel()
        keyboardShownMonitor?.cancel()
    }

SwiftUI手势+语法糖


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

EnvironmentKey

extension EnvironmentValues {
    var keyboardIsShown: Bool {
        get { return self[KeyboardIsShownEVK] }
        set { self[KeyboardIsShownEVK] = newValue }
    }
}

private struct KeyboardIsShownEVK: EnvironmentKey {
    static let defaultValue: Bool = false
}

2

在 iOS 15 上使用 .onSubmit@FocusState

通过使用 .onSubmit@FocusState,您可以在按下回车键时关闭键盘,或者选择另一个TextField来接收焦点:

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)
      }
      .onSubmit {
        fieldInFocus = nil
      }
    }
  }
}

或者,如果您想使用.onSubmit将焦点带到不同的TextField

  .onSubmit {
      } if fieldInFocus == .email {
        fieldInFocus = .password
      } else if fieldInFocus == .password {
        fieldInFocus = nil
      }
  }

这个回答是否针对问题中的第二种情况? - Zaporozhchenko Oleksandr

2
到目前为止,以上选项对我没有用,因为我有表单和内部按钮、链接、选择器等。
我创建了下面的代码,它是有效的,得益于上面的示例。
import Combine
import SwiftUI

private class KeyboardListener: ObservableObject {
    @Published var keyabordIsShowing: Bool = false
    var cancellable = Set<AnyCancellable>()

    init() {
        NotificationCenter.default
            .publisher(for: UIResponder.keyboardWillShowNotification)
            .sink { [weak self ] _ in
                self?.keyabordIsShowing = true
            }
            .store(in: &cancellable)

       NotificationCenter.default
            .publisher(for: UIResponder.keyboardWillHideNotification)
            .sink { [weak self ] _ in
                self?.keyabordIsShowing = false
            }
            .store(in: &cancellable)
    }
}

private struct DismissingKeyboard: ViewModifier {
    @ObservedObject var keyboardListener = KeyboardListener()

    fileprivate func body(content: Content) -> some View {
        ZStack {
            content
            Rectangle()
                .background(Color.clear)
                .opacity(keyboardListener.keyabordIsShowing ? 0.01 : 0)
                .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
                .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)
                }
        }
    }
}

extension View {
    func dismissingKeyboard() -> some View {
        ModifiedContent(content: self, modifier: DismissingKeyboard())
    }
}

使用方法:

 var body: some View {
        NavigationView {
            Form {
                picker
                button
                textfield
                text
            }
            .dismissingKeyboard()

1
extension UIView{

 override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {  
     UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)

  }
}

将“Set”替换为“Set<UITouch>”。 - Sergei Volkov
目前你的回答不够清晰,请编辑并添加更多细节,以帮助其他人理解它如何回答问题。你可以在帮助中心找到有关如何编写好答案的更多信息。 - Community
太棒了!这应该是默认设置。不确定为什么苹果会让它开放。我会把这个交给一个薪资更高的人,而我则专注于编写我的新应用! - user3413723

1

iOS 13+

iOS13+的一个简单技巧是为每个文本字段设置一个“禁用”状态变量。显然不是理想的解决方案,但在某些情况下可能会完成任务。

一旦您设置了disabled = True,那么所有链接的响应者都会自动辞职。

@State var isEditing: Bool
@State var text: String

....

TextField("Text", text: self.$text).disabled(!self.isEditing)

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