在以下情况下如何隐藏keyboard
使用SwiftUI
?
情况1
我有TextField
,我需要keyboard
在用户单击return
按钮时隐藏。
情况二
我有TextField
,keyboard
当用户在外面轻按时,我需要隐藏它。
我该如何使用SwiftUI
呢?
注意:
我还没有问有关的问题UITextField
。我想用做SwifUI.TextField
。
在以下情况下如何隐藏keyboard
使用SwiftUI
?
情况1
我有TextField
,我需要keyboard
在用户单击return
按钮时隐藏。
情况二
我有TextField
,keyboard
当用户在外面轻按时,我需要隐藏它。
我该如何使用SwiftUI
呢?
注意:
我还没有问有关的问题UITextField
。我想用做SwifUI.TextField
。
Answers:
您可以通过向共享应用程序发送操作来强制第一响应者辞职:
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的答案。
.tapAction
已重命名为.onTapGesture
经过大量尝试,我发现了一种解决方案(当前)(不会)阻止任何控件-向中添加手势识别器UIWindow
。
UITapGestureRecognizer
仅需复制步骤3即可:创建可与任何触摸配合使用的自定义手势识别器类:
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
}
}
在SceneDelegate.swift
中func scene
,添加下一个代码:
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)
实施UIGestureRecognizerDelegate
以允许同时触摸。
extension SceneDelegate: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
现在,任何视图上的任何键盘都将在触摸或拖动到外部时关闭。
PS:如果您只想关闭特定的TextField,则每当调用TextField的回调时,在窗口中添加和删除手势识别器 onEditingChanged
@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测试)
Can't find keyplane that supports type 4 for keyboard iPhone-PortraitChoco-NumberPad; using 25686_PortraitChoco_iPhone-Simple-Pad_Default
我在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)})
我找到了另一种不需要访问该keyWindow
属性的方法来关闭键盘。事实上,编译器会使用
UIApplication.shared.keyWindow?.endEditing(true)
iOS 13.0中已弃用“ keyWindow”:不应将其用于支持多个场景的应用程序,因为它会返回所有已连接场景的关键窗口
相反,我使用了以下代码:
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
只需在“ SceneDelegate.swift”文件中添加SwiftUI:.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()
}
}
这对于使用应用程序中的键盘的每个视图就足够了...
这里是更新的溶液SwiftUI 2/14的iOS(最初提出这里米哈伊尔)。
如果您使用SwiftUI生命周期,它不会使用AppDelegate
或SceneDelegate
缺少的内容:
@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
}
}
这是一个示例,如何检测除长按手势以外的同时手势:
extension UIApplication: UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return !otherGestureRecognizer.isKind(of: UILongPressGestureRecognizer.self)
}
}
return false
。
我的解决方案是如何在用户点击外部时隐藏软件键盘。您需要使用contentShape
withonLongPressGesture
来检测整个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: {})
}
}
我更喜欢使用.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)
}
好像endEditing
解决方案是@rraphael指出的唯一解决方案。
到目前为止,我所看到的最干净的示例是:
extension View {
func endEditing(_ force: Bool) {
UIApplication.shared.keyWindow?.endEditing(force)
}
}
然后在 onCommit:
.keyWindow
现在已弃用。请参阅Lorenzo Santini的答案。
通过@Feldur(基于@RyanTCB的答案)扩展答案,这是一个更具表达力和功能的解决方案,允许您关闭键盘上的其他手势onTapGesture
,而无需在功能调用中指定键盘。
// MARK: - View
extension RestoreAccountInputMnemonicScreen: View {
var body: some View {
List(viewModel.inputWords) { inputMnemonicWord in
InputMnemonicCell(mnemonicInput: inputMnemonicWord)
}
.dismissKeyboard(on: [.tap, .drag])
}
}
或使用All.gestures
(只用糖作Gestures.allCases
🍬)
.dismissKeyboard(on: All.gestures)
enum All {
static let gestures = all(of: Gestures.self)
private static func all<CI>(of _: CI.Type) -> CI.AllCases where CI: CaseIterable {
return CI.allCases
}
}
enum Gestures: Hashable, CaseIterable {
case tap, longPress, drag, magnification, rotation
}
protocol ValueGesture: Gesture where Value: Equatable {
func onChanged(_ action: @escaping (Value) -> Void) -> _ChangedGesture<Self>
}
extension LongPressGesture: ValueGesture {}
extension DragGesture: ValueGesture {}
extension MagnificationGesture: ValueGesture {}
extension RotationGesture: ValueGesture {}
extension Gestures {
@discardableResult
func apply<V>(to view: V, perform voidAction: @escaping () -> Void) -> AnyView where V: View {
func highPrio<G>(
gesture: G
) -> AnyView where G: ValueGesture {
view.highPriorityGesture(
gesture.onChanged { value in
_ = value
voidAction()
}
).eraseToAny()
}
switch self {
case .tap:
// not `highPriorityGesture` since tapping is a common gesture, e.g. wanna allow users
// to easily tap on a TextField in another cell in the case of a list of TextFields / Form
return view.gesture(TapGesture().onEnded(voidAction)).eraseToAny()
case .longPress: return highPrio(gesture: LongPressGesture())
case .drag: return highPrio(gesture: DragGesture())
case .magnification: return highPrio(gesture: MagnificationGesture())
case .rotation: return highPrio(gesture: RotationGesture())
}
}
}
struct DismissingKeyboard: ViewModifier {
var gestures: [Gestures] = Gestures.allCases
dynamic func body(content: Content) -> some View {
let action = {
let forcing = true
let keyWindow = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow?.endEditing(forcing)
}
return gestures.reduce(content.eraseToAny()) { $1.apply(to: $0, perform: action) }
}
}
extension View {
dynamic func dismissKeyboard(on gestures: [Gestures] = Gestures.allCases) -> some View {
return ModifiedContent(content: self, modifier: DismissingKeyboard(gestures: gestures))
}
}
请注意,如果您使用所有手势,它们可能会发生冲突,因此我没有提出任何巧妙的解决方案来解决该问题。
eraseToAny()
此方法使您可以将键盘隐藏在垫片上!
首先添加此功能(来自SwiftUI的信用:Casper Zandbergen无法使用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)
}
}
下面的函数将添加到您的View类中,有关更多详细信息,请仅参考rraphael的此处最佳答案。
private func endEditing() {
UIApplication.shared.endEditing()
}
最后,您现在可以简单地致电...
Spacer().onTapGesture {
self.endEditing()
}
这将使任何间隔物区域现在关闭键盘。不再需要大的白色背景视图!
假设您可以将此技术extension
应用于支持TapGestures所需的任何控件,而这些控件目前尚不支持,并onTapGesture
结合使用该函数以在您需要的self.endEditing()
任何情况下关闭键盘。
请检查https://github.com/michaelhenry/KeyboardAvoider
只需KeyboardAvoider {}
在您的主视图上添加即可,仅此而已。
KeyboardAvoider {
VStack {
TextField()
TextField()
TextField()
TextField()
}
}
根据@Sajjon的答案,这是一个解决方案,可让您根据自己的选择在敲击,长按,拖动,放大和旋转手势时关闭键盘。
此解决方案在XCode 11.4中有效
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))
}
}
您可以完全避免与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)一起使用,而不会出现任何问题。
Return
按键除了有关在textField之外点击的所有答案之外,当用户点击键盘上的回车键时,您可能还希望关闭键盘:
定义此全局函数:
func resignFirstResponder() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
并在onCommit
参数中添加use :
TextField("title", text: $text, onCommit: {
resignFirstResponder()
})
到目前为止,以上选项对我而言不起作用,因为我具有窗体以及内部按钮,链接,选择器...
在上面的示例的帮助下,我创建了下面的有效代码。
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()
SwiftUI已于2020年6月/ Xcode 12和iOS 14发布,其中添加了hideKeyboardOnTap()修饰符。这样可以解决您的案例编号2。针对您的案例编号1的解决方案随Xcode 12和iOS 14免费提供:当按下Return键时,TextField的默认键盘会自动隐藏。