Jak ukryć keyboard
używanie SwiftUI
w poniższych przypadkach?
Przypadek 1
Mam TextField
i muszę ukryć, keyboard
kiedy użytkownik kliknie return
przycisk.
Przypadek 2
Mam TextField
i muszę ukryć, keyboard
kiedy użytkownik stuka na zewnątrz.
Jak mogę to zrobić za pomocą SwiftUI
?
Uwaga:
Nie zadałem pytania dotyczącego UITextField
. Chcę to zrobić za pomocą SwifUI.TextField
.
Odpowiedzi:
Możesz wymusić rezygnację pierwszego respondenta, wysyłając akcję do udostępnionej aplikacji:
extension UIApplication { func endEditing() { sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } }
Teraz możesz użyć tej metody, aby zamknąć klawiaturę, kiedy tylko chcesz:
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() } } } }
Jeśli chcesz zamknąć klawiaturę za pomocą stuknięcia, możesz utworzyć biały widok pełnoekranowy z akcją dotknięcia, która uruchomi
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() } }
źródło
.keyWindow
jest teraz przestarzała. Zobacz odpowiedź Lorenzo Santiniego ..tapAction
została zmieniona na.onTapGesture
Po wielu próbach znalazłem rozwiązanie, które (obecnie) nie blokuje żadnych kontrolek - dodałem do niego rozpoznawanie gestów
UIWindow
.UITapGestureRecognizer
i po prostu skopiować krok 3:Utwórz niestandardową klasę rozpoznawania gestów, która działa z każdym dotknięciem:
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 } }
W
SceneDelegate.swift
wfunc scene
, dodać kolejny kod: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)
Zaimplementuj,
UIGestureRecognizerDelegate
aby umożliwić jednoczesne dotknięcia.extension SceneDelegate: UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } }
Teraz każda klawiatura w dowolnym widoku zostanie zamknięta po dotknięciu lub przeciągnięciu na zewnątrz.
PS Jeśli chcesz zamknąć tylko określone pola tekstowe - dodaj i usuń rozpoznawanie gestów w oknie za każdym razem, gdy wywoływane jest wywołanie zwrotne TextField
onEditingChanged
źródło
Odpowiedź @ RyanTCB jest dobra; oto kilka udoskonaleń, które upraszczają korzystanie i pozwalają uniknąć potencjalnej awarii:
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) } } }
„Naprawianie błędów” jest po prostu takie, jakie
keyWindow!.endEditing(true)
powinno byćkeyWindow?.endEditing(true)
(tak, możesz twierdzić, że to się nie zdarzy)Bardziej interesujące jest to, jak możesz z niego korzystać. Na przykład załóżmy, że masz formularz z wieloma edytowalnymi polami. Po prostu zawiń to w ten sposób:
Form { . . . } .modifier(DismissingKeyboard())
Teraz dotknięcie dowolnej kontrolki, która sama nie przedstawia klawiatury, spowoduje odpowiednie odrzucenie.
(Testowane z wersją beta 7)
źródło
Can't find keyplane that supports type 4 for keyboard iPhone-PortraitChoco-NumberPad; using 25686_PortraitChoco_iPhone-Simple-Pad_Default
Doświadczyłem tego podczas używania TextField w NavigationView. To jest moje rozwiązanie. Odrzuci klawiaturę, gdy zaczniesz przewijać.
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)})
źródło
Znalazłem inny sposób na odrzucenie klawiatury, który nie wymaga dostępu do
keyWindow
właściwości; w rzeczywistości kompilator zwraca ostrzeżenie za pomocąUIApplication.shared.keyWindow?.endEditing(true)
Zamiast tego użyłem tego kodu:
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
źródło
SwiftUI w pliku „SceneDelegate.swift” po prostu dodaj: .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() } }
to wystarczy dla każdego widoku za pomocą klawiatury w Twojej aplikacji ...
źródło
SwiftUI 2
Oto zaktualizowane rozwiązanie dla SwiftUI 2 / iOS 14 (pierwotnie zaproponowane tutaj przez Michaiła).
Nie używa
AppDelegate
ani tych,SceneDelegate
których brakuje, jeśli używasz cyklu życia 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 } }
Oto przykład wykrywania jednoczesnych gestów z wyjątkiem gestów długiego naciśnięcia:
extension UIApplication: UIGestureRecognizerDelegate { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return !otherGestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) } }
źródło
return false
.Moje rozwiązanie, jak ukryć klawiaturę programową, gdy użytkownicy dotykają na zewnątrz. Musisz użyć
contentShape
z,onLongPressGesture
aby wykryć cały kontener widoku.onTapGesture
wymagane, aby uniknąć blokowania ostrościTextField
. Możesz użyćonTapGesture
zamiast,onLongPressGesture
ale elementy NavigationBar nie będą działać.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: {}) } }
źródło
dodaj ten modyfikator do widoku, w którym chcesz wykrywać dotknięcia użytkownika
.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) }
źródło
Wolę używać
.onLongPressGesture(minimumDuration: 0)
, co nie powoduje mrugania klawiatury, gdyTextView
aktywowany jest inny (efekt uboczny.onTapGesture
). Ukryj kod klawiatury może być funkcją wielokrotnego użytku..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) }
źródło
Ponieważ
keyWindow
jest przestarzały.extension View { func endEditing(_ force: Bool) { UIApplication.shared.windows.forEach { $0.endEditing(force)} } }
źródło
force
Parametr nie jest używany. Powinno być{ $0.endEditing(force)}
Wygląda na to, że
endEditing
rozwiązanie jest jedynym wskazanym przez @rraphael.Najczystszym przykładem, jaki do tej pory widziałem, jest:
extension View { func endEditing(_ force: Bool) { UIApplication.shared.keyWindow?.endEditing(force) } }
a następnie używając go w
onCommit:
źródło
.keyWindow
jest teraz przestarzała. Zobacz odpowiedź Lorenzo Santiniego .Rozszerzając odpowiedź @Feldur (która została oparta na @ RyanTCB), tutaj jest jeszcze bardziej ekspresyjne i potężne rozwiązanie, które pozwala odrzucić klawiaturę na inne gesty niż
onTapGesture
możesz określić, który chcesz w wywołaniu funkcji.Stosowanie
// MARK: - View extension RestoreAccountInputMnemonicScreen: View { var body: some View { List(viewModel.inputWords) { inputMnemonicWord in InputMnemonicCell(mnemonicInput: inputMnemonicWord) } .dismissKeyboard(on: [.tap, .drag]) } }
Lub używając
All.gestures
(tylko cukier zaGestures.allCases
🍬).dismissKeyboard(on: All.gestures)
Kod
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)) } }
Słowo ostrzeżenia
Zwróć uwagę, że jeśli użyjesz wszystkich gestów, mogą one powodować konflikty, a ja nie wymyśliłem żadnego zgrabnego rozwiązania tego problemu.
źródło
eraseToAny()
Ta metoda pozwala ukryć klawiaturę na podkładkach!
Najpierw dodaj tę funkcję (Kredyt przyznany : Casper Zandbergen, z SwiftUI nie można stuknąć w 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 } } }
Następnie dodaj następujące 2 funkcje (kredyt przyznany: rraphael, z tego pytania)
extension UIApplication { func endEditing() { sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } }
Poniższa funkcja zostanie dodana do Twojej klasy View, po prostu zapoznaj się z najlepszą odpowiedzią od rraphaela, aby uzyskać więcej informacji.
private func endEditing() { UIApplication.shared.endEditing() }
Wreszcie możesz teraz po prostu zadzwonić ...
Spacer().onTapGesture { self.endEditing() }
Spowoduje to, że dowolny obszar odstępnika zamknie teraz klawiaturę. Nie ma już potrzeby dużego, białego widoku tła!
Możesz hipotetycznie zastosować tę technikę
extension
do dowolnych kontrolek potrzebnych do obsługi TapGestures, które obecnie tego nie obsługują, i wywołaćonTapGesture
funkcję w połączeniu z,self.endEditing()
aby zamknąć klawiaturę w dowolnej sytuacji.źródło
Proszę sprawdzić https://github.com/michaelhenry/KeyboardAvoider
Po prostu uwzględnij
KeyboardAvoider {}
u góry głównego widoku i to wszystko.KeyboardAvoider { VStack { TextField() TextField() TextField() TextField() } }
źródło
W oparciu o odpowiedź @ Sajjon, oto rozwiązanie pozwalające na odrzucenie gestów klawiatury po dotknięciu, długim naciśnięciu, przeciągnięciu, powiększeniu i obracaniu zgodnie z Twoim wyborem.
To rozwiązanie działa w XCode 11.4
Użycie do uzyskania zachowania zadanego przez @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) } }
Kod
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)) } }
źródło
Możesz całkowicie uniknąć interakcji z UIKit i zaimplementować go w czystym SwiftUI . Po prostu dodaj
.id(<your id>)
modyfikator do swojegoTextField
i zmień jego wartość, kiedy chcesz zamknąć klawiaturę (po przesunięciu, widoku, dotknięciu, działaniu przycisku, ...).Przykładowa realizacja:
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 }) } } }
Zauważ, że przetestowałem go tylko w najnowszej wersji beta Xcode 12, ale powinien działać ze starszymi wersjami (nawet Xcode 11) bez żadnego problemu.
źródło
Return
Klucz klawiaturyOprócz wszystkich odpowiedzi dotyczących dotykania poza polem tekstowym możesz chcieć zamknąć klawiaturę, gdy użytkownik naciśnie klawisz powrotu na klawiaturze:
zdefiniuj tę globalną funkcję:
func resignFirstResponder() { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) }
I dodajmy w
onCommit
argumentacji to:TextField("title", text: $text, onCommit: { resignFirstResponder() })
Korzyści
Próbny
źródło
Jak dotąd powyższe opcje nie działały u mnie, ponieważ mam formularz i wewnątrz przyciski, linki, selektor ...
Poniżej tworzę działający kod, korzystając z powyższych przykładów.
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()) } }
Stosowanie:
var body: some View { NavigationView { Form { picker button textfield text } .dismissingKeyboard()
źródło
SwiftUI wydany w czerwcu / 2020 z Xcode 12 i iOS 14 dodaje modyfikator hideKeyboardOnTap (). To powinno rozwiązać problem numer 2. Rozwiązanie dla sprawy numer 1 jest dostępne bezpłatnie w Xcode 12 i iOS 14: domyślna klawiatura TextField ukrywa się automatycznie po naciśnięciu przycisku Return.
źródło