使用 SwiftUI 时如何隐藏键盘?

如何隐藏 keyboard使用 SwiftUI为下列情况?

案例1

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

案例2

我有 TextField和我需要隐藏的 keyboard时,用户点击外面。

如何使用 SwiftUI做到这一点?

注:

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

89403 次浏览

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

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

斯威夫特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") })

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

.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属性的键盘; 事实上,编译器使用

UIApplication.shared.keyWindow?.endEditing(true)

“ keyWindow”在 iOS 13.0中被反对: 不应该用于支持多个场景的应用程序,因为它返回一个跨所有连接场景的键窗口

相反,我使用了以下代码:

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

因为 keyWindow已被弃用。

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

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

‘ bug 修复’很简单,keyWindow!.endEditing(true)应该是 keyWindow?.endEditing(true)(是的,你可能认为它不会发生)

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

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

现在,点击任何本身没有显示键盘的控件都可以适当地解除。

(使用 beta 7测试)

SwiftUI 在“ SceneRegiate.swift”文件中,只需添加: < strong > . 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()
}
}

这是足够的每个视图使用键盘在您的应用程序..。

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

提醒一句

请注意,如果你使用 所有的手势,他们可能会冲突,我没有想出任何整洁的解决方案。

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

此方法允许您在 太空人!上使用 把键盘藏起来

首先添加这个函数(提供者: Casper Zandbergen,来自 SwiftUI 不能点击 HStack 的 Spacer)

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个函数(来源于这个问题: rraphael)

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

下面的函数将被添加到你的视图类中,更多细节请参考拉斐尔的顶部答案。

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

最后,您现在可以简单地拨打..。

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

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

假设您可以将 extension的这种技术应用到需要支持 TapGesture 的任何控件上,这些控件目前不支持这种技术,然后结合 self.endEditing()调用 onTapGesture函数,以便在您希望的任何情况下关闭键盘。

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

只要在你的主视图上包含 KeyboardAvoider {}就可以了。

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


}

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

我的解决方案如何隐藏软件键盘当用户点击外面。 您需要使用 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: {})
}
}

经过多次尝试,我找到了一个解决方案,(目前)不阻止任何控件-添加手势识别到 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. 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
    }
    }
    

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

基于@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))
}
}

2020年6月发布的 SwiftUI 使用 Xcode 12和 iOS 14添加了 hideKeyboardOnTap ()修饰符。 案例1的解决方案是 Xcode 12和 iOS 14免费提供的: 当按下 Return 按钮时,TextField 的默认键盘会自动隐藏。

纯 SwiftUI (iOS15)

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

要关闭键盘,只需将 view 的 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 (iOS14及以下版本)

您可以完全避免与 UIKit 交互,并在 纯 SwiftUI中实现它。只需添加一个 .id(<your id>)修饰符到您的 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 beta 中测试了它,但是它应该可以在较旧的版本(甚至是 Xcode 11)中工作,没有任何问题。

IOS15 +

(键盘上方的完成按钮)

从 iOS15开始,我们现在可以使用 @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
}
}
}
}
}
}

IOS14 +

(点击任何地方隐藏键盘)

这里是一个更新的解决方案的 SwiftUI 2/iOS 14(最初提出的 给你米哈伊尔)。

它没有使用 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
}
}

如果你想检测其他手势(不仅仅是 踢踏舞手势) ,你可以像 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)
}
}

键盘的 Return

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

定义这个全局函数:

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

并在 onCommit中添加使用它的参数:

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

福利

  • 你可以在任何地方打电话
  • 它不依赖于 UIKit 或 SwiftUI (可以在 Mac 应用程序中使用)
  • 它甚至可以在 iOS13中使用

演示

demo

到目前为止,上述选项不适合我,因为我有形式和内部按钮,链接,选择器..。

在上面示例的帮助下,我创建了下面正在运行的代码。

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

展开 作者: 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!
}
}
}

我发现一个非常有效的方法是

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

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

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

一个更干净的 SwiftUI 本地化方式,通过点击不阻止任何复杂的形式或诸如此类的东西来关闭键盘...@user3441734将 GestureMask 标记为一个干净的方法。

  1. 显示 UIWindow.keyboardWillShowNotification/willHide

  2. 通过在/a 根视图中设置的 Environmental Key 传递当前键盘状态

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

真正的 SwiftUI 解决方案

@State var dismissKeyboardToggle = false
var body: some View {
if dismissKeyboardToggle {
textfield
} else {
textfield
}
    

Button("Hide Keyboard") {
dismissKeyboardToggle.toggle()
}
}

一定会完美无缺的

对我来说,最简单的解决方案就是使用 给你库。

SwiftUI 支持有一定的局限性,我通过将这段代码放在@main 结构中来使用它:

import IQKeyboardManagerSwift


@main
struct MyApp: App {
            

init(){
IQKeyboardManager.shared.enable = true
IQKeyboardManager.shared.shouldResignOnTouchOutside = true
        

}


...
}

在 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都需要将此附加到它,因为它作用于不同的状态。

我试图隐藏键盘,而单击 & Picker 也应该与单击 SwiftUIForms 工作。

我找了很多,想找到一个合适的解决方案,但是没有找到一个对我有用的。所以我自己做了一个扩展,效果很好。

在 SwiftUI 窗体视图中使用:

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

键盘管理工具:

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
}
    

}

从 iOS15开始,你可以使用 @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
}
}
}
}

来自@Mikhail 的回复工作得非常好; 它只是有一个问题,它不能支持拖动到 TextView 中选择文本-点击选定的文本时键盘将关闭。我在下面扩展了 AnyGesture 的解决方案,以提供更好的文本编辑用户体验。(答案来自 如何检查 UITextRangeView?)

对优化 while 循环有什么建议吗?

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

扩展 UIView {

重写 open func touch esBegan (_ touch: Set,with event: UIEvent?){
SendAction (# selector (UIResponder.resignFirstResponder) ,转换为: nil,从: nil,转换为: nil)

} }

IOS13 +

IOS13 + 的一个简单技巧是为每个文本字段设置一个“禁用”状态变量。显然不是很理想,但在某些情况下可以完成工作。

一旦设置了 disabled = True,则所有链接的应答器都会自动退出。

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

....

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

在 + iOS15上使用 .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
}
}

我想指出的是。OnTapGesture 可能会使用用于导航链接的事件。您可以选择使用拖动手势,这不应该与最常见的元素冲突。

.gesture(
DragGesture().onEnded { value in
self.dismissKeyboard()
})

但是,这样可以防止刷新操作。我通过在 CoporateIdentity 视图的背景视图中添加事件来避免这种情况:

struct CIView: View {
var displayView: AnyView
  

var body: some View {
ZStack{
Color("Background").edgesIgnoringSafeArea(.all)
.gesture(
DragGesture().onEnded { value in
self.dismissKeyboard()
})
displayView
}
.foregroundColor(Color("Foreground"))
}
  

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

这个视图可以这样使用:

  CIView(displayView: AnyView(YourView()))