У меня TextField
в main ContentView
. Когда пользователь открывает клавиатуру, некоторые из TextField
них скрываются под рамкой клавиатуры. Поэтому я хочу двигаться TextField
вверх, когда появится клавиатура.
Я использовал приведенный ниже код, чтобы добавить его TextField
на экран.
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"))
}
}
}
Выход:
Ответы:
Код обновлен для Xcode, beta 7.
Для этого вам не нужны отступы, ScrollViews или списки. Хотя это решение тоже подойдет им. Я привожу здесь два примера.
Первый перемещает все textField вверх, если для любого из них появляется клавиатура. Но только при необходимости. Если клавиатура не скрывает текстовые поля, они не будут перемещаться.
Во втором примере представление перемещается ровно настолько, чтобы не скрывать активное текстовое поле.
В обоих примерах используется один и тот же общий код, который находится в конце: GeometryGetter и KeyboardGuardian.
Первый пример (показать все текстовые поля)
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)) } }
Второй пример (показать только активное поле)
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
Это представление, которое поглощает размер и положение своего родительского представления. Для этого он вызывается внутри модификатора .background. Это очень мощный модификатор, а не просто способ украсить фон вида. При передаче представления в .background (MyView ()) MyView получает измененное представление в качестве родителя. Использование GeometryReader - это то, что позволяет представлению знать геометрию родительского объекта.
Например:
Text("hello").background(GeometryGetter(rect: $bounds))
будет заполнять границы переменных размером и положением текстового представления и с использованием глобального координатного пространства.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) } } } }
Обновление Я добавил DispatchQueue.main.async, чтобы избежать возможности изменения состояния представления во время его визуализации. ***
КлавиатураGuardian
Назначение KeyboardGuardian - отслеживать события отображения / скрытия клавиатуры и вычислять, сколько места необходимо сместить.
Обновление: я изменил KeyboardGuardian, чтобы обновить слайд, когда пользователь переходит из одного поля в другое.
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) } } } }
источник
GeometryGetter
в качестве модификатора представления вместо фона, сделав его совместимым сViewModifier
протоколом?.modifier(GeometryGetter(rect: $kGuardian.rects[1]))
вместо.background(GeometryGetter(rect: $kGuardian.rects[1]))
. Небольшая разница (всего на 2 символа меньше).geometry.frame
из системыDispatchQueue.main.async
помог с SIGNAL ABORT, теперь проверим ваше решение. Обновление:if geometry.size.width > 0 && geometry.size.height > 0
до назначенияself.rect
помогло.Чтобы построить на основе решения @rraphael, я преобразовал его для использования с сегодняшней поддержкой xcode11 swiftUI.
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 } }
Применение:
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)) } }
Опубликованное
currentHeight
приведет к повторному рендерингу пользовательского интерфейса и перемещению TextField вверх при отображении клавиатуры и обратно вниз при закрытии. Однако я не использовал ScrollView.источник
.animation(.easeOut(duration: 0.16))
чтобы попытаться соответствовать скорости движения клавиатуры вверх.keyboardFrameEndUserInfoKey
. Это должно содержать последний кадр для клавиатуры.Я пробовал многие из предложенных решений, и хотя они в большинстве случаев работают, у меня были некоторые проблемы - в основном с безопасной областью (у меня есть форма внутри вкладки TabView).
В итоге я объединил несколько разных решений и использовал GeometryReader, чтобы получить нижнюю вставку безопасной области конкретного вида и использовать ее в расчете отступов:
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)) }) } } }
Применение:
struct MyView: View { var body: some View { Form {...} .modifier(AdaptsToKeyboard()) } }
источник
Thread 1: signal SIGABRT
в сети,rect.height - geometry.safeAreaInsets.bottom
когда перехожу к просмотру с клавиатуры во второй раз и нажимаюTextField
. Неважно, нажимаю яTextField
первый раз или нет. Приложение по-прежнему вылетает.Я создал представление, которое может обернуть любое другое представление, чтобы сжать его при появлении клавиатуры.
Все очень просто. Мы создаем издателей для событий отображения / скрытия клавиатуры, а затем подписываемся на них с помощью
onReceive
. Мы используем результат для создания прямоугольника размером с клавиатуру позади клавиатуры.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 } } }
Затем вы можете использовать это представление следующим образом:
var body: some View { KeyboardHost { viewIncludingKeyboard() } }
Чтобы переместить содержимое представления вверх, а не сжимать его, можно добавить заполнение или смещение,
view
а не помещать его в VStack с прямоугольником.источник
self.view
и он отлично работает. Никаких проблем с анимациейvar body: some View { VStack { view .padding(.bottom, keyboardHeight) .animation(.default) } .onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = height } }
Я создал действительно простой в использовании модификатор вида.
Добавьте файл Swift с приведенным ниже кодом и просто добавьте этот модификатор в свои представления:
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()) } }
источник
Или вы можете просто использовать IQKeyBoardManagerSwift
и при желании можно добавить это в делегат вашего приложения, чтобы скрыть панель инструментов и включить скрытие клавиатуры при щелчке на любом представлении, кроме клавиатуры.
IQKeyboardManager.shared.enableAutoToolbar = false IQKeyboardManager.shared.shouldShowToolbarPlaceholder = false IQKeyboardManager.shared.shouldResignOnTouchOutside = true IQKeyboardManager.shared.previousNextDisplayMode = .alwaysHide
источник
IQKeyboardManager.shared.keyboardDistanceFromTextField
до 40, чтобы получить удобный зазор.IQKeyboardManager.shared.enable = true
чтобы клавиатура не скрывала мои текстовые поля. В любом случае это лучшее решение. У меня есть 4 поля, расположенных вертикально, и другие решения будут работать для моего самого нижнего поля, но вытеснят самое верхнее из поля зрения.Вам нужно добавить
ScrollView
и установить нижний отступ в размере клавиатуры, чтобы содержимое могло прокручиваться при появлении клавиатуры.Чтобы получить размер клавиатуры, вам нужно будет использовать
NotificationCenter
для регистрации для события клавиатуры. Для этого вы можете использовать собственный класс: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
позволит вам использовать этот класс какState
и запускать обновление представления. При необходимости посмотрите учебное пособиеBindableObject
: Учебник по SwiftUIКогда вы получите это, вам нужно настроить,
ScrollView
чтобы уменьшить его размер при появлении клавиатуры. Для удобства я обернул этоScrollView
в какой-то компонент: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) } }
Все, что вам нужно сделать сейчас, это встроить ваш контент в custom
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) } }
Изменить: поведение прокрутки действительно странное, когда клавиатура скрывается. Возможно, это исправит использование анимации для обновления заполнения, или вам следует подумать об использовании чего-то другого, кроме как
padding
для настройки размера прокрутки.источник
BindableObject
устаревшим, к сожалению, это больше не работает.BindableObject
его просто переименовали вObservableObject
иdidChange
вobjectWillChange
. Объект обновляет представление просто отлично (хотя я тестировал использование@ObservedObject
вместо@State
)Xcode 12 - однострочный код
Добавьте этот модификатор в
TextField
Apple добавила клавиатуру в качестве области для безопасной области, поэтому вы можете использовать ее для перемещения любого
View
с помощью клавиатуры, как и другие области.источник
View
, в том числеTextEditor
..ignoresSafeArea(.keyboard)
к вашему просмотру.Я просмотрел и преобразовал существующие решения в удобный пакет SPM, который предоставляет
.keyboardAware()
модификатор:КлавиатураAwareSwiftUI
Пример:
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") } } }
Источник:
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()) } }
источник
Я использовал ответ Бенджамина Киндла в качестве отправной точки, но у меня было несколько проблем, которые я хотел решить.
keyboardWillChangeFrameNotification
Этим решается добавление в список обработанных уведомлений.init
функцию, которая принимает a,@ViewBuilder
чтобы вы могли использоватьKeyboardHost
представление, как любое другое представление, и просто передавать свой контент в завершающем замыкании, а не передавать представление содержимого в качестве параметраinit
.Rectangle
для регулировки нижнего отступа.UIWindow
какUIWindow.keyboardFrameEndUserInfoKey
.Собрав все это вместе, у меня есть:
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")) } } }
источник
Keyboard
высотуPridictive
в iOSkeyboard
.Settings
->General
->Keyboard
->Pridictive
. в этом случае он не исправляет calclate и добавляет отступ на клавиатуреkeyboardHeight
. На моем iPod Touch (в портретной ориентации) клавиатура с предиктивным включением составляет 254 балла. Без него 216 очков. Я даже могу отключить прогнозирование с экранной клавиатурой, и заполнение обновится правильно. Добавление клавиатуры с интеллектуальнымReceived UIKeyboardWillChangeFrameNotification with height 254.0
Received UIKeyboardWillShowNotification with height 254.0
Received UIKeyboardWillChangeFrameNotification with height 216.0
Это адаптировано из того, что построил @kontiki. У меня он работает в приложении под бета-версией 8 / GM seed, где поле, требующее прокрутки, является частью формы внутри NavigationView. Вот 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 } } }
Затем я использовал перечисление для отслеживания слотов в массиве rects и общего количества:
enum KeyboardSlots: Int { case kLogPath case kLogThreshold case kDisplayClip case kPingInterval case count }
KeyboardSlots.count.rawValue
- необходимая емкость массива; другие, как rawValue, дают соответствующий индекс, который вы будете использовать для вызовов .background (GeometryGetter).После такой настройки представления получают доступ к KeyboardGuardian следующим образом:
@ObservedObject private var kGuardian = KeyboardGuardian(textFieldCount: SettingsFormBody.KeyboardSlots.count.rawValue)
Фактическое движение выглядит так:
.offset(y: kGuardian.slide).animation(.easeInOut(duration: 1))
прикреплен к представлению. В моем случае он прикреплен ко всему NavigationView, поэтому вся сборка выдвигается вверх по мере появления клавиатуры.
Я не решил проблему получения панели инструментов Done или клавиши возврата на десятичной клавиатуре с помощью SwiftUI, поэтому вместо этого я использую это, чтобы скрыть его при нажатии в другом месте:
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) } } }
Вы прикрепляете его к представлению как
.modifier(DismissingKeyboard())
Некоторым представлениям (например, сборщикам) не нравится прикрепление этого модификатора, поэтому вам может потребоваться более детальная детализация того, как вы прикрепляете модификатор, а не просто накладывать его на крайнее представление.
Большое спасибо @kontiki за упорный труд. Вам все равно понадобится его GeometryGetter, указанный выше (нет, я тоже не работал, чтобы преобразовать его для использования предпочтений), как он показывает в своих примерах.
источник
Некоторые из вышеперечисленных решений имели некоторые проблемы и не обязательно были «самым чистым» подходом. Из-за этого я изменил несколько вещей для реализации ниже.
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() } }
Мне бы хотелось более четкого подхода и перенести ответственность на сконструированное представление (а не на модификатор) в том, как смещать контент, но, похоже, я не мог заставить издателей правильно запускать при перемещении кода смещения в представление. ...
Также обратите внимание, что в этом экземпляре должны были использоваться Publishers, так как в
final class
настоящее время возникают сбои с неизвестными исключениями (даже если они соответствуют требованиям интерфейса), а ScrollView в целом является лучшим подходом при применении кода смещения.источник
Честно говоря, многие из этих ответов кажутся действительно раздутыми. Если вы используете SwiftUI, вы также можете использовать Combine.
Создайте,
KeyboardResponder
как показано ниже, затем вы можете использовать, как показано ранее.Обновлено для 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) } }
источник
Я не уверен, что API перехода / анимации для SwiftUI завершен, но вы можете использовать
CGAffineTransform
с.transformEffect
Создайте наблюдаемый объект клавиатуры с опубликованным свойством, например:
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 } }
то вы можете использовать это свойство, чтобы изменить вид следующим образом:
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 } } } }
или проще
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())
источник
Xcode 12 beta 4 добавляет новый модификатор вида,
ignoresSafeArea
который теперь можно использовать, чтобы избежать клавиатуры..ignoresSafeArea([], edges: [])
Это позволяет избежать клавиатуры и всех краев безопасной зоны. Вы можете установить для первого параметра значение,
.keyboard
если не хотите, чтобы его избегали. В этом есть некоторые причуды, по крайней мере, в моей настройке иерархии представлений, но похоже, что именно так Apple хочет, чтобы мы избегали клавиатуры.источник
Ответ скопирован отсюда: TextField всегда сверху клавиатуры с SwiftUI
Я пробовал разные подходы, и ни один из них не помог мне. Этот ниже единственный, который работал на разных устройствах.
Добавьте это расширение в файл:
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 } } } }
На ваш взгляд, вам нужна переменная для привязки offsetValue:
struct IncomeView: View { @State private var offsetValue: CGFloat = 0.0 var body: some View { VStack { //... } .keyboardSensible($offsetValue) } }
источник
NotificationCenter.default.addObserver
... вам нужно сохранить их и удалить наблюдателей в подходящее время ...Как отметили Марк Кренек и Хейко, Apple, похоже, решила наконец-то эту проблему в Xcode 12 beta 4. Дела идут быстро. Согласно примечаниям к выпуску Xcode 12 beta 5, опубликованным 18 августа 2020 г., «Form, List и TextEditor больше не скрывают содержимое за клавиатурой. (66172025)». Я просто загрузил его и быстро протестировал в симуляторе бета 5 (iPhone SE2) с контейнером формы в приложении, которое я запустил несколько дней назад.
Теперь он «просто работает» для TextField . SwiftUI автоматически предоставит соответствующее нижнее заполнение инкапсулирующей форме, чтобы освободить место для клавиатуры. И он автоматически прокручивает форму вверх, чтобы отобразить текстовое поле прямо над клавиатурой. Контейнер ScrollView теперь хорошо ведет себя и при открытии клавиатуры.
Однако, как отметил Андрей Первушин в комментарии, есть проблема с TextEditor . Beta 5 и 6 автоматически предоставят соответствующее нижнее заполнение инкапсулирующей форме, чтобы освободить место для клавиатуры. Но он НЕ будет автоматически прокручивать форму вверх. Клавиатура накроет TextEditor. Итак, в отличие от TextField, пользователь должен прокручивать форму, чтобы сделать TextEditor видимым. Я отправлю отчет об ошибке. Возможно, Beta 7 это исправит. Так близко …
https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-14-beta-release-notes/
источник
Применение:
import SwiftUI var body: some View { ScrollView { VStack { /* TextField() */ } }.keyboardSpace() }
Код:
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 } }
источник
Обработка
TabView
«ю.ш.Мне нравится ответ Бенджамина Киндла, но он не поддерживает TabViews. Вот моя корректировка его кода для обработки TabView:
UITabView
сохранить размер tabView, когда он установлен. Мы можем сохранить это в статической переменной, потому что обычно в проекте есть только один tabView (если у вас их несколько, вам нужно будет настроить).extension UITabBar { static var size: CGSize = .zero open override var frame: CGRect { get { super.frame } set { UITabBar.size = newValue.size super.frame = newValue } } }
onReceive
в нижней частиKeyboardHost
представления, чтобы учесть высоту панели вкладок:.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = max(height - UITabBar.size.height, 0) }
источник
Я использовал совершенно другой подход, расширив
UIHostingController
и изменив его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() } }
Затем измените
SceneDelegate
на использованиеMyHostingController
вместоUIHostingController
.Когда это будет сделано, мне не нужно беспокоиться о клавиатуре внутри моего кода SwiftUI.
(Примечание: я еще не использовал это достаточно, чтобы полностью понять любые последствия этого!)
источник
Именно так я работаю с клавиатурой в SwiftUI. Следует помнить, что он выполняет вычисления в VStack, к которому он прикреплен.
Вы используете его в представлении как модификатор. Сюда:
struct LogInView: View { var body: some View { VStack { // Your View } .modifier(KeyboardModifier()) } }
Итак, чтобы перейти к этому модификатору, сначала создайте расширение UIResponder, чтобы получить выбранную позицию TextField в 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) } }
Теперь вы можете создать KeyboardModifier с помощью Combine, чтобы клавиатура не скрывала TextField:
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() } } } }
источник
Что касается iOS 14 (beta 4), все работает довольно просто:
var body: some View { VStack { TextField(...) } .padding(.bottom, 0) }
А размер представления подстраивается под верхнюю часть клавиатуры. Конечно, есть больше возможностей для уточнения с frame (.maxHeight: ...) и т. Д. Вы разберетесь с этим.
К сожалению, плавающая клавиатура на iPad по-прежнему вызывает проблемы при перемещении. Но вышеупомянутые решения тоже будут, и это все еще бета, я надеюсь, они разберутся.
Спасибо, Apple, наконец!
источник
Мой вид:
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) } } }
Моя ВМ:
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 } }
По сути, управление нижним отступом в зависимости от высоты клавиатуры.
источник
Самый элегантный ответ, который мне удалось, похож на решение Ррафаэля. Создайте класс для прослушивания событий клавиатуры. Вместо того, чтобы использовать размер клавиатуры для изменения отступов, верните отрицательное значение размера клавиатуры и используйте модификатор .offset (y :), чтобы настроить смещение самого внешнего контейнера представления. Он достаточно хорошо анимирует и работает с любым видом.
источник
.offset(y: withAnimation { -keyboard.currentHeight })
, но контент прыгает, а не оживляет.