web-dev-qa-db-fra.com

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

Comment masquer keyboard en utilisant SwiftUI pour les cas ci-dessous?

Cas 1

J'ai TextField et je dois masquer le keyboard lorsque l'utilisateur clique sur le bouton return.

Cas 2

J'ai TextField et je dois masquer le keyboard lorsque 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).

47
IMHiteshSurani

Vous pouvez forcer le premier intervenant à 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 en plein écran avec une action tap, qui déclenchera la 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()
    }
}
47
rraphael

@ La réponse de RyanTCB est bonne; voici quelques améliorations qui le rendent plus simple à utiliser 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) devrait être correctement keyWindow?.endEditing(true) (oui, vous pourriez dire que cela ne peut pas arriver.)

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

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

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

(Testé avec beta 7)

16
Feldur

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

13
Dim Novo

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

  1. Si vous souhaitez fermer le clavier uniquement sur Tap dehors (sans gérer les traînées) - il suffit alors d'utiliser simplement UITapGestureRecognizer et de copier simplement l'étape 3:
  2. Créez une classe de reconnaissance de gestes personnalisée qui fonctionne avec toutes les touches:

    class AnyGestureRecognizer: UIGestureRecognizer {
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
            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.Swift ajouter 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. Implémentez UIGestureRecognizerDelegate pour permettre des contacts simultanés.

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

Maintenant, n'importe quel clavier sur n'importe quelle vue sera fermé au toucher ou glissé vers l'extérieur.

P.S. Si vous ne souhaitez fermer que des TextFields spécifiques, ajoutez et supprimez la reconnaissance des gestes dans la fenêtre chaque fois qu’elle est appelée rappel de TextField onEditingChanged

12
Mikhail

J'ai trouvé une autre façon de supprimer le clavier qui ne nécessite pas d'accéder à la propriété keyWindow; en fait, le compilateur renvoie un avertissement en utilisant

UIApplication.shared.keyWindow?.endEditing(true)

'keyWindow' est déconseillé 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 de 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)
11
Lorenzo Santini

J'ai vécu cela en utilisant un TextField dans un NavigationView. Ceci est ma solution pour cela. Il fermera 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)})
10
DubluDe

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)

        }
6
RyanTCB

Parce que keyWindow est obsolète.

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

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 rejeter le clavier sur d'autres gestes que onTapGesture, vous pouvez spécifier ce que vous voulez dans la fonction appel.

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 peuvent entrer en conflit et je n'ai trouvé aucune solution intéressante pour résoudre ce problème.

4
Sajjon

On dirait que la solution endEditing est la seule comme @rraphael l'a souligné.
L'exemple le plus propre que j'ai vu jusqu'à présent est le suivant:

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

puis l'utiliser dans le onCommit:

4
zero3nna

Je préfère utiliser la .onLongPressGesture(minimumDuration: 0), qui ne fait pas clignoter le clavier lorsqu'un autre TextView est activé (effet secondaire de .onTapGesture). Le code de masquage du clavier 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)
}
2
George Valkov

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

}
2
Michael Henry

Ma solution comment cacher le clavier logiciel lorsque les utilisateurs tapent à l'extérieur. Vous devez utiliser contentShape avec onLongPressGesture pour détecter l'intégralité du conteneur View. onTapGesture requis pour éviter de bloquer le focus sur TextField. Vous pouvez utiliser onTapGesture au lieu de onLongPressGesture mais 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: {})
    }
}
1
Victor Kushnerov

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

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

Ajoutez ensuite les 2 fonctions suivantes (Crédit accordé à: rraphael, à partir 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, il vous suffit de vous référer à la première réponse ici de rraphael pour plus de détails.

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

Enfin, vous pouvez maintenant simplement appeler ...

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

Ainsi, toute zone d'espacement fermera le clavier maintenant. Plus besoin d'une grande vue sur fond blanc!

Vous pouvez hypothétiquement appliquer cette technique de extension à tous les contrôles dont vous avez besoin pour prendre en charge TapGestures qui ne le font pas actuellement et appeler la fonction onTapGesture en combinaison avec self.endEditing() pour fermer le clavier dans toutes les situations que vous désirez.

1
Joseph Astrahan

Basé sur la réponse de @ Sajjon, voici une solution vous permettant de supprimer le clavier au toucher, appui long, glisser, agrandir et faire pivoter les gestes 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))
    }
}
0
Nicolas Mandica