【问题标题】:How to hide keyboard when using SwiftUI?使用 SwiftUI 时如何隐藏键盘?
【发布时间】:2019-10-22 18:54:36
【问题描述】:

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

案例 1

我有TextField,当用户单击return 按钮时,我需要隐藏keyboard

案例 2

我有TextField,当用户在外面点击时,我需要隐藏keyboard

我如何使用SwiftUI 做到这一点?

注意:

我没有问过关于UITextField 的问题。我想通过使用SwifUI.TextField来做到这一点。

【问题讨论】:

  • @DannyBuonocore 再次仔细阅读我的问题!
  • @DannyBuonocore 这不是上述问题的重复。这个问题是关于SwiftUI的,其他的都是正常的UIKit
  • @DannyBuonocore 请查看developer.apple.com/documentation/swiftui 以找出 UIKit 和 SwiftUI 之间的区别。谢谢
  • 我添加了我的解决方案here希望对您有所帮助。
  • 这里的大多数解决方案都无法按预期工作,因为它们会禁用其他控制水龙头上的预期反应。可以在此处找到可行的解决方案:forums.developer.apple.com/thread/127196

标签: ios swift keyboard swiftui textfield


【解决方案1】:

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

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(_:)

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

【讨论】:

  • .keyWindow 现在已弃用。见Lorenzo Santini's answer
  • 另外,.tapAction 已重命名为 .onTapGesture
  • 当备用控件激活时可以关闭键盘吗? stackoverflow.com/questions/58643512/…
  • 有没有办法在没有白色背景的情况下做到这一点,我正在使用垫片,我需要它来检测垫片上的点击手势。此外,白色背景策略在现在上方有额外屏幕空间的较新 iPhone 上也会产生问题。任何帮助表示赞赏!
  • 也许还值得注意的是UIApplication是UIKit的一部分,所以需要import UIKit
【解决方案2】:

似乎endEditing 解决方案是唯一像@rraphael 指出的解决方案。
到目前为止我见过的最干净的例子是这样的:

extension View {
    func endEditing(_ force: Bool) {
        UIApplication.shared.keyWindow?.endEditing(force)
    }
}

然后在onCommit:中使用它

【讨论】:

【解决方案3】:

将此修饰符添加到要检测用户点击的视图中

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

        }

【讨论】:

    【解决方案4】:

    我找到了另一种无需访问keyWindow 属性即可关闭键盘的方法;事实上,编译器使用

    返回警告
    UIApplication.shared.keyWindow?.endEditing(true)
    

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

    我使用了这个代码:

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

    【讨论】:

      【解决方案5】:

      因为keyWindow 已被弃用。

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

      【讨论】:

      • 未使用force 参数。应该是{ $0.endEditing(force)}
      【解决方案6】:

      @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 测试)

      【讨论】:

      • 嗯——点击其他控件不再注册。事件被吞没了。
      • 我无法复制它 - 使用 Apple 11 月 1 日的最新版本仍然对我有用。它是否有效,然后停止为您工作,或者??
      • 如果表单中有 DatePicker,则 DatePicker 将不再显示
      • @Albert - 这是真的;要使用这种方法,您必须将使用 DismissingKeyboard() 装饰的项目分解为更细粒度的级别,该级别适用于应该关闭并避免使用 DatePicker 的元素。
      • 使用此代码将重现警告Can't find keyplane that supports type 4 for keyboard iPhone-PortraitChoco-NumberPad; using 25686_PortraitChoco_iPhone-Simple-Pad_Default
      【解决方案7】:

      SwiftUI 在“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()
              }
          }
      

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

      【讨论】:

      • 这又带来了一个问题 - 我在文本字段旁边的 Form{} 中有一个选择器,它变得无响应。我没有使用本主题中的所有答案找到解决方案。但是,如果您不使用选择器,您的答案对于在其他地方轻按即可关闭键盘很有用。
      • 你好。我的代码 ``` var body: some View { NavigationView{ Form{ Section{ TextField("typesomething", text: $c) } Section{ Picker("name", selection: $sel) { ForEach(0..
      • 您好,目前我有两个解决方案:第一个 - 使用在返回按钮上关闭的本机键盘,第二个 - 稍微改变敲击处理(aka 'костыль') - window.rootViewController = UIHostingController(rootView: contentView.onTapGesture(count: 2, perform: { window.endEditing(true) }) ) 希望这对你有帮助...
      • 你好。谢谢你。第二种方法解决了。我使用的是数字键盘,所以用户只能输入数字,它没有返回键。我正在搜索的是通过点击关闭。
      • 这会导致列表无法导航。
      【解决方案8】:

      扩展@Feldur(基于@RyanTCB's)的答案,这是一个更具表现力和强大的解决方案,允许您在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() 是什么意思
      • eraseToAnyView
      【解决方案9】:

      我更喜欢使用.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)
      }
      

      【讨论】:

      • 用这个方法还是闪烁。
      • 这很好用,我使用它略有不同,必须确保它是在主线程上调用的。
      【解决方案10】:

      此方法可让您隐藏键盘上的垫片!

      首先添加这个函数(Credit Give To: Casper Zandbergen, from SwiftUI can't tap in Spacer of HStack

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

      接下来添加以下 2 个函数(Credit Given To: rraphael, from this question)

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

      这将使任何间隔区域现在关闭键盘。不再需要大的白色背景视图!

      您可以假设将extension 的这种技术应用于您需要支持当前不支持的 TapGestures 的任何控件,并结合self.endEditing() 调用onTapGesture 函数以在您希望的任何情况下关闭键盘。

      【讨论】:

      • 我现在的问题是,当您让键盘以这种方式消失时,如何触发文本字段上的提交?目前,“提交”只有在您按下 iOS 键盘上的返回键时才会触发。
      【解决方案11】:

      请查看https://github.com/michaelhenry/KeyboardAvoider

      只需在主视图顶部添加KeyboardAvoider {} 即可。

      KeyboardAvoider {
          VStack { 
              TextField()
              TextField()
              TextField()
              TextField()
          }
      
      }
      

      【讨论】:

      • 这不适用于带有文本字段的表单视图。表格不显示。
      【解决方案12】:

      我在 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)})
      

      【讨论】:

      • 这会导致 onDelete(滑动删除)出现奇怪的行为。
      • 这很好,但是水龙头呢?
      【解决方案13】:

      我的解决方案如何在用户点击外部时隐藏软键盘。 您需要使用 contentShapeonLongPressGesture 来检测整个 View 容器。需要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: {})
          }
      }
      

      【讨论】:

      • 这很好用,我使用它略有不同,并且必须确保它是在主线程上调用的。
      【解决方案14】:

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

      1. 如果您只想在外部点击时关闭键盘(不处理拖动) - 只需使用 UITapGestureRecognizer 并复制第 3 步即可:
      2. 创建适用于任何触摸的自定义手势识别器类:

        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. func scene中的SceneDelegate.swift中,添加下一个代码:

        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. 实现UIGestureRecognizerDelegate 以允许同时触摸。

        extension SceneDelegate: UIGestureRecognizerDelegate {
            func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
                return true
            }
        }
        

      现在任何视图上的任何键盘都将在触摸或向外拖动时关闭。

      附:如果您只想关闭特定的 TextFields - 然后在调用 TextField onEditingChanged

      的回调时向窗口添加和删除手势识别器

      【讨论】:

      • 这个答案应该在顶部。当视图中有其他控件时,其他答案会失败。​​
      • @RolandLariotte 更新了解决此问题的答案,查看 AnyGestureRecognizer 的新实现
      • 很棒的答案。完美运行。 @Mikhail 实际上很想知道如何删除专门针对某些文本字段的手势识别器(我用标签构建了一个自动完成功能,所以每次我点击列表中的一个元素时,我都不希望这个特定的文本字段失去焦点)
      • @Mikhail 您的解决方案非常好,但它结束编辑不仅适用于键盘输入。我在尝试选择某些文本时遇到问题 - 我无法更改选择。每次我尝试移动光标(以扩展选择)时,选择都会消失。你能修改你的action:#selector(UIView.endEditing) 只隐藏键盘而不干扰文本选择吗?
      • 这个解决方案实际上很棒,但是在使用了大约 3 个月后,不幸的是我发现了一个错误,直接由这种 hack 引起。请注意同样的事情发生在你身上
      【解决方案15】:

      根据@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))
          }
      }
      

      【讨论】:

        【解决方案16】:

        SwiftUI 于 2020 年 6 月发布,Xcode 12 和 iOS 14 添加了 hideKeyboardOnTap() 修饰符。这应该可以解决您的案例 2。 Xcode 12 和 iOS 14 为您的案例 1 提供了免费的解决方案:按下 Return 按钮时,TextField 的默认键盘会自动隐藏。

        【讨论】:

        【解决方案17】:

        纯 SwiftUI (iOS 15)

        iOS 15 (Xcode 13) 中的 SwiftUI 使用新的 @FocusState 属性包装器获得了对 TextField 编程焦点的原生支持。

        要关闭键盘,只需将视图的focusedField 设置为nil。返回键会自动关闭键盘(从 iOS 14 开始)。

        文档: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 中实现它。只需将 .id(&lt;your id&gt;) 修饰符添加到您的 TextField 并在您想要关闭键盘时更改其值(在滑动、查看点击、按钮操作等时)。

        示例实现:

        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 测试版中对其进行了测试,但它应该可以与旧版本(甚至是 Xcode 11)一起使用而没有任何问题。

        【讨论】:

        • 很棒的简单解决方案!每当用户点击文本字段外的任何位置时,我都会使用这种技术隐藏键盘。见stackoverflow.com/a/65798558/1590911
        • 那么在 iOS @Focused 版本中,您将如何关闭键盘以获取切换或选择器表单字段?
        【解决方案18】:

        SwiftUI 3 (iOS 15+)

        (键盘上方的完成按钮)

        从 iOS 15 开始,我们现在可以使用 @FocusState 来控制应该关注哪个字段(请参阅 this answer 以查看更多示例)。

        我们也可以直接在键盘上方添加ToolbarItems。

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

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

        SwiftUI 2 (iOS 14+)

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

        这是 SwiftUI 2 / iOS 14 的更新解决方案(最初由 Mikhail 提出 here)。

        它不使用AppDelegateSceneDelegate,如果您使用 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
            }
        }
        

        如果您想检测其他手势(不仅是tap 手势),您可以使用 AnyGestureRecognizer,如 Mikhail 的 answer

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

        【讨论】:

        • 这应该放在首位,因为牢记新的 SwiftUI 生命周期。
        • 这很好用。但是,如果我双击文本字段,而不是选择文本,键盘现在就会消失。知道如何允许双击进行选择吗?
        • @Gary 在底部扩展中,您可以看到带有注释的行如果您不想在其他手势期间检测到点击,请设置为 false。只需将其设置为return false
        • 为了回答我自己的问题,我将其设置回 true,然后设置 Mikhail 在他的回答中创建的 tapGesture= AnyGestureRecognizer(...),而不是 tapGesture=UITapGestureRecognizer(...)。这允许双击以选择文本字段内的文本,同时还允许各种手势将键盘隐藏在文本字段之外。
        • @RolandLariotte 假设您使用 iOS,您可以使用 guard let window = (connectedScenes.first as? UIWindowScene)?.windows.first else { return } 来消除警告。它的行为与原始解决方案完全相同。
        【解决方案19】:

        键盘的Return

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

        定义这个全局函数:

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

        并在onCommit 参数中添加使用:

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

        好处

        • 您可以从任何地方调用它
        • 不依赖于 UIKit 或 SwiftUI(可在 mac 应用中使用)
        • 即使在 iOS 13 中也可以使用

        演示

        【讨论】:

          【解决方案20】:

          到目前为止,上述选项对我不起作用,因为我有表单和内部按钮、链接、选择器......

          我在上面的例子的帮助下创建了下面的代码。

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

          【讨论】:

            【解决方案21】:

            扩展answer by josefdolezal above,您可以在用户点击文本字段外的任意位置时隐藏键盘,如下所示:

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

            【讨论】:

              【解决方案22】:

              我发现效果很好的东西是

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

              【讨论】:

              • 此代码禁用视图上的其他触摸操作。
              【解决方案23】:

              点击“外部”的简单解决方案对我有用:

              首先在所有视图之前提供一个 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)
                }
              }
              

              【讨论】:

                【解决方案24】:

                一种更简洁的 SwiftUI 原生方式,可以通过点击关闭键盘,而不会阻止任何复杂的表单或诸如此类...感谢 @user3441734 将 GestureMask 标记为干净的方法。

                1. 监控 UIWindow.keyboardWillShowNotification / willHide

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

                针对 iOS 14.5 测试。

                将关闭手势附加到表单

                Form { }
                    .dismissKeyboardOnTap()
                
                

                在根视图中设置监视器

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

                环境键

                extension EnvironmentValues {
                    var keyboardIsShown: Bool {
                        get { return self[KeyboardIsShownEVK] }
                        set { self[KeyboardIsShownEVK] = newValue }
                    }
                }
                
                private struct KeyboardIsShownEVK: EnvironmentKey {
                    static let defaultValue: Bool = false
                }
                

                【讨论】:

                  【解决方案25】:

                  真正的 SwiftUI 解决方案

                  @State var dismissKeyboardToggle = false
                  var body: some View {
                      if dismissKeyboardToggle {
                          textfield
                      } else {
                          textfield
                      }
                      
                      Button("Hide Keyboard") {
                          dismissKeyboardToggle.toggle()
                      }                   
                  }
                  

                  这将完美无缺

                  【讨论】:

                  • 它的工作原理其实很容易理解
                  【解决方案26】:

                  嗯,对我来说最简单的解决方案是简单地使用库here

                  SwiftUI 支持有些有限,我通过将此代码放在 @main 结构中来使用它:

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

                  【讨论】:

                  • 我过去常常忽略推荐 IQKeyboardManager 的消息,因为我认为“只是另一个库”。在与 SwiftUI 键盘进行了一番苦战之后,我终于实现了它。
                  【解决方案27】:

                  在 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) 示例中使用了带有 2 个扩展的 @main。你是说我必须扔掉所有代码才能在 iOS 15 中实现相同的功能吗?在点击任意位置关闭键盘时,没有简单的解决方法来关闭键盘吗?
                  • @GalenSmith 不,我是说我测试了我在 iOS15 中发布的解决方案。但它应该可以在 iOS14、13 等中使用,只需对命名进行一些小的更改。我认为具体.onTapGesture 是不同的
                  • @JoeScotto 这太棒了,谢谢!
                  【解决方案28】:

                  我正在尝试隐藏键盘,同时单击和选取器也应该与 SwiftUIForms 中的单击一起使用。

                  我进行了很多搜索以找到合适的解决方案,但没有找到适合我的解决方案。所以我做了我自己的扩展,效果很好。

                  在您的 SwiftUI 表单视图中使用:

                  var body: some View {
                                  .onAppear {                    KeyboardManager.shared.setCurrentView(UIApplication.topViewController()?.view)
                                  }
                  }
                  

                  KeyboardManager 实用程序:

                  enum KeyboardNotificationType {
                      case show
                      case hide
                  }
                  
                  typealias KeyBoardSizeBlock = ((CGSize?, UIView?, KeyboardNotificationType) -> Void)
                  
                  class KeyboardManager: NSObject {
                      
                      static let shared = KeyboardManager()
                      
                      private weak var view: UIView?
                      
                      var didReceiveKeyboardEvent: KeyBoardSizeBlock?
                      
                      @objc public var shouldResignOnTouchOutside = true {
                          didSet {
                              resignFirstResponderGesture.isEnabled = shouldResignOnTouchOutside
                          }
                      }
                  
                      @objc lazy public var resignFirstResponderGesture: UITapGestureRecognizer = {
                          let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissCurrentKeyboard))
                          tap.cancelsTouchesInView = false
                          tap.delegate = self
                          return tap
                      }()
                      
                      private override init() {
                          super.init()
                          self.setup()
                      }
                      
                      func setCurrentView(_ view: UIView?) {
                          self.view = view
                          resignFirstResponderGesture.isEnabled = true
                          if let view = self.view {
                              view.addGestureRecognizer(resignFirstResponderGesture)
                          }
                      }
                      
                      private func setup() {
                          registerForKeyboardWillShowNotification()
                          registerForKeyboardWillHideNotification()
                      }
                      
                      private func topViewHasCurrenView() -> Bool {
                          if view == nil { return false }
                          let currentView = UIApplication.topViewController()?.view
                          if currentView == view { return true }
                          for subview in UIApplication.topViewController()?.view.subviews ?? [] where subview == view {
                              return true
                          }
                          return false
                      }
                          
                      @objc func dismissCurrentKeyboard() {
                          view?.endEditing(true)
                      }
                      
                      func removeKeyboardObserver(_ observer: Any) {
                          NotificationCenter.default.removeObserver(observer)
                      }
                      
                      private func findFirstResponderInViewHierarchy(_ view: UIView) -> UIView? {
                          for subView in view.subviews {
                              if subView.isFirstResponder {
                                  return subView
                              } else {
                                  let result = findFirstResponderInViewHierarchy(subView)
                                  if result != nil {
                                      return result
                                  }
                              }
                          }
                          return nil
                      }
                      
                      deinit {
                          removeKeyboardObserver(self)
                      }
                  }
                  
                  // MARK: - Keyboard Notifications
                  
                  extension KeyboardManager {
                      
                      private func registerForKeyboardWillShowNotification() {
                          _ = NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidShowNotification, object: nil, queue: nil, using: { [weak self] notification -> Void in
                              guard let `self` = self else { return }
                              guard let userInfo = notification.userInfo else { return }
                              guard var kbRect = (userInfo[UIResponder.keyboardFrameEndUserInfoKey]! as AnyObject).cgRectValue else { return }
                              kbRect.size.height -= self.view?.safeAreaInsets.bottom ?? 0.0
                              var mainResponder: UIView?
                              
                              guard self.topViewHasCurrenView() else { return }
                              
                              if let scrollView = self.view as? UIScrollView {
                                  
                                  let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: kbRect.size.height, right: 0.0)
                                  scrollView.contentInset = contentInsets
                                  scrollView.scrollIndicatorInsets = contentInsets
                                  
                                  guard let firstResponder = self.findFirstResponderInViewHierarchy(scrollView) else {
                                      return
                                  }
                                  mainResponder = firstResponder
                                  var aRect = scrollView.frame
                                  aRect.size.height -= kbRect.size.height
                                  
                                  if (!aRect.contains(firstResponder.frame.origin) ) {
                                      scrollView.scrollRectToVisible(firstResponder.frame, animated: true)
                                  }
                                  
                              } else if let tableView = self.view as? UITableView {
                                  
                                  guard let firstResponder = self.findFirstResponderInViewHierarchy(tableView),
                                        let pointInTable = firstResponder.superview?.convert(firstResponder.frame.origin, to: tableView) else {
                                      return
                                  }
                                  mainResponder = firstResponder
                                  var contentOffset = tableView.contentOffset
                                  contentOffset.y = (pointInTable.y - (firstResponder.inputAccessoryView?.frame.size.height ?? 0)) - 10
                                  tableView.setContentOffset(contentOffset, animated: true)
                                  
                              } else if let view = self.view {
                                  
                                  guard let firstResponder = self.findFirstResponderInViewHierarchy(view) else {
                                      return
                                  }
                                  mainResponder = firstResponder
                                  var aRect = view.frame
                                  aRect.size.height -= kbRect.size.height
                                  
                                  if (!aRect.contains(firstResponder.frame.origin) ) {
                                      UIView.animate(withDuration: 0.1) {
                                          view.transform = CGAffineTransform(translationX: 0, y: -kbRect.size.height)
                                      }
                                  }
                              }
                              if let block = self.didReceiveKeyboardEvent {
                                  block(kbRect.size, mainResponder, .show)
                              }
                          })
                      }
                  
                      private func registerForKeyboardWillHideNotification() {
                          _ = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: nil, using: { [weak self] notification -> Void in
                              guard let `self` = self else { return }
                              guard let userInfo = notification.userInfo else { return }
                              guard let kbRect = (userInfo[UIResponder.keyboardFrameEndUserInfoKey]! as AnyObject).cgRectValue else { return }
                              let contentInsets = UIEdgeInsets.zero
                              
                              guard self.topViewHasCurrenView() else { return }
                  
                              if let scrollView = self.view as? UIScrollView {
                                  scrollView.contentInset = contentInsets
                                  scrollView.scrollIndicatorInsets = contentInsets
                                  
                              } else if let tableView = self.view as? UITableView {
                                  tableView.contentInset = contentInsets
                                  tableView.scrollIndicatorInsets = contentInsets
                                  tableView.contentOffset = CGPoint(x: 0, y: 0)
                              } else if let view = self.view {
                                  view.transform = CGAffineTransform(translationX: 0, y: 0)
                                  
                              }
                              
                              if let block = self.didReceiveKeyboardEvent {
                                  block(kbRect.size, nil, .hide)
                              }
                          })
                      }
                      
                  }
                  
                  //MARK: - UIGestureRecognizerDelegate
                  
                  extension KeyboardManager: UIGestureRecognizerDelegate {
                      
                      func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
                          return false
                      }
                  
                      func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
                          if touch.view is UIControl  ||
                             touch.view is UINavigationBar { return false }
                          return true
                      }
                      
                  }
                  

                  【讨论】:

                    【解决方案29】:

                    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的正确解决方案!
                    猜你喜欢
                    • 2020-09-11
                    • 1970-01-01
                    • 1970-01-01
                    • 2022-09-29
                    • 2021-09-16
                    • 1970-01-01
                    • 2012-01-10
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多