Verschieben Sie TextField nach oben, wenn die Tastatur in SwiftUI angezeigt wurde


100

Ich habe sieben TextFieldin meiner Hauptleitung ContentView. Wenn der Benutzer die Tastatur öffnet, werden einige davon TextFieldunter dem Tastaturrahmen ausgeblendet. Ich möchte also alle nach TextFieldoben bewegen , wenn die Tastatur erschienen ist.

Ich habe den folgenden Code verwendet, um TextFieldauf dem Bildschirm hinzuzufügen .

struct ContentView : View {
    @State var textfieldText: String = ""

    var body: some View {
            VStack {
                TextField($textfieldText, placeholder: Text("TextField1"))
                TextField($textfieldText, placeholder: Text("TextField2"))
                TextField($textfieldText, placeholder: Text("TextField3"))
                TextField($textfieldText, placeholder: Text("TextField4"))
                TextField($textfieldText, placeholder: Text("TextField5"))
                TextField($textfieldText, placeholder: Text("TextField6"))
                TextField($textfieldText, placeholder: Text("TextField6"))
                TextField($textfieldText, placeholder: Text("TextField7"))
            }
    }
}

Ausgabe:

Ausgabe



1
@ PrashantTukadiya Danke für die schnelle Antwort. Ich habe TextField in Scrollview hinzugefügt, habe aber immer noch das gleiche Problem.
Hitesh Surani

1
@ DimaPaliychuk Das wird nicht funktionieren. es ist SwiftUI
Prashant Tukadiya

20
Das Anzeigen der Tastatur und das Verdecken von Inhalten auf dem Bildschirm gibt es seit was, der ersten Objective C iPhone App? Dieses Problem wird ständig gelöst. Ich bin enttäuscht, dass Apple dies nicht mit SwiftUi angesprochen hat. Ich weiß, dass dieser Kommentar für niemanden hilfreich ist, aber ich wollte dieses Problem ansprechen, dass wir wirklich Druck auf Apple ausüben sollten, um eine Lösung bereitzustellen, und uns nicht darauf verlassen sollten, dass die Community immer die häufigsten Probleme liefert.
P. Ent

1
Es gibt einen sehr guten Artikel von Vadim vadimbulavin.com/…
Sudara

Antworten:


64

Code aktualisiert für den Xcode, Beta 7.

Sie benötigen keine Auffüllung, ScrollViews oder Listen, um dies zu erreichen. Obwohl diese Lösung auch mit ihnen gut spielen wird. Ich füge hier zwei Beispiele hinzu.

Der erste verschiebt den gesamten TextField nach oben, wenn die Tastatur für einen von ihnen angezeigt wird. Aber nur wenn nötig. Wenn die Tastatur die Textfelder nicht ausblendet, werden sie nicht verschoben.

Im zweiten Beispiel bewegt sich die Ansicht nur so weit, dass das aktive Textfeld nicht ausgeblendet wird.

Beide Beispiele verwenden denselben allgemeinen Code wie am Ende: GeometryGetter und KeyboardGuardian

Erstes Beispiel (alle Textfelder anzeigen)

Wenn die Tastatur geöffnet wird, werden die 3 Textfelder so weit nach oben verschoben, dass alle sichtbar bleiben

struct ContentView: View {
    @ObservedObject private var kGuardian = KeyboardGuardian(textFieldCount: 1)
    @State private var name = Array<String>.init(repeating: "", count: 3)

    var body: some View {

        VStack {
            Group {
                Text("Some filler text").font(.largeTitle)
                Text("Some filler text").font(.largeTitle)
            }

            TextField("enter text #1", text: $name[0])
                .textFieldStyle(RoundedBorderTextFieldStyle())

            TextField("enter text #2", text: $name[1])
                .textFieldStyle(RoundedBorderTextFieldStyle())

            TextField("enter text #3", text: $name[2])
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .background(GeometryGetter(rect: $kGuardian.rects[0]))

        }.offset(y: kGuardian.slide).animation(.easeInOut(duration: 1.0))
    }

}

Zweites Beispiel (nur das aktive Feld anzeigen)

Wenn auf jedes Textfeld geklickt wird, wird die Ansicht nur so weit nach oben verschoben, dass das angeklickte Textfeld sichtbar wird.

struct ContentView: View {
    @ObservedObject private var kGuardian = KeyboardGuardian(textFieldCount: 3)
    @State private var name = Array<String>.init(repeating: "", count: 3)

    var body: some View {

        VStack {
            Group {
                Text("Some filler text").font(.largeTitle)
                Text("Some filler text").font(.largeTitle)
            }

            TextField("text #1", text: $name[0], onEditingChanged: { if $0 { self.kGuardian.showField = 0 } })
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .background(GeometryGetter(rect: $kGuardian.rects[0]))

            TextField("text #2", text: $name[1], onEditingChanged: { if $0 { self.kGuardian.showField = 1 } })
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .background(GeometryGetter(rect: $kGuardian.rects[1]))

            TextField("text #3", text: $name[2], onEditingChanged: { if $0 { self.kGuardian.showField = 2 } })
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .background(GeometryGetter(rect: $kGuardian.rects[2]))

            }.offset(y: kGuardian.slide).animation(.easeInOut(duration: 1.0))
    }.onAppear { self.kGuardian.addObserver() } 
.onDisappear { self.kGuardian.removeObserver() }

}

GeometryGetter

Dies ist eine Ansicht, die die Größe und Position der übergeordneten Ansicht absorbiert. Um dies zu erreichen, wird es im Modifikator .background aufgerufen. Dies ist ein sehr leistungsfähiger Modifikator, nicht nur eine Möglichkeit, den Hintergrund einer Ansicht zu dekorieren. Wenn Sie eine Ansicht an .background (MyView ()) übergeben, erhält MyView die geänderte Ansicht als übergeordnetes Element. Die Verwendung von GeometryReader ermöglicht es der Ansicht, die Geometrie des übergeordneten Elements zu kennen.

Beispiel: Text("hello").background(GeometryGetter(rect: $bounds))Füllt variable Grenzen mit der Größe und Position der Textansicht und verwendet den globalen Koordinatenraum.

struct GeometryGetter: View {
    @Binding var rect: CGRect

    var body: some View {
        GeometryReader { geometry in
            Group { () -> AnyView in
                DispatchQueue.main.async {
                    self.rect = geometry.frame(in: .global)
                }

                return AnyView(Color.clear)
            }
        }
    }
}

Update Ich habe DispatchQueue.main.async hinzugefügt, um die Möglichkeit zu vermeiden, den Status der Ansicht während des Renderns zu ändern. ***

KeyboardGuardian

Der Zweck von KeyboardGuardian besteht darin, die Ereignisse zum Ein- und Ausblenden der Tastatur zu verfolgen und zu berechnen, wie viel Platz die Ansicht verschoben werden muss.

Update: Ich habe KeyboardGuardian geändert, um die Folie zu aktualisieren, wenn der Benutzer von einem Feld in ein anderes wechselt

import SwiftUI
import Combine

final class KeyboardGuardian: ObservableObject {
    public var rects: Array<CGRect>
    public var keyboardRect: CGRect = CGRect()

    // keyboardWillShow notification may be posted repeatedly,
    // this flag makes sure we only act once per keyboard appearance
    public var keyboardIsHidden = true

    @Published var slide: CGFloat = 0

    var showField: Int = 0 {
        didSet {
            updateSlide()
        }
    }

    init(textFieldCount: Int) {
        self.rects = Array<CGRect>(repeating: CGRect(), count: textFieldCount)

    }

    func addObserver() {
NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyBoardDidHide(notification:)), name: UIResponder.keyboardDidHideNotification, object: nil)
}

func removeObserver() {
 NotificationCenter.default.removeObserver(self)
}

    deinit {
        NotificationCenter.default.removeObserver(self)
    }



    @objc func keyBoardWillShow(notification: Notification) {
        if keyboardIsHidden {
            keyboardIsHidden = false
            if let rect = notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect {
                keyboardRect = rect
                updateSlide()
            }
        }
    }

    @objc func keyBoardDidHide(notification: Notification) {
        keyboardIsHidden = true
        updateSlide()
    }

    func updateSlide() {
        if keyboardIsHidden {
            slide = 0
        } else {
            let tfRect = self.rects[self.showField]
            let diff = keyboardRect.minY - tfRect.maxY

            if diff > 0 {
                slide += diff
            } else {
                slide += min(diff, 0)
            }

        }
    }
}

1
Ist es möglich, GeometryGetterals Ansichtsmodifikator als Hintergrund anzuhängen, indem es ViewModifierprotokollkonform gemacht wird?
Sudara

2
Es ist möglich, aber was ist der Gewinn? Sie würden es so anhängen: .modifier(GeometryGetter(rect: $kGuardian.rects[1]))statt .background(GeometryGetter(rect: $kGuardian.rects[1])). Kein großer Unterschied (nur 2 Zeichen weniger).
Kontiki

3
In einigen Situationen kann es vorkommen, dass Sie beim Zuweisen des neuen Rechtecks ​​aus dem Programm im GeometryGetter ein SIGNAL ABORT erhalten, wenn Sie von diesem Bildschirm weg navigieren. In diesem Fall fügen Sie einfach Code hinzu, um zu überprüfen, ob die Größe der Geometrie größer als Null ist (Geometrie.Größe.Breite> 0 && Geometrie.Größe.Höhe> 0), bevor Sie self.rect einen Wert zuweisen
Julio Bailon

1
@JulioBailon Ich weiß nicht , warum , aber bewegend geometry.framevon aus DispatchQueue.main.asyncmit SIGNAL ABORT half, wird nun Ihre Lösung testen. Update: if geometry.size.width > 0 && geometry.size.height > 0vor dem Zuweisen self.rectgeholfen.
Roman Vasilyev

2
Dies bricht auch für mich ab, wenn self.rect = geometr.frame (in: .global) SIGNAL ABORT erhält und alle vorgeschlagenen Lösungen versucht, um diesen Fehler zu
beheben

55

Um auf der Lösung von @rraphael aufzubauen, habe ich sie so konvertiert, dass sie von der heutigen xcode11 swiftUI-Unterstützung verwendet werden kann.

import SwiftUI

final class KeyboardResponder: ObservableObject {
    private var notificationCenter: NotificationCenter
    @Published private(set) var currentHeight: CGFloat = 0

    init(center: NotificationCenter = .default) {
        notificationCenter = center
        notificationCenter.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        notificationCenter.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    deinit {
        notificationCenter.removeObserver(self)
    }

    @objc func keyBoardWillShow(notification: Notification) {
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
            currentHeight = keyboardSize.height
        }
    }

    @objc func keyBoardWillHide(notification: Notification) {
        currentHeight = 0
    }
}

Verwendung:

struct ContentView: View {
    @ObservedObject private var keyboard = KeyboardResponder()
    @State private var textFieldInput: String = ""

    var body: some View {
        VStack {
            HStack {
                TextField("uMessage", text: $textFieldInput)
            }
        }.padding()
        .padding(.bottom, keyboard.currentHeight)
        .edgesIgnoringSafeArea(.bottom)
        .animation(.easeOut(duration: 0.16))
    }
}

Das veröffentlichte currentHeightwird ein erneutes Rendern der Benutzeroberfläche auslösen und Ihr TextField nach oben verschieben, wenn die Tastatur angezeigt wird, und nach unten, wenn es geschlossen wird. Ich habe jedoch kein ScrollView verwendet.


6
Ich mag diese Antwort wegen ihrer Einfachheit. Ich habe hinzugefügt .animation(.easeOut(duration: 0.16)), um zu versuchen, die Geschwindigkeit der Tastatur anzupassen, die nach oben rutscht.
Mark Moeykens

Warum haben Sie eine maximale Höhe von 340 für die Tastatur festgelegt?
Daniel Ryan

1
@DanielRyan Manchmal gab die Tastaturhöhe falsche Werte im Simulator zurück. Ich kann nicht scheinen, einen Weg zu finden, um das Problem derzeit
Michael Neas

1
Ich habe dieses Problem selbst nicht gesehen. Vielleicht ist es in den neuesten Versionen behoben. Ich wollte die Größe nicht sperren, falls es größere Tastaturen gibt (oder geben wird).
Daniel Ryan

1
Sie könnten es versuchen keyboardFrameEndUserInfoKey. Das sollte den letzten Frame für die Tastatur enthalten.
Mathias Claassen

50

Ich habe viele der vorgeschlagenen Lösungen ausprobiert, und obwohl sie in den meisten Fällen funktionieren, hatte ich einige Probleme - hauptsächlich mit dem sicheren Bereich (ich habe ein Formular auf der Registerkarte von TabView).

Am Ende habe ich einige verschiedene Lösungen kombiniert und GeometryReader verwendet, um den unteren Bereich des sicheren Bereichs einer bestimmten Ansicht zu erhalten und ihn für die Berechnung des Polsters zu verwenden:

import SwiftUI
import Combine

struct AdaptsToKeyboard: ViewModifier {
    @State var currentHeight: CGFloat = 0

    func body(content: Content) -> some View {
        GeometryReader { geometry in
            content
                .padding(.bottom, self.currentHeight)
                .animation(.easeOut(duration: 0.16))
                .onAppear(perform: {
                    NotificationCenter.Publisher(center: NotificationCenter.default, name: UIResponder.keyboardWillShowNotification)
                        .merge(with: NotificationCenter.Publisher(center: NotificationCenter.default, name: UIResponder.keyboardWillChangeFrameNotification))
                        .compactMap { notification in
                            notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect
                    }
                    .map { rect in
                        rect.height - geometry.safeAreaInsets.bottom
                    }
                    .subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight))

                    NotificationCenter.Publisher(center: NotificationCenter.default, name: UIResponder.keyboardWillHideNotification)
                        .compactMap { notification in
                            CGFloat.zero
                    }
                    .subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight))
                })
        }
    }
}

Verwendung:

struct MyView: View {
    var body: some View {
        Form {...}
        .modifier(AdaptsToKeyboard())
    }
}

7
Wow, dies ist die schnellste Version von allen mit GeometryReader und ViewModifier. Liebe es.
Mateusz

3
Das ist so nützlich und elegant. Vielen Dank, dass Sie dies geschrieben haben.
Danilo Campos

2
Ich sehe eine kleine leere weiße Ansicht über meiner Tastatur. Diese Ansicht ist GeometryReader View, die ich durch Ändern der Hintergrundfarbe bestätigt habe. Irgendeine Idee, warum GeometryReader zwischen meiner tatsächlichen Ansicht und der Tastatur angezeigt wird.
user832

6
Ich erhalte den Fehler Thread 1: signal SIGABRTonline, rect.height - geometry.safeAreaInsets.bottomwenn ich ein zweites Mal mit der Tastatur zur Ansicht gehe und auf die Schaltfläche klicke TextField. Es spielt keine Rolle, ob ich TextFielddas erste Mal klicke oder nicht. Die App stürzt immer noch ab.
JLively

2
Endlich etwas, das funktioniert! Warum Apple das für uns nicht kann, ist verrückt !!
Dave Kozikowski

35

Ich habe eine Ansicht erstellt, die jede andere Ansicht umschließen kann, um sie zu verkleinern, wenn die Tastatur angezeigt wird.

Es ist ziemlich einfach. Wir erstellen Publisher für das Ein- und Ausblenden von Tastaturereignissen und abonnieren sie dann mit onReceive. Wir verwenden das Ergebnis davon, um ein rechteckiges Tastaturrechteck hinter der Tastatur zu erstellen.

struct KeyboardHost<Content: View>: View {
    let view: Content

    @State private var keyboardHeight: CGFloat = 0

    private let showPublisher = NotificationCenter.Publisher.init(
        center: .default,
        name: UIResponder.keyboardWillShowNotification
    ).map { (notification) -> CGFloat in
        if let rect = notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect {
            return rect.size.height
        } else {
            return 0
        }
    }

    private let hidePublisher = NotificationCenter.Publisher.init(
        center: .default,
        name: UIResponder.keyboardWillHideNotification
    ).map {_ -> CGFloat in 0}

    // Like HStack or VStack, the only parameter is the view that this view should layout.
    // (It takes one view rather than the multiple views that Stacks can take)
    init(@ViewBuilder content: () -> Content) {
        view = content()
    }

    var body: some View {
        VStack {
            view
            Rectangle()
                .frame(height: keyboardHeight)
                .animation(.default)
                .foregroundColor(.clear)
        }.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in
            self.keyboardHeight = height
        }
    }
}

Sie können die Ansicht dann folgendermaßen verwenden:

var body: some View {
    KeyboardHost {
        viewIncludingKeyboard()
    }
}

Um den Inhalt der Ansicht nach oben zu verschieben, anstatt ihn zu verkleinern, können Sie ihn auffüllen oder versetzen, viewanstatt ihn in einen VStack mit einem Rechteck zu legen.


6
Ich denke das ist die richtige Antwort. Nur eine kleine Änderung, die ich vorgenommen habe: Anstelle eines Rechtecks ​​ändere ich nur die Polsterung von self.viewund es funktioniert großartig. Keine Probleme mit der Animation
Tae

5
Vielen Dank! Funktioniert perfekt. Wie @Taed sagte, ist es besser, einen Polsterungsansatz zu verwenden. Das Endergebnis wärevar body: some View { VStack { view .padding(.bottom, keyboardHeight) .animation(.default) } .onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = height } }
fdelafuente

1
Trotz geringerer Stimmen ist dies die schnellste Antwort. Und der vorherige Ansatz mit AnyView unterbricht die Hilfe bei der Metallbeschleunigung.
Nelson Cardaci

4
Es ist eine großartige Lösung, aber das Hauptproblem hierbei ist, dass Sie die Möglichkeit verlieren, die Ansicht nur dann nach oben zu verschieben, wenn die Tastatur das von Ihnen bearbeitete Textfeld verbirgt. Ich meine: Wenn Sie ein Formular mit mehreren Textfeldern haben und das erste oben bearbeiten, möchten Sie wahrscheinlich nicht, dass es nach oben verschoben wird, da es nicht mehr auf dem Bildschirm angezeigt wird.
Superpuccio

Ich mag die Antwort wirklich, aber wie alle anderen Antworten funktioniert sie nicht, wenn sich Ihre Ansicht in einer TabBar befindet oder die Ansicht nicht bündig mit dem unteren Bildschirmrand abschließt.
Ben Patch

25

Ich habe einen wirklich einfach zu verwendenden Ansichtsmodifikator erstellt.

Fügen Sie eine Swift-Datei mit dem folgenden Code hinzu und fügen Sie diesen Modifikator einfach Ihren Ansichten hinzu:

.keyboardResponsive()
import SwiftUI

struct KeyboardResponsiveModifier: ViewModifier {
  @State private var offset: CGFloat = 0

  func body(content: Content) -> some View {
    content
      .padding(.bottom, offset)
      .onAppear {
        NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { notif in
          let value = notif.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
          let height = value.height
          let bottomInset = UIApplication.shared.windows.first?.safeAreaInsets.bottom
          self.offset = height - (bottomInset ?? 0)
        }

        NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { notif in
          self.offset = 0
        }
    }
  }
}

extension View {
  func keyboardResponsive() -> ModifiedContent<Self, KeyboardResponsiveModifier> {
    return modifier(KeyboardResponsiveModifier())
  }
}


Nett. Danke für das Teilen.
Jahrzehnte

2
Wäre cool, wenn es bei Bedarf nur versetzt wäre (dh nicht scrollen, wenn die Tastatur das Eingabeelement nicht abdeckt). Schön zu haben ...
Jahrzehnte

Das funktioniert super, danke. Sehr saubere Implementierung, und für mich wird nur bei Bedarf gescrollt.
Joshua

Genial! Warum bieten Sie dies nicht auf Github oder anderswo an? :) Oder Sie können dies github.com/hackiftekhar/IQKeyboardManager vorschlagen, da sie noch keine vollständige SwiftUI-Unterstützung haben
Schnodderbalken

Spielt nicht gut mit Orientierungsänderungen und wird versetzt, unabhängig davon, ob es benötigt wird oder nicht.
GrandSteph

16

Oder Sie können einfach IQKeyBoardManagerSwift verwenden

Sie können dies optional Ihrem App-Delegaten hinzufügen, um die Symbolleiste auszublenden und das Ausblenden der Tastatur zu aktivieren, wenn Sie auf eine andere Ansicht als die Tastatur klicken.

        IQKeyboardManager.shared.enableAutoToolbar = false
        IQKeyboardManager.shared.shouldShowToolbarPlaceholder = false
        IQKeyboardManager.shared.shouldResignOnTouchOutside = true
        IQKeyboardManager.shared.previousNextDisplayMode = .alwaysHide

Dies ist in der Tat auch für mich der (unerwartete) Weg. Solide.
HelloTimo

Dieser Rahmen funktionierte noch besser als erwartet. Ich danke Ihnen für das Teilen!
Richard Poutier

1
Ich arbeite gut für SwiftUI - danke @DominatorVbN - Ich musste im iPad-Querformat IQKeyboardManager.shared.keyboardDistanceFromTextFieldauf 40 erhöhen , um eine angenehme Lücke zu erhalten.
Richard Groves

Musste auch einstellen IQKeyboardManager.shared.enable = true, um zu verhindern, dass die Tastatur meine Textfelder verbirgt. In jedem Fall ist dies die beste Lösung. Ich habe 4 Felder vertikal angeordnet und die anderen Lösungen würden für mein unterstes Feld funktionieren, aber das oberste Feld aus dem Blickfeld verschieben.
MisterEd

12

Sie müssen ein hinzufügen ScrollViewund einen unteren Abstand von der Größe der Tastatur festlegen, damit der Inhalt scrollen kann, wenn die Tastatur angezeigt wird.

Um die Tastaturgröße zu erhalten, müssen Sie das verwenden, NotificationCenterum sich für das Tastaturereignis zu registrieren. Sie können dazu eine benutzerdefinierte Klasse verwenden:

import SwiftUI
import Combine

final class KeyboardResponder: BindableObject {
    let didChange = PassthroughSubject<CGFloat, Never>()

    private var _center: NotificationCenter
    private(set) var currentHeight: CGFloat = 0 {
        didSet {
            didChange.send(currentHeight)
        }
    }

    init(center: NotificationCenter = .default) {
        _center = center
        _center.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        _center.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    deinit {
        _center.removeObserver(self)
    }

    @objc func keyBoardWillShow(notification: Notification) {
        print("keyboard will show")
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
            currentHeight = keyboardSize.height
        }
    }

    @objc func keyBoardWillHide(notification: Notification) {
        print("keyboard will hide")
        currentHeight = 0
    }
}

Mit der BindableObjectKonformität können Sie diese Klasse als verwenden Stateund die Ansichtsaktualisierung auslösen. Schauen Sie sich bei Bedarf das Tutorial an für BindableObject: SwiftUI-Tutorial

Wenn Sie das bekommen, müssen Sie a konfigurieren ScrollView, um seine Größe zu reduzieren, wenn die Tastatur angezeigt wird. Der Einfachheit halber habe ich dies ScrollViewin eine Art Komponente eingewickelt :

struct KeyboardScrollView<Content: View>: View {
    @State var keyboard = KeyboardResponder()
    private var content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        ScrollView {
            VStack {
                content
            }
        }
        .padding(.bottom, keyboard.currentHeight)
    }
}

Jetzt müssen Sie nur noch Ihre Inhalte in die benutzerdefinierte Datei einbetten ScrollView.

struct ContentView : View {
    @State var textfieldText: String = ""

    var body: some View {
        KeyboardScrollView {
            ForEach(0...10) { index in
                TextField(self.$textfieldText, placeholder: Text("TextField\(index)")) {
                    // Hide keyboard when uses tap return button on keyboard.
                    self.endEditing(true)
                }
            }
        }
    }

    private func endEditing(_ force: Bool) {
        UIApplication.shared.keyWindow?.endEditing(true)
    }
}

Bearbeiten: Das Bildlaufverhalten ist wirklich komisch, wenn sich die Tastatur versteckt. Möglicherweise würde dies durch die Verwendung einer Animation zum Aktualisieren des Auffüllens behoben, oder Sie sollten etwas anderes als das verwenden padding, um die Größe der Bildlaufansicht anzupassen.


Hey, es scheint, du hast Erfahrung mit bindbaren Objekten. Ich kann es nicht zum Laufen bringen, wie ich will. Es wäre schön, wenn Sie einen Blick darauf werfen
SwiftiSwift

Warum verwenden Sie nicht @ObjectBinding
SwiftiSwift

3
Mit BindableObjectveraltet funktioniert das leider nicht mehr.
LinusGeffarth

2
@LinusGeffarth Für das, was es wert ist, BindableObjectwurde nur umbenannt in ObservableObjectund didChangeum objectWillChange. Das Objekt aktualisiert die Ansicht ganz gut (obwohl ich mit @ObservedObjectstatt getestet habe @State)
SeizeTheDay

Hallo, diese Lösung scrollt den Inhalt, zeigt jedoch einen weißen Bereich über der Tastatur, der die Hälfte des Textfelds verbirgt. Bitte lassen Sie mich wissen, wie wir den weißen Bereich entfernen können.
Shahbaz Sajjad

12

Xcode 12 - Einzeiliger Code

Fügen Sie diesen Modifikator dem hinzu TextField

.ignoresSafeArea(.keyboard, edges: .bottom)

Demo

Apple hinzugefügt , um die Tastatur als eine Region für den sicheren Bereich, so dass Sie es verwenden können , bewegen jedeView mit der Tastatur wie andere Regionen.


es funktioniert für TextField, aber was ist mit TextEditor?
Первушин

Es funktioniert auf Any View , einschließlich der TextEditor.
Mojtaba Hosseini

3
gerade getestet, Xcode Beta 6, TextEditor funktioniert nicht
28ндрей Первушин

1
Sie können eine Frage stellen und hier verlinken. So kann ich mir Ihren reproduzierbaren Code ansehen und sehen, ob ich helfen kann :) @kyrers
Mojtaba Hosseini

2
Ich habe es selbst herausgefunden. In .ignoresSafeArea(.keyboard)auf Ihre Ansicht.
leonboe1

6

Ich habe die vorhandenen Lösungen überprüft und in ein praktisches SPM-Paket umgewandelt, das einen .keyboardAware()Modifikator enthält:

KeyboardAwareSwiftUI

Beispiel:

struct KeyboardAwareView: View {
    @State var text = "example"

    var body: some View {
        NavigationView {
            ScrollView {
                VStack(alignment: .leading) {
                    ForEach(0 ..< 20) { i in
                        Text("Text \(i):")
                        TextField("Text", text: self.$text)
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                            .padding(.bottom, 10)
                    }
                }
                .padding()
            }
            .keyboardAware()  // <--- the view modifier
            .navigationBarTitle("Keyboard Example")
        }

    }
}

Quelle:

import UIKit
import SwiftUI

public class KeyboardInfo: ObservableObject {

    public static var shared = KeyboardInfo()

    @Published public var height: CGFloat = 0

    private init() {
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), name: UIApplication.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), name: UIResponder.keyboardWillHideNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
    }

    @objc func keyboardChanged(notification: Notification) {
        if notification.name == UIApplication.keyboardWillHideNotification {
            self.height = 0
        } else {
            self.height = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
        }
    }

}

struct KeyboardAware: ViewModifier {
    @ObservedObject private var keyboard = KeyboardInfo.shared

    func body(content: Content) -> some View {
        content
            .padding(.bottom, self.keyboard.height)
            .edgesIgnoringSafeArea(self.keyboard.height > 0 ? .bottom : [])
            .animation(.easeOut)
    }
}

extension View {
    public func keyboardAware() -> some View {
        ModifiedContent(content: self, modifier: KeyboardAware())
    }
}

Ich sehe nur die Hälfte der Höhe der Textansicht. Weißt du, wie man das löst?
Matrosov Alexander

Gut. Das hat mir Zeit gespart. Bevor wir dies verwenden, kennen wir die untere Polsterung dieses Modifikator-Handles.
Brownsoo Han

5

Ich habe die Antwort von Benjamin Kindle als Ausgangspunkt verwendet, aber ich hatte einige Probleme, die ich ansprechen wollte.

  1. Die meisten Antworten beziehen sich nicht darauf, dass die Tastatur ihren Rahmen ändert. Sie brechen daher, wenn der Benutzer das Gerät mit der Tastatur auf dem Bildschirm dreht. Durch Hinzufügen keyboardWillChangeFrameNotificationzur Liste der verarbeiteten Benachrichtigungen wird dies behoben.
  2. Ich wollte nicht mehrere Publisher mit ähnlichen, aber unterschiedlichen Kartenschließungen, also habe ich alle drei Tastaturbenachrichtigungen zu einem einzigen Publisher verkettet. Es ist zwar eine lange Kette, aber jeder Schritt ist ziemlich einfach.
  3. Ich habe die initFunktion bereitgestellt, die a akzeptiert, @ViewBuildersodass Sie die KeyboardHostAnsicht wie jede andere Ansicht verwenden und Ihren Inhalt einfach in einem nachfolgenden Abschluss übergeben können, anstatt die Inhaltsansicht als Parameter an zu übergeben init.
  4. Wie Tae und fdelafuente in Kommentaren vorgeschlagen haben, habe ich das ausgetauscht, Rectangleum die untere Polsterung anzupassen.
  5. Anstatt die fest codierte Zeichenfolge "UIKeyboardFrameEndUserInfoKey" zu verwenden, wollte ich die in UIWindowas bereitgestellten Zeichenfolgen verwenden UIWindow.keyboardFrameEndUserInfoKey.

Ich habe das alles zusammen:

struct KeyboardHost<Content>: View  where Content: View {
    var content: Content

    /// The current height of the keyboard rect.
    @State private var keyboardHeight = CGFloat(0)

    /// A publisher that combines all of the relevant keyboard changing notifications and maps them into a `CGFloat` representing the new height of the
    /// keyboard rect.
    private let keyboardChangePublisher = NotificationCenter.Publisher(center: .default,
                                                                       name: UIResponder.keyboardWillShowNotification)
        .merge(with: NotificationCenter.Publisher(center: .default,
                                                  name: UIResponder.keyboardWillChangeFrameNotification))
        .merge(with: NotificationCenter.Publisher(center: .default,
                                                  name: UIResponder.keyboardWillHideNotification)
            // But we don't want to pass the keyboard rect from keyboardWillHide, so strip the userInfo out before
            // passing the notification on.
            .map { Notification(name: $0.name, object: $0.object, userInfo: nil) })
        // Now map the merged notification stream into a height value.
        .map { ($0.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero).size.height }
        // If you want to debug the notifications, swap this in for the final map call above.
//        .map { (note) -> CGFloat in
//            let height = (note.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero).size.height
//
//            print("Received \(note.name.rawValue) with height \(height)")
//            return height
//    }

    var body: some View {
        content
            .onReceive(keyboardChangePublisher) { self.keyboardHeight = $0 }
            .padding(.bottom, keyboardHeight)
            .animation(.default)
    }

    init(@ViewBuilder _ content: @escaping () -> Content) {
        self.content = content()
    }
}

struct KeyboardHost_Previews: PreviewProvider {
    static var previews: some View {
        KeyboardHost {
            TextField("TextField", text: .constant("Preview text field"))
        }
    }
}


Diese Lösung funktioniert nicht, sie erhöht die KeyboardHöhe
GSerjo

Können Sie die Probleme, die Sie bei @GSerjo sehen, näher erläutern? Ich verwende diesen Code in meiner App und er funktioniert gut für mich.
Timothy Sanders

Könnten Sie bitte Pridictivein iOS einschalten keyboard. Settings-> General-> Keyboard-> Pridictive. In diesem Fall wird das Calclate nicht korrigiert und die Tastatur wird
aufgefüllt

@GSerjo: Ich habe Predictive Text auf einem iPad Touch (7. Generation) mit iOS 13.1 Beta aktiviert. Es wird korrekt eine Auffüllung für die Höhe der Vorhersagezeile hinzugefügt. (Es ist wichtig zu beachten, dass ich hier nicht die Höhe der Tastatur anpasse, sondern die Auffüllung der Ansicht selbst vergrößere.) Versuchen Sie, die auskommentierte Debugging-Karte auszutauschen, und spielen Sie mit den Werten, die Sie für die Vorhersage erhalten Tastatur. Ich werde ein Protokoll in einem anderen Kommentar posten.
Timothy Sanders

Wenn die "Debugging" -Karte nicht kommentiert ist, können Sie den zugewiesenen Wert sehen keyboardHeight. Auf meinem iPod Touch (im Hochformat) beträgt eine Tastatur mit eingeschalteter Anzeige 254 Punkte. Ohne es sind 216 Punkte. Ich kann Predictive sogar mit einer Tastatur auf dem Bildschirm ausschalten und die Polsterung wird ordnungsgemäß aktualisiert. Hinzufügen einer Tastatur mit Predictive: Received UIKeyboardWillChangeFrameNotification with height 254.0 Received UIKeyboardWillShowNotification with height 254.0 Wenn ich Predictive Text ausschalte:Received UIKeyboardWillChangeFrameNotification with height 216.0
Timothy Sanders

4

Dies ist angepasst an das, was @kontiki erstellt hat. Ich habe es in einer App unter Beta 8 / GM Seed ausgeführt, in der das gescrollte Feld Teil eines Formulars in einer Navigationsansicht ist. Hier ist KeyboardGuardian:

//
//  KeyboardGuardian.swift
//
//  /programming/56491881/move-textfield-up-when-thekeyboard-has-appeared-by-using-swiftui-ios
//

import SwiftUI
import Combine

/// The purpose of KeyboardGuardian, is to keep track of keyboard show/hide events and
/// calculate how much space the view needs to be shifted.
final class KeyboardGuardian: ObservableObject {
    let objectWillChange = ObservableObjectPublisher() // PassthroughSubject<Void, Never>()

    public var rects: Array<CGRect>
    public var keyboardRect: CGRect = CGRect()

    // keyboardWillShow notification may be posted repeatedly,
    // this flag makes sure we only act once per keyboard appearance
    private var keyboardIsHidden = true

    var slide: CGFloat = 0 {
        didSet {
            objectWillChange.send()
        }
    }

    public var showField: Int = 0 {
        didSet {
            updateSlide()
        }
    }

    init(textFieldCount: Int) {
        self.rects = Array<CGRect>(repeating: CGRect(), count: textFieldCount)

        NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyBoardDidHide(notification:)), name: UIResponder.keyboardDidHideNotification, object: nil)

    }

    @objc func keyBoardWillShow(notification: Notification) {
        if keyboardIsHidden {
            keyboardIsHidden = false
            if let rect = notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect {
                keyboardRect = rect
                updateSlide()
            }
        }
    }

    @objc func keyBoardDidHide(notification: Notification) {
        keyboardIsHidden = true
        updateSlide()
    }

    func updateSlide() {
        if keyboardIsHidden {
            slide = 0
        } else {
            slide = -keyboardRect.size.height
        }
    }
}

Dann habe ich eine Aufzählung verwendet, um die Slots im rects-Array und die Gesamtzahl zu verfolgen:

enum KeyboardSlots: Int {
    case kLogPath
    case kLogThreshold
    case kDisplayClip
    case kPingInterval
    case count
}

KeyboardSlots.count.rawValueist die notwendige Array-Kapazität; Die anderen als rawValue geben den entsprechenden Index an, den Sie für .background-Aufrufe (GeometryGetter) verwenden.

Mit dieser Einstellung erhalten Ansichten beim KeyboardGuardian Folgendes:

@ObservedObject private var kGuardian = KeyboardGuardian(textFieldCount: SettingsFormBody.KeyboardSlots.count.rawValue)

Die eigentliche Bewegung ist wie folgt:

.offset(y: kGuardian.slide).animation(.easeInOut(duration: 1))

an die Ansicht angehängt. In meinem Fall ist es an die gesamte Navigationsansicht angehängt, sodass die gesamte Baugruppe nach oben verschoben wird, wenn die Tastatur angezeigt wird.

Ich habe das Problem, mit SwiftUI eine Symbolleiste "Fertig" oder eine Eingabetaste auf einer Dezimaltastatur zu erhalten, nicht gelöst. Stattdessen verwende ich diese Option, um sie an einer anderen Stelle auszublenden:

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

Sie hängen es an eine Ansicht als an

.modifier(DismissingKeyboard())

Einige Ansichten (z. B. Picker) mögen es nicht, wenn dies angehängt wird. Daher müssen Sie möglicherweise etwas detaillierter vorgehen, wie Sie den Modifikator anhängen, anstatt ihn nur auf die äußerste Ansicht zu legen.

Vielen Dank an @kontiki für die harte Arbeit. Wie oben in seinen Beispielen dargestellt, benötigen Sie oben noch seinen GeometryGetter (nein, ich habe ihn auch nicht konvertiert, um Einstellungen zu verwenden).


1
An die Person, die herabgestimmt hat: Warum? Ich habe versucht, etwas Nützliches hinzuzufügen, also würde ich gerne wissen, wie ich Ihrer Meinung nach falsch
gelaufen

4

Einige der oben genannten Lösungen hatten einige Probleme und waren nicht unbedingt der "sauberste" Ansatz. Aus diesem Grund habe ich einige Dinge für die unten stehende Implementierung geändert.

extension View {
    func onKeyboard(_ keyboardYOffset: Binding<CGFloat>) -> some View {
        return ModifiedContent(content: self, modifier: KeyboardModifier(keyboardYOffset))
    }
}

struct KeyboardModifier: ViewModifier {
    @Binding var keyboardYOffset: CGFloat
    let keyboardWillAppearPublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
    let keyboardWillHidePublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)

    init(_ offset: Binding<CGFloat>) {
        _keyboardYOffset = offset
    }

    func body(content: Content) -> some View {
        return content.offset(x: 0, y: -$keyboardYOffset.wrappedValue)
            .animation(.easeInOut(duration: 0.33))
            .onReceive(keyboardWillAppearPublisher) { notification in
                let keyWindow = UIApplication.shared.connectedScenes
                    .filter { $0.activationState == .foregroundActive }
                    .map { $0 as? UIWindowScene }
                    .compactMap { $0 }
                    .first?.windows
                    .filter { $0.isKeyWindow }
                    .first

                let yOffset = keyWindow?.safeAreaInsets.bottom ?? 0

                let keyboardFrame = (notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue ?? .zero

                self.$keyboardYOffset.wrappedValue = keyboardFrame.height - yOffset
        }.onReceive(keyboardWillHidePublisher) { _ in
            self.$keyboardYOffset.wrappedValue = 0
        }
    }
}
struct RegisterView: View {
    @State var name = ""
    @State var keyboardYOffset: CGFloat = 0

    var body: some View {

        VStack {
            WelcomeMessageView()
            TextField("Type your name...", text: $name).bordered()
        }.onKeyboard($keyboardYOffset)
            .background(WelcomeBackgroundImage())
            .padding()
    }
}

Ich hätte mir einen saubereren Ansatz gewünscht und die Verantwortung für das Versetzen des Inhalts auf die konstruierte Ansicht (nicht auf den Modifikator) verlagert, aber es scheint, dass ich die Herausgeber nicht dazu bringen konnte, beim Verschieben des Versatzcodes in die Ansicht ordnungsgemäß auszulösen. ...

Beachten Sie auch, dass Publishers in diesem Fall verwendet werden mussten, da final classderzeit unbekannte Ausnahmeabstürze verursacht werden (obwohl sie die Schnittstellenanforderungen erfüllen) und eine ScrollView insgesamt der beste Ansatz beim Anwenden von Offset-Code ist.


Sehr schöne Lösung, sehr zu empfehlen! Ich habe einen Bool hinzugefügt, um anzuzeigen, ob die Tastatur derzeit aktiv ist.
Peanutsmasher

4

Viele dieser Antworten scheinen ehrlich gesagt wirklich aufgebläht zu sein. Wenn Sie SwiftUI verwenden, können Sie auch Combine verwenden.

Erstellen Sie eine KeyboardResponderwie unten gezeigt, dann können Sie sie wie zuvor gezeigt verwenden.

Aktualisiert für iOS 14.

import Combine
import UIKit

final class KeyboardResponder: ObservableObject {

    @Published var keyboardHeight: CGFloat = 0

    init() {
        NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)
            .compactMap { notification in
                (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height
            }
            .receive(on: DispatchQueue.main)
            .assign(to: \.keyboardHeight)
    }
}


struct ExampleView: View {
    @ObservedObject private var keyboardResponder = KeyboardResponder()
    @State private var text: String = ""

    var body: some View {
        VStack {
            Text(text)
            Spacer()
            TextField("Example", text: $text)
        }
        .padding(.bottom, keyboardResponder.keyboardHeight)
    }
}

Ich habe eine .animation (.easeIn) hinzugefügt, die zu der Animation passt, mit der die Tastatur angezeigt wird
Michiel Pelt

(Für iOS 13 gehen Sie zum Verlauf dieser Antwort)
Loris Foe

Hallo, .assign (to: \ .keyboardHeight) gibt den Fehler "Schlüsselpfadtyp kann nicht aus dem Kontext abgeleitet werden; explizite Angabe eines Stammtyps erwägen" aus. Bitte lassen Sie mich die richtige und saubere Lösung für ios 13 und ios 14 wissen.
Shahbaz Sajjad

3

Ich bin nicht sicher, ob die Übergangs- / Animations-API für SwiftUI vollständig ist, aber Sie könnten CGAffineTransformmit verwenden.transformEffect

Erstellen Sie ein beobachtbares Tastaturobjekt mit einer veröffentlichten Eigenschaft wie der folgenden:

    final class KeyboardResponder: ObservableObject {
    private var notificationCenter: NotificationCenter
    @Published var readyToAppear = false

    init(center: NotificationCenter = .default) {
        notificationCenter = center
        notificationCenter.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        notificationCenter.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    deinit {
        notificationCenter.removeObserver(self)
    }

    @objc func keyBoardWillShow(notification: Notification) {
        readyToAppear = true
    }

    @objc func keyBoardWillHide(notification: Notification) {
        readyToAppear = false
    }

}

Dann können Sie diese Eigenschaft verwenden, um Ihre Ansicht wie folgt neu zu ordnen:

    struct ContentView : View {
    @State var textfieldText: String = ""
    @ObservedObject private var keyboard = KeyboardResponder()

    var body: some View {
        return self.buildContent()
    }

    func buildContent() -> some View {
        let mainStack = VStack {
            TextField("TextField1", text: self.$textfieldText)
            TextField("TextField2", text: self.$textfieldText)
            TextField("TextField3", text: self.$textfieldText)
            TextField("TextField4", text: self.$textfieldText)
            TextField("TextField5", text: self.$textfieldText)
            TextField("TextField6", text: self.$textfieldText)
            TextField("TextField7", text: self.$textfieldText)
        }
        return Group{
            if self.keyboard.readyToAppear {
                mainStack.transformEffect(CGAffineTransform(translationX: 0, y: -200))
                    .animation(.spring())
            } else {
                mainStack
            }
        }
    }
}

oder einfacher

VStack {
        TextField("TextField1", text: self.$textfieldText)
        TextField("TextField2", text: self.$textfieldText)
        TextField("TextField3", text: self.$textfieldText)
        TextField("TextField4", text: self.$textfieldText)
        TextField("TextField5", text: self.$textfieldText)
        TextField("TextField6", text: self.$textfieldText)
        TextField("TextField7", text: self.$textfieldText)
    }.transformEffect(keyboard.readyToAppear ? CGAffineTransform(translationX: 0, y: -50) : .identity)
            .animation(.spring())

Ich liebe diese Antwort, bin mir aber nicht ganz sicher, woher 'ScreenSize.portrait' kommt.
Mischa Stone

Hallo @MishaStone, danke, wähle meinen Ansatz. ScreenSize.portrait ist eine Klasse, die ich erstellt habe, um Messungen der Bildschirmbasis anhand von Ausrichtung und Prozentsatz zu erhalten. Sie können sie jedoch durch jeden Wert ersetzen, den Sie für Ihre Übersetzung benötigen
blacktiago

3

Xcode 12 Beta 4 fügt einen neuen Ansichtsmodifikator hinzu ignoresSafeArea, mit dem Sie jetzt die Tastatur umgehen können.

.ignoresSafeArea([], edges: [])

Dies vermeidet die Tastatur und alle Kanten des sicheren Bereichs. Sie können den ersten Parameter auf einstellen, .keyboardwenn Sie nicht möchten, dass er vermieden wird. Es gibt einige Macken, zumindest in meiner Ansichtshierarchie, aber es scheint, dass Apple auf diese Weise möchte, dass wir die Tastatur meiden.


2

Antwort von hier kopiert: TextField immer auf der Tastatur mit SwiftUI

Ich habe verschiedene Ansätze ausprobiert und keiner von ihnen hat für mich funktioniert. Dieser unten ist der einzige, der für verschiedene Geräte funktioniert hat.

Fügen Sie diese Erweiterung in eine Datei ein:

import SwiftUI
import Combine

extension View {
    func keyboardSensible(_ offsetValue: Binding<CGFloat>) -> some View {
        
        return self
            .padding(.bottom, offsetValue.wrappedValue)
            .animation(.spring())
            .onAppear {
                NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { notification in
                    
                    let keyWindow = UIApplication.shared.connectedScenes
                        .filter({$0.activationState == .foregroundActive})
                        .map({$0 as? UIWindowScene})
                        .compactMap({$0})
                        .first?.windows
                        .filter({$0.isKeyWindow}).first
                    
                    let bottom = keyWindow?.safeAreaInsets.bottom ?? 0
                    
                    let value = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
                    let height = value.height
                    
                    offsetValue.wrappedValue = height - bottom
                }
                
                NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in
                    offsetValue.wrappedValue = 0
                }
        }
    }
}

In Ihrer Ansicht benötigen Sie eine Variable, um offsetValue zu binden:

struct IncomeView: View {

  @State private var offsetValue: CGFloat = 0.0

  var body: some View { 
    
    VStack {
     //...       
    }
    .keyboardSensible($offsetValue)
  }
}

2
Nur zu Ihrer Information, Sie besitzen die Objekte, wenn Sie anrufen NotificationCenter.default.addObserver... Sie müssen diese speichern und die Beobachter zu einem geeigneten Zeitpunkt entfernen ...
TheCodingArt

Hallo @TheCodingArt, das stimmt, ich habe versucht, das so zu machen ( oleb.net/blog/2018/01/notificationcenter-removeobserver ), aber es scheint für mich nicht zu funktionieren, irgendwelche Ideen?
Ray

2

Wie Mark Krenek und Heiko hervorgehoben haben, schien Apple dieses Problem in Xcode 12 Beta 4 endlich zu lösen. Die Dinge bewegen sich schnell. Laut den am 18. August 2020 veröffentlichten Versionshinweisen für Xcode 12 Beta 5 "verbergen Formular, Liste und TextEditor keine Inhalte mehr hinter der Tastatur. (66172025)". Ich habe es einfach heruntergeladen und es im Beta 5-Simulator (iPhone SE2) mit einem Formularcontainer in einer App getestet, die ich vor einigen Tagen gestartet habe.

Es funktioniert jetzt "nur" für ein TextField . SwiftUI stellt dem Kapselungsformular automatisch die entsprechende untere Auffüllung zur Verfügung, um Platz für die Tastatur zu schaffen. Das Formular wird automatisch nach oben gescrollt, um das Textfeld direkt über der Tastatur anzuzeigen. Der ScrollView-Container verhält sich jetzt gut, wenn auch die Tastatur aufgerufen wird.

Wie Андрей Первушин in einem Kommentar hervorhob, liegt jedoch ein Problem mit TextEditor vor . Beta 5 & 6 stellen automatisch das entsprechende untere Polster für das Kapselungsformular bereit, um Platz für die Tastatur zu schaffen. Das Formular wird jedoch NICHT automatisch nach oben gescrollt. Die Tastatur deckt den TextEditor ab. Im Gegensatz zu TextField muss der Benutzer also durch das Formular scrollen, um den TextEditor sichtbar zu machen. Ich werde einen Fehlerbericht einreichen. Vielleicht wird Beta 7 das Problem beheben. So nah …

https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-14-beta-release-notes/


Ich sehe Apple Release Notes, getestet auf Beta5 und Beta6, TextField funktioniert, TextEditor NICHT, was vermisse ich? @State var text = "" var body: einige Ansichten {Form {Abschnitt {Text (Text) .frame (Höhe: 500)} Abschnitt {TextField ("5555", Text: $ text) .frame (Höhe: 50)} Abschnitt {TextEditor (Text: $ text) .frame (Höhe: 120)}}}
29ндрей Первушин

2

Verwendung:

import SwiftUI

var body: some View {
    ScrollView {
        VStack {
          /*
          TextField()
          */
        }
    }.keyboardSpace()
}

Code:

import SwiftUI
import Combine

let keyboardSpaceD = KeyboardSpace()
extension View {
    func keyboardSpace() -> some View {
        modifier(KeyboardSpace.Space(data: keyboardSpaceD))
    }
}

class KeyboardSpace: ObservableObject {
    var sub: AnyCancellable?
    
    @Published var currentHeight: CGFloat = 0
    var heightIn: CGFloat = 0 {
        didSet {
            withAnimation {
                if UIWindow.keyWindow != nil {
                    //fix notification when switching from another app with keyboard
                    self.currentHeight = heightIn
                }
            }
        }
    }
    
    init() {
        subscribeToKeyboardEvents()
    }
    
    private let keyboardWillOpen = NotificationCenter.default
        .publisher(for: UIResponder.keyboardWillShowNotification)
        .map { $0.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect }
        .map { $0.height - (UIWindow.keyWindow?.safeAreaInsets.bottom ?? 0) }
    
    private let keyboardWillHide =  NotificationCenter.default
        .publisher(for: UIResponder.keyboardWillHideNotification)
        .map { _ in CGFloat.zero }
    
    private func subscribeToKeyboardEvents() {
        sub?.cancel()
        sub = Publishers.Merge(keyboardWillOpen, keyboardWillHide)
            .subscribe(on: RunLoop.main)
            .assign(to: \.self.heightIn, on: self)
    }
    
    deinit {
        sub?.cancel()
    }
    
    struct Space: ViewModifier {
        @ObservedObject var data: KeyboardSpace
        
        func body(content: Content) -> some View {
            VStack(spacing: 0) {
                content
                
                Rectangle()
                    .foregroundColor(Color(.clear))
                    .frame(height: data.currentHeight)
                    .frame(maxWidth: .greatestFiniteMagnitude)

            }
        }
    }
}

extension UIWindow {
    static var keyWindow: UIWindow? {
        let keyWindow = UIApplication.shared.connectedScenes
            .filter({$0.activationState == .foregroundActive})
            .map({$0 as? UIWindowScene})
            .compactMap({$0})
            .first?.windows
            .filter({$0.isKeyWindow}).first
        return keyWindow
    }
}

haben Sie Ihre Lösung ausprobiert ... Die Ansicht wird nur zur Hälfte des Textfelds gescrollt. Versuchte alle oben genannten die Lösung. Habe das gleiche Problem. Bitte helfen Sie !!!
Sona

@Zeona, versuchen Sie es in einer einfachen App, vielleicht machen Sie etwas anderes. Versuchen Sie auch, '- (UIWindow.keyWindow? .SafeAreaInsets.bottom ?? 0)' zu entfernen, wenn Sie einen sicheren Bereich verwenden.
8suhas

entfernt (UIWindow.keyWindow? .safeAreaInsets.bottom ?? 0), dann erhalte ich einen Leerraum über der Tastatur
Sona

1

Handhabung TabView‚s

Ich mag die Antwort von Benjamin Kindle, aber sie unterstützt TabViews nicht. Hier ist meine Anpassung an seinen Code für die Handhabung von TabViews:

  1. Fügen Sie eine Erweiterung hinzu, UITabViewum die Größe der tabView zu speichern, wenn der Frame festgelegt ist. Wir können dies in einer statischen Variablen speichern, da in einem Projekt normalerweise nur eine tabView vorhanden ist (wenn Ihre mehr als eine hat, müssen Sie sie anpassen).
extension UITabBar {

    static var size: CGSize = .zero

    open override var frame: CGRect {
        get {
            super.frame
        } set {
            UITabBar.size = newValue.size
            super.frame = newValue
        }
    }
}
  1. Sie müssen seine onReceiveam unteren Rand der KeyboardHostAnsicht ändern, um die Höhe der Registerkartenleiste zu berücksichtigen:
.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in
            self.keyboardHeight = max(height - UITabBar.size.height, 0)
        }
  1. Und das ist es! Super einfach 🎉.

1

Ich habe einen völlig anderen Ansatz gewählt, indem ich Folgendes erweitert UIHostingControllerund angepasst habe additionalSafeAreaInsets:

class MyHostingController<Content: View>: UIHostingController<Content> {
    override init(rootView: Content) {
        super.init(rootView: rootView)
    }

    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        NotificationCenter.default.addObserver(self, 
                                               selector: #selector(keyboardDidShow(_:)), 
                                               name: UIResponder.keyboardDidShowNotification,
                                               object: nil)
        NotificationCenter.default.addObserver(self, 
                                               selector: #selector(keyboardWillHide), 
                                               name: UIResponder.keyboardWillHideNotification, 
                                               object: nil)
    }       

    @objc func keyboardDidShow(_ notification: Notification) {
        guard let info:[AnyHashable: Any] = notification.userInfo,
            let frame = info[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
                return
        }

        // set the additionalSafeAreaInsets
        let adjustHeight = frame.height - (self.view.safeAreaInsets.bottom - self.additionalSafeAreaInsets.bottom)
        self.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: adjustHeight, right: 0)

        // now try to find a UIResponder inside a ScrollView, and scroll
        // the firstResponder into view
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { 
            if let firstResponder = UIResponder.findFirstResponder() as? UIView,
                let scrollView = firstResponder.parentScrollView() {
                // translate the firstResponder's frame into the scrollView's coordinate system,
                // with a little vertical padding
                let rect = firstResponder.convert(firstResponder.frame, to: scrollView)
                    .insetBy(dx: 0, dy: -15)
                scrollView.scrollRectToVisible(rect, animated: true)
            }
        }
    }

    @objc func keyboardWillHide() {
        self.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
    }
}

/// IUResponder extension for finding the current first responder
extension UIResponder {
    private struct StaticFirstResponder {
        static weak var firstResponder: UIResponder?
    }

    /// find the current first responder, or nil
    static func findFirstResponder() -> UIResponder? {
        StaticFirstResponder.firstResponder = nil
        UIApplication.shared.sendAction(
            #selector(UIResponder.trap),
            to: nil, from: nil, for: nil)
        return StaticFirstResponder.firstResponder
    }

    @objc private func trap() {
        StaticFirstResponder.firstResponder = self
    }
}

/// UIView extension for finding the receiver's parent UIScrollView
extension UIView {
    func parentScrollView() -> UIScrollView? {
        if let scrollView = self.superview as? UIScrollView {
            return scrollView
        }

        return superview?.parentScrollView()
    }
}

Dann ändern Sie SceneDelegatezu verwenden MyHostingControlleranstelle von UIHostingController.

Wenn das erledigt ist, muss ich mich nicht mehr um die Tastatur in meinem SwiftUI-Code kümmern.

(Hinweis: Ich habe dies noch nicht genug verwendet, um die Auswirkungen davon vollständig zu verstehen!)


1

So gehe ich mit der Tastatur in SwiftUI um. Beachten Sie, dass die Berechnungen auf dem VStack durchgeführt werden, an den es angehängt ist.

Sie verwenden es in einer Ansicht als Modifikator. Diesen Weg:

struct LogInView: View {

  var body: some View {
    VStack {
      // Your View
    }
    .modifier(KeyboardModifier())
  }
}

Um zu diesem Modifikator zu gelangen, erstellen Sie zunächst eine Erweiterung von UIResponder, um die ausgewählte TextField-Position im VStack abzurufen:

import UIKit

// MARK: Retrieve TextField first responder for keyboard
extension UIResponder {

  private static weak var currentResponder: UIResponder?

  static var currentFirstResponder: UIResponder? {
    currentResponder = nil
    UIApplication.shared.sendAction(#selector(UIResponder.findFirstResponder),
                                    to: nil, from: nil, for: nil)
    return currentResponder
  }

  @objc private func findFirstResponder(_ sender: Any) {
    UIResponder.currentResponder = self
  }

  // Frame of the superview
  var globalFrame: CGRect? {
    guard let view = self as? UIView else { return nil }
    return view.superview?.convert(view.frame, to: nil)
  }
}

Sie können den KeyboardModifier jetzt mit Combine erstellen, um zu vermeiden, dass eine Tastatur ein TextField verbirgt:

import SwiftUI
import Combine

// MARK: Keyboard show/hide VStack offset modifier
struct KeyboardModifier: ViewModifier {

  @State var offset: CGFloat = .zero
  @State var subscription = Set<AnyCancellable>()

  func body(content: Content) -> some View {
    GeometryReader { geometry in
      content
        .padding(.bottom, self.offset)
        .animation(.spring(response: 0.4, dampingFraction: 0.5, blendDuration: 1))
        .onAppear {

          NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
            .handleEvents(receiveOutput: { _ in self.offset = 0 })
            .sink { _ in }
            .store(in: &self.subscription)

          NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)
            .map(\.userInfo)
            .compactMap { ($0?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.size.height }
            .sink(receiveValue: { keyboardHeight in
              let keyboardTop = geometry.frame(in: .global).height - keyboardHeight
              let textFieldBottom = UIResponder.currentFirstResponder?.globalFrame?.maxY ?? 0
              self.offset = max(0, textFieldBottom - keyboardTop * 2 - geometry.safeAreaInsets.bottom) })
        .store(in: &self.subscription) }
        .onDisappear {
          // Dismiss keyboard
          UIApplication.shared.windows
            .first { $0.isKeyWindow }?
            .endEditing(true)

          self.subscription.removeAll() }
    }
  }
}

1

Was iOS 14 (Beta 4) betrifft, funktioniert es ganz einfach:

var body: some View {
    VStack {
        TextField(...)
    }
    .padding(.bottom, 0)
}

Die Größe der Ansicht wird an die Oberseite der Tastatur angepasst. Mit dem Rahmen (.maxHeight: ...) usw. sind sicherlich weitere Verfeinerungen möglich. Sie werden es herausfinden.

Leider verursacht die schwebende Tastatur auf dem iPad beim Verschieben immer noch Probleme. Aber die oben genannten Lösungen würden auch, und es ist noch Beta, ich hoffe, sie werden es herausfinden.

Danke Apple endlich!


Dies funktioniert überhaupt nicht (14.1). Was ist die Idee?
Burgler-dev

0

Meine Sicht:

struct AddContactView: View {
    
    @Environment(\.presentationMode) var presentationMode : Binding<PresentationMode>
    
    @ObservedObject var addContactVM = AddContactVM()
    
    @State private var offsetValue: CGFloat = 0.0
    
    @State var firstName : String
    @State var lastName : String
    @State var sipAddress : String
    @State var phoneNumber : String
    @State var emailID : String
    
  
    var body: some View {
        
        
        VStack{
            
            Header(title: StringConstants.ADD_CONTACT) {
                
                self.presentationMode.wrappedValue.dismiss()
            }
            
           ScrollView(Axis.Set.vertical, showsIndicators: false){
            
            Image("contactAvatar")
                .padding(.top, 80)
                .padding(.bottom, 100)
                //.padding(.vertical, 100)
                //.frame(width: 60,height : 60).aspectRatio(1, contentMode: .fit)
            
            VStack(alignment: .center, spacing: 0) {
                
                
                TextFieldBorder(placeHolder: StringConstants.FIRST_NAME, currentText: firstName, imageName: nil)
                
                TextFieldBorder(placeHolder: StringConstants.LAST_NAME, currentText: lastName, imageName: nil)
                
                TextFieldBorder(placeHolder: StringConstants.SIP_ADDRESS, currentText: sipAddress, imageName: "sipPhone")
                TextFieldBorder(placeHolder: StringConstants.PHONE_NUMBER, currentText: phoneNumber, imageName: "phoneIcon")
                TextFieldBorder(placeHolder: StringConstants.EMAILID, currentText: emailID, imageName: "email")
                

            }
            
           Spacer()
            
        }
        .padding(.horizontal, 20)
        
            
        }
        .padding(.bottom, self.addContactVM.bottomPadding)
        .onAppear {
            
            NotificationCenter.default.addObserver(self.addContactVM, selector: #selector(self.addContactVM.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
            
             NotificationCenter.default.addObserver(self.addContactVM, selector: #selector(self.addContactVM.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
        }
        
    }
}

Meine VM:

class AddContactVM : ObservableObject{
    
    @Published var contact : Contact = Contact(id: "", firstName: "", lastName: "", phoneNumbers: [], isAvatarAvailable: false, avatar: nil, emailID: "")
    
    @Published var bottomPadding : CGFloat = 0.0
    
    @objc  func keyboardWillShow(_ notification : Notification){
        
        if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
            let keyboardRectangle = keyboardFrame.cgRectValue
            let keyboardHeight = keyboardRectangle.height
            self.bottomPadding = keyboardHeight
        }
        
    }
    
    @objc  func keyboardWillHide(_ notification : Notification){
        
        
        self.bottomPadding = 0.0
        
    }
    
}

Grundsätzlich wird die untere Polsterung basierend auf der Tastaturhöhe verwaltet.


-3

Die eleganteste Antwort, die ich darauf erhalten habe, ähnelt der von Rraphael. Erstellen Sie eine Klasse, um auf Tastaturereignisse zu warten. Anstatt die Tastaturgröße zum Ändern der Auffüllung zu verwenden, geben Sie einen negativen Wert der Tastaturgröße zurück und passen Sie mit dem Modifikator .offset (y :) den Versatz der äußersten Ansichtscontainer an. Es animiert gut genug und funktioniert mit jeder Ansicht.


Wie haben Sie das zum Animieren gebracht? Ich habe .offset(y: withAnimation { -keyboard.currentHeight }), aber der Inhalt springt statt animiert.
Jjatie

Es ist ein paar Betas her, dass ich mich mit diesem Code beschäftigt habe, aber zum Zeitpunkt meines früheren Kommentars war SwiftUI alles, was erforderlich war, um den Offset eines vstack zur Laufzeit zu ändern.
pcallycat
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.