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

0

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

0

真正的SwiftUI解决方案

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

这将完美无缺地工作


0
我想指出,.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()))

0

我正在尝试在SwiftUIForms中实现单击隐藏键盘,同时Picker也应该能够通过单击工作。

我搜索了很多以找到一个适当的解决方案,但没有一个适用于我。所以我自己制作了一个扩展,它非常有效。

在您的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
    }
    
}

0

iOS 16

你可以简单地做到这一点,

Form {

}
.scrollDismissesKeyboard(.interactively)

-2

SwiftUI于2020年6月发布,搭配Xcode 12和iOS 14版本新增了hideKeyboardOnTap()修饰符。这应该可以解决您的第二个问题。 对于您的第一个问题,Xcode 12和iOS 14已经免费提供了解决方案:TextField的默认键盘会在按下Return按钮时自动隐藏。


1
iOS14中没有hideKeyboardOnTap修饰符。 - Teo Sartori
可能会感到困惑:https://github.com/jeroenzonneveld/SwiftUIFormHelper/blob/develop/Source/KeyboardDismissModifier.swift - user652038

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