Jak ukryć klawiaturę podczas korzystania ze SwiftUI?


88

Jak ukryć keyboardużywanie SwiftUIw poniższych przypadkach?

Przypadek 1

Mam TextFieldi muszę ukryć, keyboardkiedy użytkownik kliknie returnprzycisk.

Przypadek 2

Mam TextFieldi muszę ukryć, keyboardkiedy 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.


29
@DannyBuonocore Przeczytaj ponownie uważnie moje pytanie!
Hitesh Surani

9
@DannyBuonocore To nie jest duplikat wspomnianego pytania. To pytanie dotyczy SwiftUI, a inne to normalny UIKit
Johnykutty

1
@DannyBuonocore Zajrzyj na developer.apple.com/documentation/swiftui, aby znaleźć różnicę między UIKit a SwiftUI. Dzięki
Hitesh Surani

Dodałem tutaj moje rozwiązanie , mam nadzieję, że ci pomoże.
Victor Kushnerov

Większość rozwiązań tutaj nie działa zgodnie z oczekiwaniami, ponieważ wyłączają pożądane reakcje na innych dotknięciach kontrolnych. Działające rozwiązanie można znaleźć tutaj: forums.developer.apple.com/thread/127196
Hardy

Odpowiedzi:


79

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

1
.keyWindowjest teraz przestarzała. Zobacz odpowiedź Lorenzo Santiniego .
LinusGeffarth

3
Ponadto, .tapActionzostała zmieniona na.onTapGesture
LinusGeffarth

Czy klawiaturę można zamknąć, gdy zostanie aktywowana alternatywna kontrolka? stackoverflow.com/questions/58643512/…
Yarm

1
Czy istnieje sposób na zrobienie tego bez białego tła, używam elementów dystansowych i potrzebuję go do wykrywania gestu dotknięcia elementu dystansowego. Również strategia białego tła stwarza problem na nowszych iPhone'ach, w których jest teraz dodatkowa przestrzeń na ekranie. Każda pomoc doceniona!
Joseph Astrahan,

Opublikowałem odpowiedź, która ulepsza Twój projekt. Jeśli chcesz, możesz edytować swoją odpowiedź. Nie obchodzi mnie kredyt.
Joseph Astrahan,

61

Po wielu próbach znalazłem rozwiązanie, które (obecnie) nie blokuje żadnych kontrolek - dodałem do niego rozpoznawanie gestów UIWindow.

  1. Jeśli chcesz zamknąć klawiaturę tylko po naciśnięciu na zewnątrz (bez obsługi przeciągnięć) - wystarczy użyć UITapGestureRecognizeri po prostu skopiować krok 3:
  2. 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
        }
    }
    
  3. W SceneDelegate.swiftw func 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)  
    
  4. Zaimplementuj, UIGestureRecognizerDelegateaby 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


3
Ta odpowiedź powinna znajdować się na górze. Inne odpowiedzi kończą się niepowodzeniem, gdy w widoku są inne kontrolki.
Imthath

1
@RolandLariotte zaktualizowała odpowiedź, aby naprawić to zachowanie, spójrz na nową implementację AnyGestureRecognizer
Michaił

1
Świetna odpowiedź. Działa bez zarzutu. @Mikhail jest naprawdę zainteresowany tym, jak usunąć rozpoznawanie gestów specjalnie dla niektórych pól tekstowych (zbudowałem autouzupełnianie z tagami, więc za każdym razem, gdy dotykam elementu na liście, nie chcę, aby to konkretne pole tekstowe straciło ostrość)
Pasta

1
to rozwiązanie jest naprawdę świetne, ale po 3 miesiącach używania go niestety znalazłem błąd, bezpośrednio spowodowany tego rodzaju włamaniem. proszę, bądźcie świadomi, że to samo dzieje się z wami
glassomoss

1
Fantastyczna odpowiedź! Zastanawiam się, jak to będzie zaimplementowane z iOS 14 bez scedelegate?
Dom

28

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)


6
Hmmm - stukanie w inne kontrolki nie jest już rejestrowane. Wydarzenie zostało połknięte.
Yarm

Nie mogę tego powtórzyć - nadal działa dla mnie, korzystając z najnowszych kropli od Apple od 1.11. Czy zadziałało, a potem po prostu przestałem działać dla Ciebie, czy ??
Feldur

Jeśli masz DatePicker w formularzu, to DatePicker nie będzie już wyświetlany
Albert

@Albert - to prawda; Aby użyć tego podejścia, będziesz musiał rozbić, gdzie elementy są ozdobione DismissingKeyboard (), do drobniejszego poziomu, który ma zastosowanie do elementów, które powinny zostać odrzucone i unika DatePicker.
Feldur

Użycie tego kodu spowoduje odtworzenie ostrzeżeniaCan't find keyplane that supports type 4 for keyboard iPhone-PortraitChoco-NumberPad; using 25686_PortraitChoco_iPhone-Simple-Pad_Default
np2314

23

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

Doprowadzi to do dziwnego zachowania onDelete (przesuń, aby usunąć).
Tarek Hallak

To miłe, ale co z kranem?
Danny182,

20

Znalazłem inny sposób na odrzucenie klawiatury, który nie wymaga dostępu do keyWindowwłaściwości; w rzeczywistości kompilator zwraca ostrzeżenie za pomocą

UIApplication.shared.keyWindow?.endEditing(true)

„keyWindow” został wycofany w iOS 13.0: nie powinien być używany w aplikacjach obsługujących wiele scen, ponieważ zwraca okno klucza we wszystkich połączonych scenach

Zamiast tego użyłem tego kodu:

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

15

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


4
Daje to inny problem - mam selektor w Form {} obok textfield, który przestał odpowiadać. Nie znalazłem rozwiązania, korzystając ze wszystkich odpowiedzi w tym temacie. Ale twoja odpowiedź jest dobra, aby odrzucić klawiaturę stuknięciem w innym miejscu - jeśli nie używasz zbieraczy.
Nalov

Witaj. mój kod `` `var body: some View {NavigationView {Form {Form {Section {TextField (" typesomething ", text: $ c)} Section {Picker (" name ", selection: $ sel) {ForEach (0 .. <200 ) {Text ("(self.array [$ 0])%")}}} `` Klawiatura jest odrzucana po dotknięciu w innym miejscu, ale selektor przestał odpowiadać. Nie znalazłem sposobu, żeby to zadziałało.
Nalov

2
Witam ponownie, w tej chwili mam dwa rozwiązania: pierwsze - to użycie natywnej klawiatury odrzuconej na przycisku powrotu, drugie - to nieznaczna zmiana obsługi podsłuchu (aka 'костыль') - window.rootViewController = UIHostingController (rootView : contentView.onTapGesture (count: 2, perform: {window.endEditing (true)})) Mam nadzieję, że to ci pomoże ...
Dim Novo

Witaj. Dziękuję Ci. Drugi sposób go rozwiązał. Używam klawiatury numerycznej, więc użytkownicy mogą wprowadzać tylko liczby, nie ma klawisza powrotu. Odrzucanie przez stukanie było tym, czego szukałem.
Nalov

spowoduje to, że po liście nie będzie można nawigować.
Cui Mingda

13

SwiftUI 2

Oto zaktualizowane rozwiązanie dla SwiftUI 2 / iOS 14 (pierwotnie zaproponowane tutaj przez Michaiła).

Nie używa AppDelegateani tych, SceneDelegatektó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)
    }
}

2
To działa idealnie! Dzięki za rozwiązanie
NotAPhoenix

2
To powinno być na topie, ponieważ pamiętaj o nowym cyklu życia SwiftUI.
carlosobedgomez

To działa świetnie. Jednak po dwukrotnym dotknięciu pola tekstowego zamiast zaznaczania tekstu klawiatura znika. Masz jakiś pomysł, jak mogę zezwolić na dwukrotne dotknięcie w celu zaznaczenia?
Gary

@Gary W dolnym rozszerzeniu możesz zobaczyć linię z komentarzem ustawionym na fałsz, jeśli nie chcesz wykrywać dotknięcia podczas innych gestów . Po prostu ustaw return false.
pawello2222

Ustawienie wartości false działa, ale klawiatura również nie odrzuca, jeśli ktoś długo naciska, przeciąga lub przewija poza obszar tekstowy. Czy jest jakiś sposób, aby ustawić to na fałsz tylko dla podwójnych kliknięć (najlepiej podwójne kliknięcia wewnątrz pola tekstowego, ale wystarczyłyby nawet wszystkie podwójne kliknięcia).
Gary

11

Moje rozwiązanie, jak ukryć klawiaturę programową, gdy użytkownicy dotykają na zewnątrz. Musisz użyć contentShapez, onLongPressGestureaby wykryć cały kontener widoku. onTapGesturewymagane, aby uniknąć blokowania ostrości TextField. Możesz użyć onTapGesturezamiast, onLongPressGestureale 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: {})
    }
}

To działało świetnie, użyłem go nieco inaczej i musiałem się upewnić, że został wywołany w głównym wątku.
keegan3d

7

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)

        }

7

Wolę używać .onLongPressGesture(minimumDuration: 0), co nie powoduje mrugania klawiatury, gdy TextViewaktywowany 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)
}

Wciąż migotanie przy użyciu tej metody.
Daniel Ryan

To działało świetnie, użyłem go nieco inaczej i musiałem się upewnić, że został wywołany w głównym wątku.
keegan3d

6

Ponieważ keyWindowjest przestarzały.

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

1
forceParametr nie jest używany. Powinno być{ $0.endEditing(force)}
Davide

5

Wygląda na to, że endEditingrozwią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:


2
.keyWindowjest teraz przestarzała. Zobacz odpowiedź Lorenzo Santiniego .
LinusGeffarth

W systemie iOS 13+ jest amortyzowany
Ahmadreza

4

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ż onTapGesturemoż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 za Gestures.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.


co to znaczy eraseToAny()
Ravindra_Bhati

2

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ę extensiondo dowolnych kontrolek potrzebnych do obsługi TapGestures, które obecnie tego nie obsługują, i wywołać onTapGesturefunkcję w połączeniu z, self.endEditing()aby zamknąć klawiaturę w dowolnej sytuacji.


Moje pytanie brzmi teraz, jak wywołać zatwierdzenie w polu tekstowym, gdy klawiatura odejdzie w ten sposób? obecnie „zatwierdzenie” jest wyzwalane tylko po naciśnięciu klawisza Return na klawiaturze iOS.
Joseph Astrahan,


2

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

2

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.


0

ReturnKlucz klawiatury

Opró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 onCommitargumentacji to:

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

Korzyści

  • Możesz zadzwonić z dowolnego miejsca
  • Nie jest zależny od UIKit ani SwiftUI (może być używany w aplikacjach na Maca)
  • Działa nawet w iOS 13

Próbny

próbny


0

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

-2

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.


1
W iOS14 nie ma modyfikatora hideKeyboardOnTap
Teo Sartori

Prawdopodobnie się pomyliłem
Jessy
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.