Como esconder keyboard
usando SwiftUI
para os casos abaixo?
Caso 1
Eu tenho TextField
e preciso ocultar o keyboard
quando o usuário clica no return
botão.
Caso 2
Eu tenho TextField
e preciso esconder o keyboard
quando o usuário toca fora.
Como posso fazer isso usando SwiftUI
?
Nota:
Eu não fiz uma pergunta a respeito UITextField
. Eu quero fazer isso usando SwifUI.TextField
.
Respostas:
Você pode forçar o primeiro respondente a renunciar enviando uma ação para o aplicativo compartilhado:
extension UIApplication { func endEditing() { sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } }
Agora você pode usar este método para fechar o teclado sempre que desejar:
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() } } } }
Se você deseja fechar o teclado com um toque, pode criar uma visualização em tela inteira branca com uma ação de toque, que irá acionar
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() } }
fonte
.keyWindow
agora está obsoleto. Veja a resposta de Lorenzo Santini ..tapAction
foi renomeado para.onTapGesture
Depois de muitas tentativas, encontrei uma solução que (atualmente) não bloqueia nenhum controle - adicionando o reconhecedor de gestos ao
UIWindow
.UITapGestureRecognizer
e apenas copiar a etapa 3:Crie uma classe de reconhecedor de gestos personalizada que funciona com qualquer toque:
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 } }
No
SceneDelegate.swift
nofunc scene
, adicione seguinte código: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)
Implemente
UIGestureRecognizerDelegate
para permitir toques simultâneos.extension SceneDelegate: UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } }
Agora, qualquer teclado em qualquer visualização será fechado ao tocar ou arrastar para fora.
PS Se você deseja fechar apenas TextFields específicos - então adicione e remova o reconhecedor de gestos da janela sempre que for chamado de retorno de TextField
onEditingChanged
fonte
A resposta de @RyanTCB é boa; Aqui estão alguns refinamentos que o tornam mais simples de usar e evitam uma possível falha:
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) } } }
A 'correção de bug' é simplesmente que
keyWindow!.endEditing(true)
deveria serkeyWindow?.endEditing(true)
(sim, você pode argumentar que isso não pode acontecer).Mais interessante é como você pode usá-lo. Por exemplo, suponha que você tenha um formulário com vários campos editáveis. Basta embrulhar assim:
Form { . . . } .modifier(DismissingKeyboard())
Agora, tocar em qualquer controle que não apresente um teclado fará o descarte apropriado.
(Testado com beta 7)
fonte
Can't find keyplane that supports type 4 for keyboard iPhone-PortraitChoco-NumberPad; using 25686_PortraitChoco_iPhone-Simple-Pad_Default
Eu experimentei isso ao usar um TextField dentro de um NavigationView. Esta é a minha solução para isso. Isso dispensará o teclado quando você começar a rolar.
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)})
fonte
Encontrei outra maneira de dispensar o teclado que não requer acesso à
keyWindow
propriedade; na verdade, o compilador dá um aviso usandoUIApplication.shared.keyWindow?.endEditing(true)
Em vez disso, usei este código:
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
fonte
SwiftUI no arquivo 'SceneDelegate.swift' basta adicionar: .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() } }
isso é o suficiente para cada visualização usando o teclado em seu aplicativo ...
fonte
SwiftUI 2
Aqui está uma solução atualizada para SwiftUI 2 / iOS 14 (originalmente proposta aqui por Mikhail).
Ele não usa
AppDelegate
nem oSceneDelegate
que estão faltando se você usar o ciclo de vida 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 } }
Aqui está um exemplo de como detectar gestos simultâneos, exceto gestos Long Press:
extension UIApplication: UIGestureRecognizerDelegate { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return !otherGestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) } }
fonte
return false
.Minha solução como esconder o teclado do software quando os usuários tocam fora. Você precisa usar
contentShape
comonLongPressGesture
para detectar todo o container View.onTapGesture
necessário para evitar o bloqueio do focoTextField
. Você pode usar emonTapGesture
vez de,onLongPressGesture
mas os itens de NavigationBar não funcionarão.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: {}) } }
fonte
adicione este modificador à vista que deseja detectar toques do usuário
.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) }
fonte
Prefiro usar o
.onLongPressGesture(minimumDuration: 0)
, o que não faz com que o teclado pisque quando outroTextView
é ativado (efeito colateral de.onTapGesture
). O código de teclado oculto pode ser uma função reutilizável..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) }
fonte
Porque
keyWindow
está obsoleto.extension View { func endEditing(_ force: Bool) { UIApplication.shared.windows.forEach { $0.endEditing(force)} } }
fonte
force
parâmetro não é usado. Deveria ser{ $0.endEditing(force)}
Parece que a
endEditing
solução é a única como @rraphael apontou.O exemplo mais limpo que vi até agora é este:
extension View { func endEditing(_ force: Bool) { UIApplication.shared.keyWindow?.endEditing(force) } }
e então usá-lo no
onCommit:
fonte
.keyWindow
agora está obsoleto. Veja a resposta de Lorenzo Santini .Expandindo a resposta de @Feldur (que foi baseada em @ RyanTCB), aqui está uma solução ainda mais expressiva e poderosa que permite dispensar o teclado em outros gestos do que
onTapGesture
você pode especificar qual deseja na chamada de função.Uso
// MARK: - View extension RestoreAccountInputMnemonicScreen: View { var body: some View { List(viewModel.inputWords) { inputMnemonicWord in InputMnemonicCell(mnemonicInput: inputMnemonicWord) } .dismissKeyboard(on: [.tap, .drag]) } }
Ou usando
All.gestures
(apenas açúcar paraGestures.allCases
🍬).dismissKeyboard(on: All.gestures)
Código
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)) } }
Palavra de cautela
Por favor, note que se você usar todos os gestos, eles podem entrar em conflito e eu não encontrei nenhuma solução legal para isso.
fonte
eraseToAny()
Este método permite ocultar o teclado em espaçadores!
Primeiro adicione esta função (Crédito dado a: Casper Zandbergen, do SwiftUI não pode tocar no espaçador do 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 } } }
Em seguida, adicione as 2 funções a seguir (Crédito dado a: rraphael, desta questão)
extension UIApplication { func endEditing() { sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } }
A função abaixo seria adicionada à sua classe View, apenas consulte a principal resposta aqui de rraphael para mais detalhes.
private func endEditing() { UIApplication.shared.endEditing() }
Finalmente, você pode simplesmente ligar para ...
Spacer().onTapGesture { self.endEditing() }
Isso fará com que qualquer área do espaçador feche o teclado agora. Não há mais necessidade de uma grande visão de fundo branco!
Você poderia, hipoteticamente, aplicar esta técnica
extension
a quaisquer controles de que precise para suportar TapGestures que atualmente não o fazem e chamar aonTapGesture
função em combinação comself.endEditing()
para fechar o teclado em qualquer situação desejada.fonte
Verifique https://github.com/michaelhenry/KeyboardAvoider
Basta incluir
KeyboardAvoider {}
no topo de sua visualização principal e pronto.KeyboardAvoider { VStack { TextField() TextField() TextField() TextField() } }
fonte
Com base na resposta de @Sajjon, aqui está uma solução que permite dispensar o teclado com um toque, manter pressionado, arrastar, ampliar e gestos de rotação de acordo com sua escolha.
Esta solução está funcionando no XCode 11.4
Uso para obter o comportamento solicitado por @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) } }
Código
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)) } }
fonte
Você pode evitar completamente a interação com o UIKit e implementá-lo em SwiftUI puro . Basta adicionar um
.id(<your id>)
modificador ao seuTextField
e alterar seu valor sempre que desejar dispensar o teclado (ao deslizar, ver toque, ação do botão, ..).Implementação de amostra:
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 }) } } }
Observe que eu apenas testei no último Xcode 12 beta, mas ele deve funcionar com versões mais antigas (até mesmo o Xcode 11) sem nenhum problema.
fonte
Return
Tecla do tecladoAlém de todas as respostas sobre como tocar fora de textField, você pode dispensar o teclado quando o usuário tocar na tecla Return do teclado:
definir esta função global:
func resignFirstResponder() { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) }
E adicione o uso no
onCommit
argumento:TextField("title", text: $text, onCommit: { resignFirstResponder() })
Benefícios
Demo
fonte
Até agora, as opções acima não funcionaram para mim, porque tenho Formulários e botões internos, links, seletor ...
Eu crio o código abaixo que está funcionando, com a ajuda dos exemplos acima.
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()) } }
Uso:
var body: some View { NavigationView { Form { picker button textfield text } .dismissingKeyboard()
fonte
SwiftUI lançado em junho / 2020 com Xcode 12 e iOS 14 adiciona o modificador hideKeyboardOnTap (). Isso deve resolver seu caso número 2. A solução para seu caso número 1 vem de graça com o Xcode 12 e iOS 14: o teclado padrão para TextField oculta automaticamente quando o botão Return é pressionado.
fonte