Переместить TextField вверх, когда клавиатура появилась с помощью SwiftUI? : iOS
У меня есть семь TextField
внутри моего основного 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"))
}
}
}
Выход:
Ответы
Ответ 1
Код обновлен для Xcode, бета-версия 7.
Вам не нужно заполнение, ScrollViews или списки для достижения этой цели. Хотя это решение будет хорошо с ними играть. Я привожу здесь два примера.
Первый перемещает все текстовое поле вверх, если для любого из них появляется клавиатура. Но только при необходимости. Если клавиатура не скрывает текстовые поля, они не будут двигаться.
Во втором примере представление только перемещается достаточно, чтобы избежать скрытия активного текстового поля.
Оба примера используют один и тот же общий код, найденный в конце: 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, чтобы избежать возможности изменения состояния представления во время его визуализации. ***
KeyboardGuardian
Цель 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)
}
}
}
}
Ответ 2
Я создал вид, который может обернуть любой другой вид, чтобы уменьшить его при появлении клавиатуры.
Это довольно просто. Мы создаем издателей для событий отображения/скрытия клавиатуры, а затем подписываемся на них, используя onReceive
. Мы используем результат этого для создания прямоangularьника размером с клавиатуру за клавиатурой.
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 с прямоangularьником.
Ответ 3
Вам необходимо добавить 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)
}
}
Все, что вам нужно сделать сейчас, это встроить ваш контент в пользовательский 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)
}
}
Edit:
Поведение прокрутки действительно странно, когда клавиатура прячется. Возможно, использование анимации для обновления отступов исправит это, или вам следует рассмотреть возможность использования чего-то другого, кроме padding
, для настройки размера представления прокрутки.
Ответ 4
Чтобы создать решение @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.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
currentHeight = max(keyboardSize.height, 340.0)
}
}
@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)
}
}
Опубликованный currentHeight
вызовет повторный рендеринг пользовательского интерфейса и переместит ваш TextField вверх, когда клавиатура отобразится, и вернется вниз, когда закроется. Однако я не использовал ScrollView.
Ответ 5
В качестве отправной точки я использовал ответ бенджамина Киндла, но у меня было несколько вопросов, которые я хотел решить.
- Большинство ответов здесь не касаются смены рамки клавиатуры, поэтому они ломаются, если пользователь поворачивает устройство с помощью клавиатуры на экране. Добавление
keyboardWillChangeFrameNotification
в список обработанных уведомлений адресовывает это.
- Я не хотел, чтобы несколько издателей имели похожие, но разные закрытия карт, поэтому я связал все три клавиатурных уведомления в одном издателе. По общему признанию, это длинная цепь, но каждый шаг довольно прост.
- Я предоставил функцию
init
, которая принимает @ViewBuilder
, так что вы можете использовать представление KeyboardHost
, как и любой другой вид, и просто передавать свой контент в конце замыкания, в отличие от передачи представления контента в качестве параметра для init
.
- Как предложили Tae и fdelafuente в комментариях, я поменял
Rectangle
для настройки нижнего отступа.
- Вместо использования жестко закодированной строки "UIKeyboardFrameEndUserInfoKey" я хотел использовать строки, представленные в
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"))
}
}
}
Ответ 6
Самый элегантный ответ, который мне удалось найти, похож на решение Рафаэля. Создайте класс для прослушивания событий клавиатуры. Вместо того чтобы использовать размер клавиатуры для изменения отступов, верните отрицательное значение размера клавиатуры и используйте модификатор .offset(y :), чтобы отрегулировать внешнее смещение большинства контейнеров вида. Он оживляет достаточно хорошо и работает с любым видом.
Ответ 7
Решение Kontiki очень приятно. Тем не менее, кажется, прокрутить кадр вниз в Xcode 11 бета 4.
Ответ 8
Это адаптировано из того, что @kontiki построил. У меня он работает в приложении под бета 8/GM seed, где нуждающаяся в прокрутке область является частью формы внутри NavigationView. Здесь KeyboardGuardian:
//
// KeyboardGuardian.swift
//
// https://stackoverflow.com/info/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
}
}
}
Затем я использовал enum для отслеживания слотов в массиве 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, поэтому вся сборка сдвигается вверх по мере появления клавиатуры.
Я не решил проблему получения панели инструментов "Готово" или клавиши возврата на десятичной клавиатуре с помощью 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 выше (нет, я не выполнил работу по его преобразованию в настройки), как он иллюстрирует в своих примерах.