Comment masquer le clavier lors de l'utilisation de SwiftUI?

88

Comment se cacher en keyboardutilisant SwiftUIpour les cas ci-dessous?

Cas 1

J'ai TextFieldet je dois masquer le keyboardlorsque l'utilisateur clique sur le returnbouton.

Cas 2

J'ai TextFieldet je dois masquer le keyboardlorsque l'utilisateur tape à l'extérieur.

Comment puis-je faire cela en utilisant SwiftUI?

Remarque:

Je n'ai pas posé de question concernant UITextField. Je veux le faire en utilisant SwifUI.TextField.

Hitesh Surani
la source
29
@DannyBuonocore Relisez attentivement ma question!
Hitesh Surani
9
@DannyBuonocore Ce n'est pas un double de la question mentionnée. Cette question concerne SwiftUI, et d'autres sont UIKit normal
Johnykutty
1
@DannyBuonocore, veuillez consulter developer.apple.com/documentation/swiftui pour trouver la différence entre UIKit et SwiftUI. Merci
Hitesh Surani
J'ai ajouté ma solution ici j'espère qu'elle vous aidera.
Victor Kushnerov le
La plupart des solutions ici ne fonctionnent pas comme souhaité, car elles désactivent les réactions souhaitées sur les autres robinets de commande. Une solution de travail peut être trouvée ici: forums.developer.apple.com/thread/127196
Hardy

Réponses:

79

Vous pouvez forcer le premier répondeur à démissionner en envoyant une action à l'application partagée:

extension UIApplication {
    func endEditing() {
        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

Vous pouvez maintenant utiliser cette méthode pour fermer le clavier quand vous le souhaitez:

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

Si vous souhaitez fermer le clavier avec un tap out, vous pouvez créer une vue blanche plein écran avec une action tap, qui déclenchera le 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()
    }
}
rraphael
la source
1
.keyWindowest désormais obsolète. Voir la réponse de Lorenzo Santini .
LinusGeffarth
3
Aussi, .tapActiona été renommé en.onTapGesture
LinusGeffarth
Le clavier peut-il être ignoré lorsqu'une commande alternative devient active? stackoverflow.com/questions/58643512/…
Yarm
1
Y a-t-il un moyen de faire cela sans le fond blanc, j'utilise des entretoises et j'en ai besoin pour détecter un geste de tapotement sur l'entretoise. De plus, la stratégie d'arrière-plan blanc crée un problème sur les nouveaux iPhones où il y a maintenant plus d'espace d'écran au-dessus. Toute aide appréciée!
Joseph Astrahan
J'ai publié une réponse qui améliore votre conception. N'hésitez pas à apporter des modifications à votre réponse si vous voulez que je ne me soucie pas du crédit.
Joseph Astrahan
61

Après de nombreuses tentatives, j'ai trouvé une solution qui (actuellement) ne bloque aucun contrôle - l'ajout de reconnaissance de gestes à UIWindow.

  1. Si vous souhaitez fermer le clavier uniquement en appuyant sur l'extérieur (sans gérer les traînées), il suffit d'utiliser juste UITapGestureRecognizeret simplement copier l'étape 3:
  2. Créez une classe de reconnaissance de mouvement personnalisée qui fonctionne avec toutes les touches:

    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. Dans SceneDelegate.swiftdans le func scene, ajoutez le code suivant:

    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. Mettre UIGestureRecognizerDelegateen œuvre pour permettre des touches simultanées.

    extension SceneDelegate: UIGestureRecognizerDelegate {
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            return true
        }
    }
    

Désormais, n'importe quel clavier de n'importe quelle vue sera fermé au toucher ou en le faisant glisser à l'extérieur.

PS Si vous souhaitez fermer uniquement des TextFields spécifiques - ajoutez et supprimez le module de reconnaissance de gestes à la fenêtre chaque fois que vous êtes appelé rappel de TextField onEditingChanged

Mikhail
la source
3
Cette réponse devrait être en haut. Les autres réponses échouent lorsqu'il existe d'autres contrôles dans la vue.
Imthath
1
@RolandLariotte a mis à jour la réponse pour corriger ce problème, regardez la nouvelle implémentation d'AnyGestureRecognizer
Mikhail
1
Réponse géniale. Fonctionne parfaitement. @Mikhail est réellement intéressé de savoir comment supprimer le module de reconnaissance de gestes spécifiquement pour certains champs de texte (j'ai créé une saisie semi-automatique avec des balises, donc chaque fois que je touche un élément de la liste, je ne veux pas que ce champ de texte spécifique perde le focus)
Pasta
1
cette solution est en fait excellente, mais après l'avoir utilisée pendant environ 3 mois, j'ai malheureusement trouvé un bogue, directement causé par ce type de piratage. s'il vous plaît, soyez conscient que la même chose vous
arrive
1
Réponse fantastique! Je me demande comment cela sera implémenté avec iOS 14 sans le délégué de scène?
Dom
28

La réponse de @ RyanTCB est bonne; voici quelques améliorations qui simplifient l'utilisation et évitent un crash potentiel:

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

La 'correction de bogue' est simplement que keyWindow!.endEditing(true)correctement devrait êtrekeyWindow?.endEditing(true) (oui, vous pourriez dire que cela ne peut pas arriver.)

Plus intéressant, c'est comment vous pouvez l'utiliser. Par exemple, supposons que vous ayez un formulaire contenant plusieurs champs modifiables. Emballez-le simplement comme ceci:

Form {
    .
    .
    .
}
.modifier(DismissingKeyboard())

Maintenant, en tapant sur n'importe quel contrôle qui ne présente pas de clavier lui-même, le rejet sera approprié.

(Testé avec beta 7)

Feldur
la source
6
Hmmm - taper sur d'autres contrôles ne fait plus de registres. L'événement est avalé.
Yarm
Je ne peux pas reproduire cela - cela fonctionne toujours pour moi en utilisant les dernières gouttes d'Apple à partir du 11/1. Cela a-t-il fonctionné et ensuite arrêté de travailler pour vous, ou ??
Feldur
Si vous avez un DatePicker dans le formulaire, alors le DatePicker ne sera plus affiché
Albert
@Albert - c'est vrai; pour utiliser cette approche, vous devrez décomposer où les éléments sont décorés avec DismissingKeyboard () à un niveau plus fin qui s'applique aux éléments qui doivent ignorer et évite le DatePicker.
Feldur
L'utilisation de ce code reproduira l'avertissementCan't find keyplane that supports type 4 for keyboard iPhone-PortraitChoco-NumberPad; using 25686_PortraitChoco_iPhone-Simple-Pad_Default
np2314
23

J'ai vécu cela en utilisant un TextField dans un NavigationView. C'est ma solution pour ça. Il supprimera le clavier lorsque vous commencerez à faire défiler.

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)})
DubluDe
la source
Cela conduira onDelete (glisser pour supprimer) à un comportement étrange.
Tarek Hallak le
C'est bien, mais qu'en est-il du robinet?
Danny182
20

J'ai trouvé un autre moyen de supprimer le clavier qui ne nécessite pas d'accéder à la keyWindowpropriété; en fait, le compilateur renvoie un avertissement en utilisant

UIApplication.shared.keyWindow?.endEditing(true)

'keyWindow' était obsolète dans iOS 13.0: ne doit pas être utilisé pour les applications qui prennent en charge plusieurs scènes car il renvoie une fenêtre clé sur toutes les scènes connectées

Au lieu de cela, j'ai utilisé ce code:

UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
Lorenzo Santini
la source
15

SwiftUI dans le fichier 'SceneDelegate.swift' ajoutez simplement: .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()
        }
    }

cela suffit pour chaque vue utilisant le clavier de votre application ...

Dim Novo
la source
4
Cela pose un autre problème - j'ai un sélecteur dans le formulaire {} à côté du champ de texte, il ne répond plus. Je n'ai pas trouvé de solution en utilisant toutes les réponses de ce sujet. Mais votre réponse est une bonne chose pour ignorer le clavier avec un robinet ailleurs - si vous n'utilisez pas de sélecteurs.
Nalov
Bonjour. mon code `` `var body: some View {NavigationView {Form {Section {TextField (" typesomething ", text: $ c)} Section {Picker (" name ", selection: $ sel) {ForEach (0 .. <200 ) {Text ("(self.array [$ 0])%")}}} `` `` Le clavier est ignoré lorsque vous appuyez ailleurs, mais le sélecteur ne répond plus. Je n'ai pas trouvé de moyen de le faire fonctionner.
Nalov
2
Salut encore, pour le moment, j'ai deux solutions: la première - consiste à utiliser le clavier natif ignoré sur le bouton de retour, la seconde - est de modifier légèrement la gestion du tapotement (aka 'костыль') - window.rootViewController = UIHostingController (rootView : contentView.onTapGesture (count: 2, perform: {window.endEditing (true)})) J'espère que cela vous aidera ...
Dim Novo
Bonjour. Merci. La deuxième façon l'a résolu. J'utilise le pavé numérique, donc les utilisateurs ne peuvent entrer que des chiffres, il n'a pas de clé de retour. Rejeter avec tapotement était ce que je cherchais.
Nalov
cela entraînera une liste impossible de naviguer.
Cui Mingda
14

SwiftUI 2

Voici une solution mise à jour pour SwiftUI 2 / iOS 14 (initialement proposée ici par Mikhail).

Il n'utilise AppDelegateni le ni le SceneDelegatequi sont manquants si vous utilisez le cycle de vie 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
    }
}

Voici un exemple de détection de gestes simultanés à l'exception des gestes Long Press:

extension UIApplication: UIGestureRecognizerDelegate {
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return !otherGestureRecognizer.isKind(of: UILongPressGestureRecognizer.self)
    }
}
pawello2222
la source
2
Cela fonctionne parfaitement! Merci pour la solution
NotAPhoenix
2
Cela devrait être au top car garde à l'esprit le nouveau cycle de vie SwiftUI.
carlosobedgomez
Cela fonctionne très bien. Cependant, si je touche deux fois dans un champ de texte, au lieu de sélectionner le texte, le clavier disparaît maintenant. Une idée de la façon dont je peux autoriser le double tap pour la sélection?
Gary
@Gary Dans l'extension du bas, vous pouvez voir la ligne avec le commentaire défini sur false si vous ne voulez pas détecter le tap pendant d'autres gestes . Réglez-le simplement sur return false.
pawello2222
Le définir sur false fonctionne, mais le clavier ne disparaît pas non plus si quelqu'un appuie longtemps, fait glisser ou fait défiler en dehors de la zone de texte. Existe-t-il un moyen de le définir sur false uniquement pour les doubles clics (de préférence, des doubles clics à l'intérieur du champ de texte, mais même tous les doubles clics feraient l'affaire).
Gary
11

Ma solution comment masquer le clavier logiciel lorsque les utilisateurs tapent à l'extérieur. Vous devez utiliser contentShapeavec onLongPressGesturepour détecter l'ensemble du conteneur View. onTapGesturenécessaire pour éviter de bloquer la mise au point sur TextField. Vous pouvez utiliser à la onTapGestureplace de onLongPressGesturemais les éléments NavigationBar ne fonctionneront pas.

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: {})
    }
}
Victor Kushnerov
la source
Cela fonctionnait très bien, je l'ai utilisé légèrement différemment et je devais être sûr qu'il était appelé sur le fil principal.
keegan3d
7

ajoutez ce modificateur à la vue que vous souhaitez détecter les tapotements des utilisateurs

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

        }
RyanTCB
la source
7

Je préfère utiliser le .onLongPressGesture(minimumDuration: 0), qui ne fait pas clignoter le clavier lorsqu'un autre TextViewest activé (effet secondaire de .onTapGesture). Le code de clavier de masquage peut être une fonction réutilisable.

.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)
}
George Valkov
la source
Encore un scintillement en utilisant cette méthode.
Daniel Ryan
Cela fonctionnait très bien, je l'ai utilisé légèrement différemment et je devais être sûr qu'il était appelé sur le fil principal.
keegan3d
6

Parce que keyWindowc'est obsolète.

extension View {
    func endEditing(_ force: Bool) {
        UIApplication.shared.windows.forEach { $0.endEditing(force)}
    }
}
msk
la source
1
Le forceparamètre n'est pas utilisé. Ça devrait être{ $0.endEditing(force)}
Davide
5

On dirait que la endEditingsolution est la seule comme l'a souligné @rraphael.
L'exemple le plus clair que j'ai vu jusqu'à présent est celui-ci:

extension View {
    func endEditing(_ force: Bool) {
        UIApplication.shared.keyWindow?.endEditing(force)
    }
}

puis l'utiliser dans le onCommit:

zero3nna
la source
2
.keyWindowest désormais obsolète. Voir la réponse de Lorenzo Santini .
LinusGeffarth
Il est déprécié sur iOS 13+
Ahmadreza le
4

En développant la réponse de @Feldur (qui était basée sur @ RyanTCB), voici une solution encore plus expressive et puissante vous permettant de supprimer le clavier sur d'autres gestes que onTapGesture , vous pouvez spécifier ce que vous voulez dans l'appel de fonction.

Usage

// MARK: - View
extension RestoreAccountInputMnemonicScreen: View {
    var body: some View {
        List(viewModel.inputWords) { inputMnemonicWord in
            InputMnemonicCell(mnemonicInput: inputMnemonicWord)
        }
        .dismissKeyboard(on: [.tap, .drag])
    }
}

Ou en utilisant All.gestures(juste du sucre pour Gestures.allCases🍬)

.dismissKeyboard(on: All.gestures)

Code

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

Un mot d'avertissement

Veuillez noter que si vous utilisez tous les gestes, ils pourraient entrer en conflit et je n'ai pas trouvé de solution intéressante pour résoudre ce problème.

Sajjon
la source
que signifie eraseToAny()
Ravindra_Bhati
2

Cette méthode vous permet de masquer le clavier sur les entretoises!

Ajoutez d'abord cette fonction (Crédit accordé à: Casper Zandbergen, de SwiftUI ne peut pas appuyer sur 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
        }
    }
}

Ensuite, ajoutez les 2 fonctions suivantes (Crédit accordé à: rraphael, de cette question)

extension UIApplication {
    func endEditing() {
        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

La fonction ci-dessous serait ajoutée à votre classe View, reportez-vous simplement à la première réponse de rraphael pour plus de détails.

private func endEditing() {
   UIApplication.shared.endEditing()
}

Enfin, vous pouvez maintenant simplement appeler ...

Spacer().onTapGesture {
    self.endEditing()
}

Cela permettra à toute zone d'espacement de fermer le clavier maintenant. Plus besoin d'une grande vue de fond blanc!

Vous pouvez hypothétiquement appliquer cette technique extensionà toutes les commandes dont vous avez besoin pour prendre en charge TapGestures qui ne le font pas actuellement et appeler la onTapGesturefonction en combinaison avec self.endEditing()pour fermer le clavier dans n'importe quelle situation que vous souhaitez.

Joseph Astrahan
la source
Ma question est maintenant de savoir comment déclencher une validation sur un champ de texte lorsque vous faites disparaître le clavier de cette façon? actuellement, le 'commit' ne se déclenche que si vous appuyez sur la touche de retour sur le clavier iOS.
Joseph Astrahan
2

Veuillez vérifier https://github.com/michaelhenry/KeyboardAvoider

Incluez simplement KeyboardAvoider {}en haut de votre vue principale et c'est tout.

KeyboardAvoider {
    VStack { 
        TextField()
        TextField()
        TextField()
        TextField()
    }

}
Michael Henry
la source
Cela ne fonctionne pas pour une vue Formulaire avec des champs de texte. Le formulaire n'apparaît pas.
Nils
2

Sur la base de la réponse de @ Sajjon, voici une solution vous permettant de supprimer les gestes du clavier au toucher, appui long, glissement, grossissement et rotation selon votre choix.

Cette solution fonctionne dans XCode 11.4

Utilisation pour obtenir le comportement demandé par @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)
    }
}

Code

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))
    }
}
Nicolas Mandica
la source
2

Vous pouvez éviter complètement l'interaction avec UIKit et l'implémenter dans SwiftUI pur . Ajoutez simplement un .id(<your id>)modificateur à votre TextFieldet modifiez sa valeur chaque fois que vous souhaitez ignorer le clavier (en faisant glisser, appuyez sur la vue, action sur le bouton, ..).

Exemple d'implémentation:

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

Notez que je ne l'ai testé que dans la dernière version bêta de Xcode 12, mais cela devrait fonctionner avec les anciennes versions (même Xcode 11) sans aucun problème.

Josefdolezal
la source
0

ReturnTouche du clavier

En plus de toutes les réponses sur le fait d'appuyer en dehors de textField, vous souhaiterez peut-être fermer le clavier lorsque l'utilisateur appuie sur la touche de retour du clavier:

définir cette fonction globale:

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

Et ajoutez l'utilisation en onCommitargument:

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

Avantages

  • Vous pouvez l'appeler de n'importe où
  • Il ne dépend pas de UIKit ou SwiftUI (peut être utilisé dans les applications Mac)
  • Cela fonctionne même sous iOS 13

Démo

démo

Mojtaba Hosseini
la source
0

Jusqu'à présent, les options ci-dessus ne fonctionnaient pas pour moi, car j'ai un formulaire et des boutons internes, des liens, un sélecteur ...

Je crée ci-dessous le code qui fonctionne, avec l'aide des exemples ci-dessus.

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

Usage:

 var body: some View {
        NavigationView {
            Form {
                picker
                button
                textfield
                text
            }
            .dismissingKeyboard()
zdravko zdravkin
la source
-2

SwiftUI publié en juin / 2020 avec Xcode 12 et iOS 14 ajoute le modificateur hideKeyboardOnTap (). Cela devrait résoudre votre cas numéro 2. La solution pour votre cas numéro 1 est fournie gratuitement avec Xcode 12 et iOS 14: le clavier par défaut de TextField se masque automatiquement lorsque le bouton Retour est enfoncé.

Marcel Mendes Filho
la source
1
Il n'y a pas de modificateur hideKeyboardOnTap dans iOS14
Teo Sartori