Come nascondere la tastiera quando si utilizza SwiftUI?


96

Come nascondere l' keyboardutilizzo SwiftUIper i casi seguenti?

Caso 1

Ho TextFielde ho bisogno di nascondere keyboardquando l'utente fa clic sul returnpulsante.

Caso 2

Ho TextFielde ho bisogno di nascondere il keyboardmomento in cui l'utente tocca all'esterno.

Come posso farlo usando SwiftUI?

Nota:

Non ho fatto una domanda in merito UITextField. Voglio farlo usando SwifUI.TextField.


29
@DannyBuonocore Leggi di nuovo attentamente la mia domanda!
Hitesh Surani,

9
@DannyBuonocore Questo non è un duplicato della domanda menzionata. Questa domanda riguarda SwiftUI e l'altra è normale UIKit
Johnykutty

1
@DannyBuonocore per favore guarda su developer.apple.com/documentation/swiftui per trovare la differenza tra UIKit e SwiftUI. Grazie
Hitesh Surani

Ho aggiunto la mia soluzione qui, spero che ti aiuti.
Victor Kushnerov

La maggior parte delle soluzioni qui non funziona come desiderato, poiché disabilitano le reazioni desiderate su altri rubinetti di controllo. Una soluzione funzionante può essere trovata qui: forums.developer.apple.com/thread/127196
Hardy

Risposte:


86

È possibile forzare le dimissioni del primo risponditore inviando un'azione all'applicazione condivisa:

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

Ora puoi utilizzare questo metodo per chiudere la tastiera ogni volta che desideri:

struct ContentView : View {
    @State private var name: String = ""

    var body: some View {
        VStack {
            Text("Hello \(name)")
            TextField("Name...", text: self.$name) {
                // Called when the user tap the return button
                // see `onCommit` on TextField initializer.
                UIApplication.shared.endEditing()
            }
        }
    }
}

Se desideri chiudere la tastiera con un tocco, puoi creare una vista bianca a schermo intero con un'azione di tocco, che attiverà 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()
    }
}

2
.keyWindowè ora deprecato. Vedi la risposta di Lorenzo Santini .
LinusGeffarth

4
Inoltre, .tapActionè stato ribattezzato.onTapGesture
LinusGeffarth

È possibile chiudere la tastiera quando un controllo alternativo diventa attivo? stackoverflow.com/questions/58643512/…
Yarm

1
C'è un modo per farlo senza lo sfondo bianco, sto usando i distanziatori e ne ho bisogno per rilevare un gesto di tocco sul distanziatore. Anche la strategia dello sfondo bianco crea un problema sugli iPhone più recenti in cui c'è spazio extra sullo schermo sopra ora. Qualsiasi aiuto apprezzato!
Joseph Astrahan

2
Forse vale anche la pena notare che UIApplicationfa parte di UIKit, quindi è necessario import UIKit.
Alienbash

62

Dopo molti tentativi ho trovato una soluzione che (attualmente) non blocca alcun controllo, aggiungendo il riconoscimento dei gesti a UIWindow.

  1. Se desideri chiudere la tastiera solo su Tocca all'esterno (senza gestire i trascini), è sufficiente utilizzare solo UITapGestureRecognizere copiare il passaggio 3:
  2. Crea una classe di riconoscimento gestuale personalizzata che funziona con qualsiasi tocco:

    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. In SceneDelegate.swiftin func scene, aggiungere il codice seguente:

    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. Attrezzo UIGestureRecognizerDelegateper consentire tocchi simultanei.

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

Ora qualsiasi tastiera in qualsiasi vista verrà chiusa al tocco o al trascinamento all'esterno.

PS Se desideri chiudere solo campi di testo specifici, aggiungi e rimuovi il riconoscimento dei gesti nella finestra ogni volta che viene richiamato onEditingChanged


3
Questa risposta dovrebbe essere in cima. Altre risposte falliscono quando sono presenti altri controlli nella vista.
Imthath

1
@RolandLariotte risposta aggiornata per correggere questo comportamento, guarda la nuova implementazione di AnyGestureRecognizer
Mikhail

2
Ottima risposta. Funziona perfettamente. @Mikhail in realtà è interessato a sapere come rimuovere il riconoscimento dei gesti appositamente per alcuni campi di testo (ho creato un completamento automatico con i tag, quindi ogni volta che tocco un elemento nell'elenco, non voglio che questo campo di testo specifico perda il focus)
Pasta

1
questa soluzione è davvero ottima, ma dopo averla usata per circa 3 mesi, purtroppo ho trovato un bug, causato direttamente da questo tipo di hack. per favore, sii consapevole dello stesso accadere a te
glassomoss

1
Risposta fantastica! Mi chiedo come sarà implementato con iOS 14 senza scenedelegate?
Dom

27

La risposta di @ RyanTCB è buona; ecco un paio di perfezionamenti che ne semplificano l'uso ed evitano un potenziale arresto anomalo:

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 "correzione dei bug" è semplicemente che keyWindow!.endEditing(true)dovrebbe essere correttamentekeyWindow?.endEditing(true) (sì, potresti sostenere che non può accadere).

Più interessante è come puoi usarlo. Ad esempio, supponi di avere un modulo con più campi modificabili al suo interno. Basta avvolgerlo in questo modo:

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

Ora, toccando qualsiasi controllo che di per sé non presenti una tastiera eseguirà la chiusura appropriata.

(Testato con beta 7)


6
Hmmm - toccando altri controlli non si registra più. L'evento viene inghiottito.
Yarm

Non posso replicarlo: funziona ancora per me utilizzando le ultime gocce di Apple a partire dall'1 / 1. Ha funzionato e poi ha smesso di funzionare per te, o ??
Feldur

Se hai un DatePicker nel modulo, il DatePicker non verrà più mostrato
Albert

@ Albert - è vero; per utilizzare questo approccio, dovrai suddividere il punto in cui gli elementi sono decorati con DismissingKeyboard () a un livello più fine che si applica agli elementi che dovrebbero ignorare ed evita il DatePicker.
Feldur

L'utilizzo di questo codice riprodurrà l'avvisoCan't find keyplane that supports type 4 for keyboard iPhone-PortraitChoco-NumberPad; using 25686_PortraitChoco_iPhone-Simple-Pad_Default
np2314

25

L'ho sperimentato durante l'utilizzo di un TextField all'interno di un NavigationView. Questa è la mia soluzione per questo. Chiuderà la tastiera quando inizi a scorrere.

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

1
Questo porterà onDelete (scorri per eliminare) a uno strano comportamento.
Tarek Hallak

Questo è carino, ma per quanto riguarda il rubinetto?
DanielZanchi

21

Ho trovato un altro modo per chiudere la tastiera che non richiede l'accesso alla keyWindowproprietà; infatti il ​​compilatore restituisce un avviso usando

UIApplication.shared.keyWindow?.endEditing(true)

'keyWindow' è stato deprecato in iOS 13.0: non deve essere utilizzato per applicazioni che supportano più scene poiché restituisce una finestra chiave su tutte le scene connesse

Invece ho usato questo codice:

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

21

SwiftUI 2

Ecco una soluzione aggiornata per SwiftUI 2 / iOS 14 (originariamente proposta qui da Mikhail).

Non usa il AppDelegatené il SceneDelegateche mancano se usi il ciclo di vita di 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
    }
}

Ecco un esempio su come rilevare i gesti simultanei eccetto i gesti di pressione lunga:

extension UIApplication: UIGestureRecognizerDelegate {
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return !otherGestureRecognizer.isKind(of: UILongPressGestureRecognizer.self)
    }
}

2
Questo dovrebbe essere al primo posto perché tiene presente il nuovo ciclo di vita di SwiftUI.
carlosobedgomez

Funziona alla grande. Tuttavia, se tocco due volte in un campo di testo, invece di selezionare il testo la tastiera ora scompare. Qualche idea su come posso consentire il doppio tocco per la selezione?
Gary

@Gary Nell'estensione in basso puoi vedere la riga con il commento impostato su false se non vuoi rilevare il tocco durante altri gesti . Basta impostarlo su return false.
pawello2222

Impostandolo su false funziona, ma anche la tastiera non si chiude se qualcuno preme a lungo o trascina o scorre fuori dall'area di testo. C'è un modo per impostarlo su false solo per i doppi clic (preferibilmente i doppi clic all'interno del campo di testo, ma anche solo tutti i doppi clic andrebbero bene).
Gary

1
Per rispondere alla mia domanda, l'ho impostato su true, quindi ho impostato tapGesture = AnyGestureRecognizer (...) che Mikhail ha creato nella sua risposta piuttosto che tapGesture = UITapGestureRecognizer (...). Ciò consente ai doppi tocchi di selezionare il testo all'interno del campo di testo, consentendo anche a vari gesti di nascondere la tastiera al di fuori del campo di testo.
Gary

15

SwiftUI nel file "SceneDelegate.swift" aggiungi semplicemente: .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()
        }
    }

questo è sufficiente per ogni vista utilizzando la tastiera nella tua app ...


4
Questo dà un altro problema: ho un selettore nel Modulo {} accanto al campo di testo, è diventato non risponde. Non ho trovato una soluzione utilizzando tutte le risposte in questo argomento. Ma la tua risposta è utile per chiudere la tastiera con un tocco altrove, se non usi i selettori.
Nalov

Ciao. il mio codice `` var body: some View {NavigationView {Form {Section {TextField ("typesomething", text: $ c)} Section {Picker ("name", selection: $ sel) {ForEach (0 .. <200 ) {Text ("(self.array [$ 0])%")}}} `` `` La tastiera viene chiusa quando si tocca altrove, ma il selettore non risponde. Non ho trovato un modo per farlo funzionare.
Nalov

2
Salve ancora, al momento ho due soluzioni: la prima - è usare la tastiera nativa chiusa sul pulsante di ritorno, la seconda - è cambiare leggermente la gestione del tocco (aka 'костыль') - window.rootViewController = UIHostingController (rootView : contentView.onTapGesture (count: 2, perform: {window.endEditing (true)})) Spero che questo ti aiuti ...
Dim Novo

Ciao. Grazie. Il secondo modo lo ha risolto. Sto usando il tastierino numerico, quindi gli utenti possono inserire solo numeri, non ha la chiave di ritorno. Ignorare toccando era quello che cercavo.
Nalov

questo farà sì che l'elenco non possa essere navigato.
Cui Mingda

11

La mia soluzione su come nascondere la tastiera del software quando gli utenti toccano all'esterno. È necessario utilizzare contentShapecon onLongPressGestureper rilevare l'intero contenitore di visualizzazione. onTapGesturenecessario per evitare di bloccare il focus su TextField. Puoi usare onTapGestureinvece di onLongPressGesturema gli elementi NavigationBar non funzioneranno.

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

Funzionava benissimo, l'ho usato in modo leggermente diverso e dovevo essere sicuro che fosse chiamato nel thread principale.
keegan3d

7

aggiungi questo modificatore alla visualizzazione in cui desideri rilevare i tocchi degli utenti

.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

Preferisco usare il .onLongPressGesture(minimumDuration: 0), che non fa lampeggiare la tastiera quando ne TextViewviene attivata un'altra (effetto collaterale di .onTapGesture). Il codice della tastiera nascondi può essere una funzione riutilizzabile.

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

Ancora uno sfarfallio usando questo metodo.
Daniel Ryan

Funzionava benissimo, l'ho usato in modo leggermente diverso e dovevo essere sicuro che fosse chiamato nel thread principale.
keegan3d

6

Perché keyWindowè deprecato.

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

1
Il forceparametro non viene utilizzato. Dovrebbe essere{ $0.endEditing(force)}
Davide

5

Sembra che la endEditingsoluzione sia l'unica come ha sottolineato @rraphael.
L'esempio più chiaro che ho visto finora è questo:

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

e quindi utilizzarlo in onCommit:


2
.keyWindowè ora deprecato. Vedi la risposta di Lorenzo Santini .
LinusGeffarth

È ammortizzato su iOS 13+
Ahmadreza

4

Espandendo la risposta di @Feldur (che era basata su @ RyanTCB), ecco una soluzione ancora più espressiva e potente che ti consente di chiudere la tastiera su altri gesti rispetto a onTapGesture, puoi specificare quale vuoi nella chiamata di funzione.

Utilizzo

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

O usando All.gestures(solo zucchero per Gestures.allCases🍬)

.dismissKeyboard(on: All.gestures)

Codice

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

Parola di cautela

Tieni presente che se usi tutti i gesti potrebbero entrare in conflitto e non ho trovato alcuna soluzione accurata per risolverlo.


cosa significa eraseToAny()
Ravindra_Bhati

2

Questo metodo ti permette di nascondere la tastiera sui distanziatori!

Per prima cosa aggiungi questa funzione (Credito dato a: Casper Zandbergen, da SwiftUI non posso toccare 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
        }
    }
}

Quindi aggiungi le seguenti 2 funzioni (Credito dato a: rraphael, da questa domanda)

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

La funzione di seguito verrebbe aggiunta alla tua classe View, fai riferimento alla risposta principale qui di rraphael per maggiori dettagli.

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

Infine, ora puoi semplicemente chiamare ...

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

In questo modo, qualsiasi area di spaziatura chiuderà la tastiera. Non è più necessario un ampio sfondo bianco!

Si potrebbe ipoteticamente applicare questa tecnica extensiona tutti i controlli necessari per supportare TapGestures che attualmente non lo fanno e chiamare la onTapGesturefunzione in combinazione con self.endEditing()per chiudere la tastiera in qualsiasi situazione si desideri.


La mia domanda ora è come si attiva un commit su un campo di testo quando si fa andare via la tastiera in questo modo? attualmente il "commit" si attiva solo se premi il tasto Invio sulla tastiera iOS.
Joseph Astrahan,

2

Si prega di controllare https://github.com/michaelhenry/KeyboardAvoider

Includi semplicemente KeyboardAvoider {}in cima alla tua visualizzazione principale e questo è tutto.

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

}

Ciò non funziona per una visualizzazione Maschera con campi di testo. Il modulo non viene visualizzato.
Nils

2

Sulla base della risposta di @ Sajjon, ecco una soluzione che ti consente di ignorare i gesti di tocco, pressione prolungata, trascinamento, ingrandimento e rotazione in base alla tua scelta.

Questa soluzione funziona in XCode 11.4

Utilizzo per ottenere il comportamento richiesto da @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)
    }
}

Codice

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

Puoi evitare completamente l'interazione con UIKit e implementarla in SwiftUI puro . Basta aggiungere un .id(<your id>)modificatore al tuo fileTextField e cambiare il suo valore ogni volta che vuoi chiudere la tastiera (durante lo scorrimento, il tocco della vista, l'azione del pulsante, ..).

Implementazione di esempio:

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

Nota che l'ho testato solo nell'ultima beta di Xcode 12, ma dovrebbe funzionare con le versioni precedenti (anche Xcode 11) senza alcun problema.


1

ReturnChiave della tastiera

Oltre a tutte le risposte sul tocco al di fuori del campo text, potresti voler chiudere la tastiera quando l'utente tocca il tasto Invio sulla tastiera:

definire questa funzione globale:

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

E aggiungi l'uso onCommitnell'argomento:

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

Benefici

  • Puoi chiamarlo da qualsiasi luogo
  • Non dipende da UIKit o SwiftUI (può essere utilizzato nelle app per mac)
  • Funziona anche su iOS 13

Demo

demo


0

Finora le opzioni sopra non hanno funzionato per me, perché ho Form e all'interno di pulsanti, collegamenti, selettore ...

Creo sotto il codice che funziona, con l'aiuto degli esempi sopra.

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

Utilizzo:

 var body: some View {
        NavigationView {
            Form {
                picker
                button
                textfield
                text
            }
            .dismissingKeyboard()

-2

SwiftUI rilasciato a giugno / 2020 con Xcode 12 e iOS 14 aggiunge il modificatore hideKeyboardOnTap (). Questo dovrebbe risolvere il tuo caso numero 2. La soluzione per il tuo caso numero 1 viene fornita gratuitamente con Xcode 12 e iOS 14: la tastiera predefinita per TextField si nasconde automaticamente quando viene premuto il pulsante Invio.


1
Non c'è il modificatore hideKeyboardOnTap in iOS14
Teo Sartori

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.