Przenieś TextField w górę, gdy klawiatura pojawi się w SwiftUI


100

Mam siedem TextFieldw moim głównym ContentView. Gdy użytkownik otwiera klawiaturę, niektóre z nich TextFieldsą ukryte pod ramką klawiatury. Więc chcę przesunąć wszystko TextFieldodpowiednio w górę, gdy pojawi się klawiatura.

Użyłem poniższego kodu, aby dodać TextFieldna ekranie.

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

Wynik:

Wynik



1
@PrashantTukadiya Dzięki za szybką odpowiedź. Dodałem TextField w Scrollview, ale nadal mam ten sam problem.
Hitesh Surani

1
@DimaPaliychuk To nie zadziała. to SwiftUI
Prashant Tukadiya

28
Wyświetlanie klawiatury i zasłanianie treści na ekranie było obecne od czasu pierwszej aplikacji Objective C na iPhone'a? To jest problem, który jest ciągle rozwiązywany. Po pierwsze, jestem rozczarowany, że Apple nie rozwiązało tego problemu w SwiftUi. Wiem, że ten komentarz nikomu nie jest pomocny, ale chciałem poruszyć tę kwestię, że naprawdę powinniśmy wywierać presję na Apple, aby zapewnił rozwiązanie, a nie polegać na społeczności, która zawsze będzie dostarczać ten najczęstszy problem.
P. Ent,

3
Jest bardzo dobry artykuł autorstwa Vadima vadimbulavin.com/…
Sudara

Odpowiedzi:


64

Zaktualizowany kod dla Xcode, beta 7.

Aby to osiągnąć, nie potrzebujesz dopełnienia, ScrollViews ani List. Chociaż to rozwiązanie też się z nimi sprawdzi. Podaję tutaj dwa przykłady.

Pierwsza przesuwa cały tekst TextField w górę, jeśli pojawi się klawiatura dla któregokolwiek z nich. Ale tylko w razie potrzeby. Jeśli klawiatura nie zakrywa pól tekstowych, nie będą się one poruszać.

W drugim przykładzie widok przesuwa się tylko na tyle, aby uniknąć ukrycia aktywnego pola tekstowego.

Oba przykłady używają tego samego wspólnego kodu, który znajduje się na końcu: GeometryGetter i KeyboardGuardian

Pierwszy przykład (pokaż wszystkie pola tekstowe)

Gdy klawiatura jest otwarta, trzy pola tekstowe są przesuwane na tyle w górę, aby były wtedy widoczne

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

}

Drugi przykład (pokaż tylko aktywne pole)

Po kliknięciu każdego pola tekstowego widok jest przesuwany tylko na tyle, aby widoczne było kliknięte pole tekstowe.

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

Jest to widok, który pochłania rozmiar i położenie widoku macierzystego. Aby to osiągnąć, jest wywoływana wewnątrz modyfikatora .background. To bardzo potężny modyfikator, a nie tylko sposób na dekorację tła widoku. Podczas przekazywania widoku do .background (MyView ()), MyView pobiera zmodyfikowany widok jako element nadrzędny. Użycie GeometryReader umożliwia widokowi poznanie geometrii elementu nadrzędnego.

Na przykład: Text("hello").background(GeometryGetter(rect: $bounds))wypełni zmienne granice, rozmiarem i położeniem widoku Tekst i używając globalnego obszaru współrzędnych.

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

Aktualizacja Dodałem DispatchQueue.main.async, aby uniknąć możliwości modyfikowania stanu widoku podczas renderowania. ***

KeyboardGuardian

Zadaniem KeyboardGuardian jest śledzenie zdarzeń pokazywania / ukrywania klawiatury i obliczanie, o ile miejsce należy przesunąć widok.

Aktualizacja: Zmodyfikowałem KeyboardGuardian, aby odświeżał slajd, gdy użytkownik przechodzi z jednego pola do drugiego

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
Czy można dołączyć GeometryGetterjako modyfikator widoku niż tło, dostosowując je do ViewModifierprotokołu?
Sudara

2
Jest to możliwe, ale jaki jest zysk? Dołączasz to w ten sposób: .modifier(GeometryGetter(rect: $kGuardian.rects[1]))zamiast .background(GeometryGetter(rect: $kGuardian.rects[1])). Niewielka różnica (tylko 2 znaki mniej).
kontiki

3
W niektórych sytuacjach możesz uzyskać SIGNAL ABORT z programu wewnątrz GeometryGetter podczas przypisywania nowego prostokąta, jeśli oddalasz się od tego ekranu. Jeśli tak się stanie, po prostu dodaj kod, aby sprawdzić, czy rozmiar geometrii jest większy niż zero (geometry.size.width> 0 && geometry.size.height> 0) przed przypisaniem wartości do self.rect
Julio Bailon

1
@JulioBailon Nie wiem dlaczego, ale geometry.framewyprowadzam się z DispatchQueue.main.asyncpomocy SIGNAL ABORT, teraz przetestuję Twoje rozwiązanie. Aktualizacja: if geometry.size.width > 0 && geometry.size.height > 0przed przypisaniem self.rectpomogła.
Roman Wasiljew

2
to psuje mi się również w przypadku self.rect = geometry.frame (w: .global) podczas pobierania SIGNAL ABORT i wypróbowałem wszystkie proponowane rozwiązania, aby rozwiązać ten błąd
Marwan Roushdy

55

Aby zbudować rozwiązanie @rraphael, przekonwertowałem je tak, aby było użyteczne dzięki dzisiejszej obsłudze swiftUI xcode11.

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

Stosowanie:

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

Opublikowane currentHeightwyzwoli ponowne renderowanie interfejsu użytkownika i przeniesie TextField w górę, gdy pojawi się klawiatura, i z powrotem w dół po zamknięciu. Jednak nie użyłem ScrollView.


6
Podoba mi się ta odpowiedź za jej prostotę. Dodałem, .animation(.easeOut(duration: 0.16))aby spróbować dopasować prędkość przesuwania się klawiatury.
Mark Moeykens

Dlaczego ustawiłeś maksymalną wysokość klawiatury na 340?
Daniel Ryan

1
@DanielRyan Czasami wysokość klawiatury w symulatorze zwracała nieprawidłowe wartości. W tej chwili nie mogę znaleźć sposobu na ustalenie problemu
Michael Neas

1
Sam nie widziałem tego problemu. Może zostało to naprawione w najnowszych wersjach. Nie chciałem blokować rozmiaru na wypadek, gdyby były (lub będą) większe klawiatury.
Daniel Ryan

1
Możesz spróbować keyboardFrameEndUserInfoKey. To powinno zawierać ostatnią klatkę klawiatury.
Mathias Claassen

51

Wypróbowałem wiele z proponowanych rozwiązań i chociaż działają one w większości przypadków, miałem pewne problemy - głównie z bezpiecznym obszarem (mam Formularz w zakładce TabView).

Skończyło się na połączeniu kilku różnych rozwiązań i użyciu GeometryReader, aby uzyskać wstawkę dolną w bezpiecznym obszarze określonego widoku i użyć jej w obliczeniach wypełnienia:

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

Stosowanie:

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

7
Wow, to jest najbardziej SwiftUI z nich wszystkich, z GeometryReader i ViewModifier. Kocham to.
Mateusz

3
To jest tak przydatne i eleganckie. Dziękuję bardzo za napisanie tego.
Danilo Campos

2
Widzę mały, pusty, biały widok na mojej klawiaturze. Ten widok to widok GeometryReader, potwierdzony zmianą koloru tła. Każdy pomysł, dlaczego GeometryReader wyświetla się między moim rzeczywistym widokiem a klawiaturą.
user832

6
Pojawia się błąd Thread 1: signal SIGABRTon-line, rect.height - geometry.safeAreaInsets.bottomkiedy po raz drugi przechodzę do widoku z klawiaturą i klikam TextField. Nie ma znaczenia, czy kliknę TextFieldpierwszy raz, czy nie. Aplikacja nadal się zawiesza.
JLively

2
Wreszcie coś, co działa! Dlaczego Apple nie może tego zrobić, to dla nas orzechy !!
Dave Kozikowski

35

Utworzyłem Widok, który może zawijać dowolny inny widok, aby go zmniejszyć, gdy pojawi się klawiatura.

To całkiem proste. Tworzymy wydawców dla wydarzeń show / hide klawiatury, a następnie zapisujemy się do nich za pomocą onReceive. Użyjemy wyniku tego do utworzenia prostokąta wielkości klawiatury za klawiaturą.

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

Następnie możesz użyć widoku w następujący sposób:

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

Aby przesunąć zawartość widoku w górę, zamiast ją zmniejszać, można dodać dopełnienie lub przesunięcie viewzamiast umieszczać go w VStack z prostokątem.


6
Myślę, że to dobra odpowiedź. Tylko drobna poprawka, którą zrobiłem: zamiast prostokąta modyfikuję tylko wypełnienie self.viewi działa świetnie. Żadnych problemów z animacją
Tae

5
Dzięki! Działa świetnie. Jak powiedział @Taed, lepiej jest zastosować podejście dopełniające. Ostateczny wynik tovar body: some View { VStack { view .padding(.bottom, keyboardHeight) .animation(.default) } .onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = height } }
fdelafuente

1
Pomimo mniejszej liczby głosów jest to najszybsza odpowiedź interfejsu użytkownika. Wcześniejsze podejście z użyciem AnyView przerywa pomoc dotyczącą przyspieszania metalu.
Nelson Cardaci

4
To świetne rozwiązanie, ale głównym problemem jest to, że tracisz możliwość przesuwania widoku w górę tylko wtedy, gdy klawiatura ukrywa edytowane pole tekstowe. Mam na myśli: jeśli masz formularz z kilkoma polami tekstowymi i zaczynasz edytować pierwszy na górze, prawdopodobnie nie chcesz, aby przesunął się w górę, ponieważ zniknąłby z ekranu.
matteopuc

Naprawdę podoba mi się odpowiedź, ale tak jak wszystkie inne odpowiedzi, nie działa, jeśli twój widok znajduje się wewnątrz paska TabBar lub widok nie jest wyrównany z dolną częścią ekranu.
Ben Patch

25

Stworzyłem bardzo prosty w użyciu modyfikator widoku.

Dodaj plik Swift z poniższym kodem i po prostu dodaj ten modyfikator do swoich widoków:

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


2
Byłoby fajnie, gdyby tylko przesunął, jeśli to konieczne (tj. Nie przewijaj, jeśli klawiatura nie zakrywa elementu wejściowego). Miło mieć ...
dekady

To działa świetnie, dziękuję. Bardzo czysta implementacja, a dla mnie przewija się tylko w razie potrzeby.
Joshua

Niesamowite! Dlaczego nie udostępnisz tego na Github lub gdzie indziej? :) Lub możesz zasugerować to github.com/hackiftekhar/IQKeyboardManager, ponieważ nie mają jeszcze pełnego wsparcia
SwiftUI

Nie będzie dobrze grać ze zmianami orientacji i będzie kompensować niezależnie od tego, czy jest to potrzebne, czy nie.
GrandSteph

Jednym z problemów jest to, że to w ogóle nie jest animowane ... tworzy bardzo roztrzęsiony ruch 😢
Zorayr

22

Xcode 12 - kod jednowierszowy

Dodaj ten modyfikator do TextField

.ignoresSafeArea(.keyboard, edges: .bottom)

Próbny

Jabłko dodaje klawiaturę jako region dla bezpiecznej okolicy, więc można go używać do poruszania któregokolwiekView z klawiaturą jak w innych regionach.


Działa na Any View , w tym na TextEditor.
Mojtaba Hosseini

@MojtabaHosseini co jeśli chcę zapobiec takiemu zachowaniu? Mam obraz, który jest teraz przesuwany w górę, gdy otwieram klawiaturę, i nie chcę go ruszać.
kyrers

1
Możesz zadać pytanie i połączyć je tutaj. Mogę więc rzucić okiem na twój odtwarzalny kod i zobaczyć, czy mogę pomóc :) @kyrers
Mojtaba Hosseini

2
Sam to rozgryzłem. Dodaj .ignoresSafeArea(.keyboard)do swojego widoku.
leonboe1

3
niestety to tylko iOS 14 ...
Xaxxus,

16

Lub możesz po prostu użyć IQKeyBoardManagerSwift

i opcjonalnie można dodać to do delegata aplikacji, aby ukryć pasek narzędzi i włączyć ukrywanie klawiatury po kliknięciu dowolnego widoku innego niż klawiatura.

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

Dla mnie jest to rzeczywiście (nieoczekiwana) droga. Solidny.
HelloTimo

Ramy te działały nawet lepiej niż oczekiwano. Dziękuję za udostępnienie!
Richard Poutier

1
Pracuję dla mnie dobrze na SwiftUI - dzięki @DominatorVbN - W trybie poziomym iPada musiałem zwiększyć IQKeyboardManager.shared.keyboardDistanceFromTextFielddo 40, aby uzyskać wygodną lukę.
Richard Groves

Musiałem także ustawić, IQKeyboardManager.shared.enable = trueaby klawiatura nie ukrywała moich pól tekstowych. W każdym razie jest to najlepsze rozwiązanie. Mam 4 pola ułożone pionowo, a inne rozwiązania działałyby dla mojego najniższego pola, ale wypchnęłyby najwyższe z pola widzenia.
MisterEd

12

Musisz dodać ScrollViewi ustawić dolne wypełnienie rozmiaru klawiatury, aby zawartość mogła być przewijana, gdy pojawi się klawiatura.

Aby uzyskać rozmiar klawiatury, musisz NotificationCenterzarejestrować się na zdarzenie klawiatury. Możesz do tego użyć klasy niestandardowej:

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

BindableObjectZgodności pozwoli Ci korzystać z tej klasy jak Statei wywołać aktualizację widoku. W razie potrzeby zapoznaj się z samouczkiem dotyczącym BindableObject: samouczka SwiftUI

Kiedy to otrzymasz, musisz skonfigurować a, ScrollViewaby zmniejszyć jego rozmiar, gdy pojawi się klawiatura. Dla wygody zawarłem to ScrollVieww jakiś element:

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

Wszystko, co musisz teraz zrobić, to osadzić zawartość w niestandardowym 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)
    }
}

Edycja: zachowanie przewijania jest naprawdę dziwne, gdy klawiatura się chowa. Być może użycie animacji do aktualizacji dopełnienia rozwiązałoby ten problem lub powinieneś rozważyć użycie czegoś innego niż paddingdo dostosowania rozmiaru widoku przewijania.


hej, wygląda na to, że masz doświadczenie w bindableobject. Nie mogę sprawić, żeby działało tak, jak chcę. Byłoby miło, gdybyś mógł przejrzeć: stackoverflow.com/questions/56500147/ ...
SwiftiSwift

Dlaczego nie używasz @ObjectBinding
SwiftiSwift

3
Z BindableObject wycofaniu niestety to już nie działa.
LinusGeffarth

2
@LinusGeffarth Na co warto, BindableObjectzostał właśnie przemianowany na ObservableObjecti didChangena objectWillChange. Obiekt aktualizuje widok dobrze (chociaż testowałem przy użyciu @ObservedObjectzamiast @State)
SeizeTheDay

Cześć, to rozwiązanie przewija zawartość, ale pokazuje biały obszar nad klawiaturą, który ukrywa połowę pola tekstowego. Daj mi znać, jak możemy usunąć biały obszar.
Shahbaz Sajjad

6

Przejrzałem i przerobiłem istniejące rozwiązania w poręczny pakiet SPM, który zawiera .keyboardAware()modyfikator:

KeyboardAwareSwiftUI

Przykład:

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

    }
}

Źródło:

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

Widzę tylko połowę wysokości widoku tekstu. czy wiesz, jak to rozwiązać?
Matrosov Alexander

Dobry. to zaoszczędziło mi czasu. Przed użyciem tego wiemy, że dolne wypełnienie widoku uchwytu modyfikatora.
Brownsoo Han

5

Jako punkt wyjścia użyłem odpowiedzi Benjamina Kindle'a, ale miałem kilka problemów, którymi chciałem się zająć.

  1. Większość odpowiedzi tutaj nie dotyczy klawiatury zmieniającej ramkę, więc psują się, gdy użytkownik obraca urządzenie za pomocą klawiatury na ekranie. Dodanie keyboardWillChangeFrameNotificationdo listy przetworzonych powiadomień adresuje to.
  2. Nie chciałem, aby wielu wydawców miało podobne, ale różne zamknięcia mapy, więc połączyłem wszystkie trzy powiadomienia z klawiatury w jednym wydawcy. To co prawda długi łańcuch, ale każdy krok jest dość prosty.
  3. Udostępniłem initfunkcję, która akceptuje a, @ViewBuilderdzięki czemu można używać KeyboardHostwidoku jak każdego innego widoku i po prostu przekazywać zawartość w końcowym zamknięciu, w przeciwieństwie do przekazywania widoku treści jako parametru do init.
  4. Jak zasugerowali Tae i fdelafuente w komentarzach, zamieniłem Rectanglewyściółkę na dolne wypełnienie.
  5. Zamiast używać zakodowanego na stałe ciągu „UIKeyboardFrameEndUserInfoKey”, chciałem użyć ciągów dostarczonych w UIWindowas UIWindow.keyboardFrameEndUserInfoKey.

Biorąc to wszystko razem, mam:

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


to rozwiązanie nie działa, zwiększa Keyboardwysokość
GSerjo

Czy możesz opisać problemy, które widzisz @GSerjo? Używam tego kodu w mojej aplikacji i działa dobrze.
Timothy Sanders

Czy mógłbyś włączyć Pridictivew iOS keyboard. Settings-> General-> Keyboard-> Pridictive. w tym przypadku nie koryguje obliczeń i dodaje dopełnienie do klawiatury
GSerjo

@GSerjo: Mam włączoną funkcję przewidywania tekstu na iPadzie Touch (7.generacji) z systemem iOS 13.1 beta. Poprawnie dodaje wypełnienie wysokości wiersza prognozy. (Ważne, aby pamiętać, nie dostosowuję tutaj wysokości klawiatury , dodam do wypełnienia samego widoku). Spróbuj zamienić mapę debugowania, która jest zakomentowana i graj z wartościami, które otrzymujesz dla predykcyjnego klawiatura. Opublikuję dziennik w kolejnym komentarzu.
Timothy Sanders

Gdy mapa „debugowania” nie jest komentowana, możesz zobaczyć przypisywaną wartość keyboardHeight. Na moim iPodzie Touch (w orientacji pionowej) klawiatura z włączoną funkcją przewidywania ma 254 punkty. Bez tego jest 216 punktów. Mogę nawet wyłączyć przewidywanie za pomocą klawiatury na ekranie, a wypełnienie aktualizuje się prawidłowo. Dodawanie klawiatury z predykcją: Received UIKeyboardWillChangeFrameNotification with height 254.0 Received UIKeyboardWillShowNotification with height 254.0 Kiedy wyłączam przewidywanie tekstu:Received UIKeyboardWillChangeFrameNotification with height 216.0
Timothy Sanders

5

Szczerze mówiąc, wiele z tych odpowiedzi wydaje się po prostu nadęty. Jeśli używasz SwiftUI, równie dobrze możesz skorzystać z Combine.

Utwórz, KeyboardResponderjak pokazano poniżej, a następnie możesz użyć, jak pokazano wcześniej.

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

Dodałem .animation (.easeIn), aby dopasować animację, z którą pojawia się klawiatura
Michiel Pelt.

(W przypadku iOS 13 przejdź do historii tej odpowiedzi)
Loris Foe,

1
Cześć,. Assign (to: \ .keyboardHeight) podaje ten błąd „Nie można wywnioskować typu ścieżki klucza z kontekstu; rozważ jawne określenie typu głównego”. Proszę dać mi znać właściwe i czyste rozwiązanie zarówno dla iOS 13, jak i iOS 14.
Shahbaz Sajjad

Musiałem dodać kolejny odbiornik dla UIResponder.keyboardWillHideNotification. Poza tym - to jedyne rozwiązanie, które u mnie zadziałało. Dziękuję Ci!
Antonín Karásek

4

Jest to zaadaptowane z tego, co zbudował @kontiki. Mam go uruchomionego w aplikacji w wersji beta 8 / GM, gdzie pole wymagające przewijania jest częścią formularza w NavigationView. Oto 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
        }
    }
}

Następnie użyłem wyliczenia do śledzenia gniazd w tablicy rects i całkowitej liczby:

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

KeyboardSlots.count.rawValuejest niezbędną pojemnością tablicy; pozostałe jako rawValue podają odpowiedni indeks, którego będziesz używać do wywołań .background (GeometryGetter).

Przy takiej konfiguracji widoki są wyświetlane w KeyboardGuardian w ten sposób:

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

Rzeczywisty ruch wygląda następująco:

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

dołączony do widoku. W moim przypadku jest dołączony do całego NavigationView, więc cały zestaw przesuwa się w górę, gdy pojawia się klawiatura.

Nie rozwiązałem problemu z uzyskaniem paska narzędzi Gotowe lub klawisza powrotu na klawiaturze dziesiętnej za pomocą SwiftUI, więc zamiast tego używam tego, aby ukryć go w innym miejscu:

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

Dołączasz go do widoku jako

.modifier(DismissingKeyboard())

Niektóre widoki (np. Zbieracze) nie lubią mieć tego dołączonego, więc może być konieczne nieco szczegółowe dołączanie modyfikatora, zamiast po prostu uderzać go w najbardziej zewnętrzny widok.

Wielkie dzięki dla @kontiki za ciężką pracę. Nadal będziesz potrzebować jego GeometryGetter powyżej (nie, nie wykonałem też pracy, aby przekształcić go w preferencje), jak ilustruje w swoich przykładach.


1
Do osoby, która przegłosowała: dlaczego? Próbowałem dodać coś pożytecznego, więc chciałbym wiedzieć, jak Twoim zdaniem poszedłem źle
Feldur

4

Kilka z powyższych rozwiązań wiązało się z pewnymi problemami i niekoniecznie stanowiło „najczystsze” podejście. Z tego powodu zmodyfikowałem kilka rzeczy do implementacji poniżej.

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

Wolałbym bardziej przejrzyste podejście i przenieść odpowiedzialność na skonstruowany widok (a nie modyfikator) w zakresie sposobu przesunięcia treści, ale wydaje się, że nie mogłem zmusić wydawców do prawidłowego uruchomienia podczas przenoszenia kodu przesunięcia do widoku. ...

Należy również pamiętać, że w tym przypadku należało użyć wydawców, ponieważ final classobecnie powoduje awarie nieznanych wyjątków (nawet jeśli spełnia wymagania interfejsu), a ScrollView jest ogólnie najlepszym podejściem do stosowania kodu przesunięcia.


Bardzo fajne rozwiązanie, bardzo polecam! Dodałem Bool, aby wskazać, czy klawiatura jest obecnie aktywna.
Peanutsmasher

3

Nie jestem pewien, czy interfejs API przejścia / animacji dla SwiftUI jest kompletny, ale można go użyć CGAffineTransformz.transformEffect

Utwórz obserwowalny obiekt klawiatury z opublikowaną właściwością, taką jak ta:

    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
    }

}

możesz użyć tej właściwości, aby zmienić układ widoku w następujący sposób:

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

lub prostsze

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

Uwielbiam tę odpowiedź, ale nie jestem do końca pewien, skąd pochodzi „ScreenSize.portrait”.
Misha Stone

Cześć @MishaStone, dzięki za wybór mojego podejścia. ScreenSize.portrait jest klasą, że zrobiłem, aby uzyskać pomiary podstawy ekranu na orientację i procent .... ale można zastąpić go z dowolnej wartości potrzebne w swoim tłumaczeniu
blacktiago

3

Xcode 12 beta 4 dodaje nowy modyfikator widoku ignoresSafeArea, którego możesz teraz użyć, aby uniknąć klawiatury.

.ignoresSafeArea([], edges: [])

Pozwala to uniknąć klawiatury i wszystkich krawędzi obszaru bezpiecznego. Możesz ustawić pierwszy parametr na, .keyboardjeśli nie chcesz tego unikać. Jest w tym kilka dziwactw, przynajmniej w moim widoku hierarchii, ale wydaje się, że to jest sposób, w jaki Apple chce, abyśmy unikali klawiatury.


2

Odpowiedź skopiowana stąd: TextField zawsze na górze klawiatury z SwiftUI

Próbowałem różnych podejść i żadne z nich nie działało dla mnie. Ten poniżej jest jedynym, który działał na różnych urządzeniach.

Dodaj to rozszerzenie do pliku:

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

Twoim zdaniem potrzebujesz zmiennej do powiązania offsetValue:

struct IncomeView: View {

  @State private var offsetValue: CGFloat = 0.0

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

2
Tylko do Twojej wiadomości, jesteś właścicielem obiektów podczas dzwonienia NotificationCenter.default.addObserver... musisz je przechowywać i usuwać obserwatorów w odpowiednim czasie ...
TheCodingArt

Cześć @TheCodingArt, zgadza się, próbowałem to zrobić w ten sposób ( oleb.net/blog/2018/01/notificationcenter-removeobserver ), ale wydaje mi się, że to nie działa, jakieś pomysły?
Ray

2

Jak zauważyli Mark Krenek i Heiko, Apple wydawało się w końcu zająć się tym problemem w Xcode 12 beta 4. Sprawy toczą się szybko. Zgodnie z uwagami do wydania dla Xcode 12 beta 5 opublikowanymi 18 sierpnia 2020 r. „Form, List i TextEditor nie ukrywają już zawartości za klawiaturą. (66172025)”. Po prostu go pobrałem i wykonałem szybki test w symulatorze beta 5 (iPhone SE2) z kontenerem Form w aplikacji, którą uruchomiłem kilka dni temu.

Teraz „po prostu działa” dla pola tekstowego . SwiftUI automatycznie zapewni odpowiednią dolną wyściółkę do hermetyzującego formularza, aby zrobić miejsce na klawiaturę. I automatycznie przewinie formularz w górę, aby wyświetlić pole tekstowe tuż nad klawiaturą. Kontener ScrollView teraz zachowuje się dobrze, gdy pojawia się klawiatura.

Jednak, jak zauważył Андрей Первушин w komentarzu, istnieje problem z TextEditorem . Beta 5 i 6 automatycznie zapewnią odpowiednią dolną wyściółkę do hermetyzującego formularza, aby zrobić miejsce na klawiaturę. Ale NIE spowoduje to automatycznego przewijania formularza w górę. Klawiatura zakryje TextEditor. W przeciwieństwie do TextField, użytkownik musi przewinąć formularz, aby TextEditor był widoczny. Złożę raport o błędzie. Być może Beta 7 to naprawi. Tak blisko …

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


widzę informacje o wydaniu Apple, testowane na beta5 i beta6, TextField działa, TextEditor NIE, za czym tęsknię? @State var text = "" var body: some View {Form {Section {Text (text) .frame (height: 500)} Section {TextField ("5555", text: $ text) .frame (height: 50)} Sekcja {TextEditor (text: $ text) .frame (height: 120)}}}
Андрей Первушин

2

Stosowanie:

import SwiftUI

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

Kod:

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

wypróbowane rozwiązanie ... widok jest przewijany tylko do połowy pola tekstowego. Wypróbowałem wszystkie powyższe rozwiązanie. Mam ten sam problem. Proszę pomóż!!!
Sona

@Zeona, wypróbuj prostą aplikację, być może robisz coś innego. Spróbuj również usunąć „- (UIWindow.keyWindow? .SafeAreaInsets.bottom ?? 0)”, jeśli używasz bezpiecznego obszaru.
8suhas

usunięte (UIWindow.keyWindow? .safeAreaInsets.bottom ?? 0) to pojawia się biała spacja nad klawiaturą
Sona

1

Obchodzenie TabView„s

Podoba mi się odpowiedź Benjamina Kindle'a, ale nie obsługuje ona TabViews. Oto moje dostosowanie do jego kodu do obsługi TabViews:

  1. Dodaj rozszerzenie do, UITabViewaby przechowywać rozmiar tabView, gdy jego ramka jest ustawiona. Możemy to zapisać w zmiennej statycznej, ponieważ w projekcie jest zwykle tylko jeden widok tabView (jeśli twój ma więcej niż jeden, musisz dostosować).
extension UITabBar {

    static var size: CGSize = .zero

    open override var frame: CGRect {
        get {
            super.frame
        } set {
            UITabBar.size = newValue.size
            super.frame = newValue
        }
    }
}
  1. Będziesz musiał zmienić jego onReceiveu dołu KeyboardHostwidoku, aby uwzględnić wysokość paska kart:
.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in
            self.keyboardHeight = max(height - UITabBar.size.height, 0)
        }
  1. I to wszystko! Super proste 🎉.

1

Przyjąłem zupełnie inne podejście, rozszerzając UIHostingControlleri dostosowując jego 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()
    }
}

Następnie zmień, SceneDelegateaby użyć MyHostingControllerzamiast UIHostingController.

Kiedy to zrobisz, nie muszę się martwić o klawiaturę w moim kodzie SwiftUI.

(Uwaga: nie wykorzystałem tego jeszcze wystarczająco, aby w pełni zrozumieć wszelkie konsekwencje zrobienia tego!)


1

W ten sposób radzę sobie z klawiaturą w SwiftUI. Należy pamiętać, że wykonuje obliczenia na VStack, do którego jest dołączony.

Używasz go w Widoku jako modyfikatora. Tą drogą:

struct LogInView: View {

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

Aby dojść do tego modyfikatora, najpierw utwórz rozszerzenie UIResponder, aby uzyskać wybraną pozycję TextField w VStack:

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

Możesz teraz utworzyć modyfikator klawiatury za pomocą opcji Połącz, aby uniknąć ukrywania pola tekstowego przez klawiaturę:

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

Jeśli chodzi o iOS 14 (beta 4), działa to dość prosto:

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

Rozmiar widoku dostosowuje się do górnej części klawiatury. Z pewnością jest więcej możliwości udoskonalenia ramy (.maxHeight: ...) itd. Zrozumiesz to.

Niestety pływająca klawiatura na iPadzie nadal powoduje problemy podczas przenoszenia. Ale powyższe rozwiązania też by to zrobiły i nadal jest to beta, mam nadzieję, że to zrozumieją.

Dzięki Apple, wreszcie!


To w ogóle nie działa (14.1). Jaki jest pomysł?
Burgler-dev

0

Mój widok:

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

Moja maszyna wirtualna:

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

Zasadniczo zarządzanie dolną wyściółką na podstawie wysokości klawiatury.


-3

Najbardziej elegancka odpowiedź, jaką udało mi się uzyskać, jest podobna do rozwiązania Rafaela. Utwórz klasę do nasłuchiwania zdarzeń związanych z klawiaturą. Jednak zamiast używać rozmiaru klawiatury do modyfikowania dopełnienia, zwróć ujemną wartość rozmiaru klawiatury i użyj modyfikatora .offset (y :), aby dostosować przesunięcie najbardziej zewnętrznych kontenerów widoku. Animuje się wystarczająco dobrze i działa z każdym widokiem.


Jak udało ci się to ożywić? Mam .offset(y: withAnimation { -keyboard.currentHeight }), ale treść skacze zamiast animacji.
jjatie

To było kilka wersji beta temu, kiedy spasowałem z tym kodem, ale w czasie mojego wcześniejszego komentarza, modyfikowanie offsetu stosu vstack podczas działania było wszystkim, czego wymagałem, SwiftUI animowałby zmianę za Ciebie.
pcallycat
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.