Mam siedem TextField
w moim głównym ContentView
. Gdy użytkownik otwiera klawiaturę, niektóre z nich TextField
są ukryte pod ramką klawiatury. Więc chcę przesunąć wszystko TextField
odpowiednio w górę, gdy pojawi się klawiatura.
Użyłem poniższego kodu, aby dodać TextField
na 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:
Odpowiedzi:
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)
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)
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) } } } }
źródło
GeometryGetter
jako modyfikator widoku niż tło, dostosowując je doViewModifier
protokołu?.modifier(GeometryGetter(rect: $kGuardian.rects[1]))
zamiast.background(GeometryGetter(rect: $kGuardian.rects[1]))
. Niewielka różnica (tylko 2 znaki mniej).geometry.frame
wyprowadzam się zDispatchQueue.main.async
pomocy SIGNAL ABORT, teraz przetestuję Twoje rozwiązanie. Aktualizacja:if geometry.size.width > 0 && geometry.size.height > 0
przed przypisaniemself.rect
pomogła.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
currentHeight
wyzwoli 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.źródło
.animation(.easeOut(duration: 0.16))
aby spróbować dopasować prędkość przesuwania się klawiatury.keyboardFrameEndUserInfoKey
. To powinno zawierać ostatnią klatkę klawiatury.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()) } }
źródło
Thread 1: signal SIGABRT
on-line,rect.height - geometry.safeAreaInsets.bottom
kiedy po raz drugi przechodzę do widoku z klawiaturą i klikamTextField
. Nie ma znaczenia, czy kliknęTextField
pierwszy raz, czy nie. Aplikacja nadal się zawiesza.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
view
zamiast umieszczać go w VStack z prostokątem.źródło
self.view
i działa świetnie. Żadnych problemów z animacjąvar body: some View { VStack { view .padding(.bottom, keyboardHeight) .animation(.default) } .onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = height } }
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:
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()) } }
źródło
Xcode 12 - kod jednowierszowy
Dodaj ten modyfikator do
TextField
Jabłko dodaje klawiaturę jako region dla bezpiecznej okolicy, więc można go używać do poruszania któregokolwiek
View
z klawiaturą jak w innych regionach.źródło
View
, w tym naTextEditor
..ignoresSafeArea(.keyboard)
do swojego widoku.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
źródło
IQKeyboardManager.shared.keyboardDistanceFromTextField
do 40, aby uzyskać wygodną lukę.IQKeyboardManager.shared.enable = true
aby 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.Musisz dodać
ScrollView
i ustawić dolne wypełnienie rozmiaru klawiatury, aby zawartość mogła być przewijana, gdy pojawi się klawiatura.Aby uzyskać rozmiar klawiatury, musisz
NotificationCenter
zarejestrować 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 } }
BindableObject
Zgodności pozwoli Ci korzystać z tej klasy jakState
i wywołać aktualizację widoku. W razie potrzeby zapoznaj się z samouczkiem dotyczącymBindableObject
: samouczka SwiftUIKiedy to otrzymasz, musisz skonfigurować a,
ScrollView
aby zmniejszyć jego rozmiar, gdy pojawi się klawiatura. Dla wygody zawarłem toScrollView
w 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ż
padding
do dostosowania rozmiaru widoku przewijania.źródło
BindableObject
wycofaniu niestety to już nie działa.BindableObject
został właśnie przemianowany naObservableObject
ididChange
naobjectWillChange
. Obiekt aktualizuje widok dobrze (chociaż testowałem przy użyciu@ObservedObject
zamiast@State
)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()) } }
źródło
Jako punkt wyjścia użyłem odpowiedzi Benjamina Kindle'a, ale miałem kilka problemów, którymi chciałem się zająć.
keyboardWillChangeFrameNotification
do listy przetworzonych powiadomień adresuje to.init
funkcję, która akceptuje a,@ViewBuilder
dzięki czemu można używaćKeyboardHost
widoku 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 doinit
.Rectangle
wyściółkę na dolne wypełnienie.UIWindow
asUIWindow.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")) } } }
źródło
Keyboard
wysokośćPridictive
w iOSkeyboard
.Settings
->General
->Keyboard
->Pridictive
. w tym przypadku nie koryguje obliczeń i dodaje dopełnienie do klawiaturykeyboardHeight
. 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
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,
KeyboardResponder
jak 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) } }
źródło
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.rawValue
jest 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.
źródło
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 class
obecnie 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.źródło
Nie jestem pewien, czy interfejs API przejścia / animacji dla SwiftUI jest kompletny, ale można go użyć
CGAffineTransform
z.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())
źródło
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,
.keyboard
jeś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.źródło
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) } }
źródło
NotificationCenter.default.addObserver
... musisz je przechowywać i usuwać obserwatorów w odpowiednim czasie ...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/
źródło
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 } }
źródło
Obchodzenie
TabView
„sPodoba mi się odpowiedź Benjamina Kindle'a, ale nie obsługuje ona TabViews. Oto moje dostosowanie do jego kodu do obsługi TabViews:
UITabView
aby 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 } } }
onReceive
u dołuKeyboardHost
widoku, aby uwzględnić wysokość paska kart:.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = max(height - UITabBar.size.height, 0) }
źródło
Przyjąłem zupełnie inne podejście, rozszerzając
UIHostingController
i dostosowując jegoadditionalSafeAreaInsets
: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ń,
SceneDelegate
aby użyćMyHostingController
zamiastUIHostingController
.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!)
źródło
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() } } } }
źródło
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!
źródło
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.
źródło
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.
źródło
.offset(y: withAnimation { -keyboard.currentHeight })
, ale treść skacze zamiast animacji.