Перемістіть TextField вгору, коли клавіатура з’явиться в SwiftUI


100

У мене 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"))
            }
    }
}

Вихід:

Вихідні дані


Ви можете використовувати ScrollView. developer.apple.com/documentation/swiftui/scrollview
Прашант Тукадія,

1
@PrashantTukadiya Дякую за швидку відповідь. Я додав TextField у Scrollview, але все ще стикаюся з тією ж проблемою.
Hitesh Сурани

1
@DimaPaliychuk Це не спрацює. це SwiftUI
Прашант Тукадія

20
Показ клавіатури та затемнення вмісту на екрані відбувається приблизно з того, що було першим додатком Objective C для iPhone? Це проблема, яка постійно вирішується. Я, насамперед, розчарований тим, що Apple не зверталася до цього за допомогою SwiftUi. Я знаю, що цей коментар нікому не корисний, але я хотів підняти це питання про те, що ми дійсно повинні тиснути на Apple, щоб запропонувати рішення, а не покладатися на співтовариство, яке завжди пропонує цю найпоширенішу проблему.
P.

1
Існує дуже хороша стаття Вадима vadimbulavin.com / ...
Sudara

Відповіді:


64

Код оновлений для Xcode, бета 7.

Для цього вам не потрібні відступи, прокрутки або списки. Хоча це рішення теж буде приємно зіграти з ними. Я включаю тут два приклади.

Перший переміщує весь textField вгору, якщо для будь-якого з них з’являється клавіатура. Але лише за потреби. Якщо клавіатура не приховує текстових полів, вони не рухатимуться.

У другому прикладі подання рухається лише настільки, щоб уникнути приховування активного текстового поля.

В обох прикладах використовується однаковий загальний код, знайдений наприкінці: GeometryGetter та KeyboardGuardian

Перший приклад (показати всі текстові поля)

Коли клавіатуру відкрито, 3 текстові поля переміщуються вгору, щоб усі видимі були

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

}

Геометрія

Це подання, яке поглинає розмір і положення батьківського подання. Для цього його викликають всередині модифікатора .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)
            }

        }
    }
}

1
Чи можна прикріпити GeometryGetterяк модифікатор подання, ніж фон, зробивши його відповідним ViewModifierпротоколу?
Sudara

2
Це можливо, але який виграш? Ви б прикріпили його так: .modifier(GeometryGetter(rect: $kGuardian.rects[1]))замість .background(GeometryGetter(rect: $kGuardian.rects[1])). Не велика різниця (лише на 2 символи менше).
Kontiki

3
У деяких ситуаціях ви можете отримати ПЕРЕМІНЕННЯ СИГНАЛА від програми всередині GeometryGetter при призначенні нового прямокутника, якщо ви відходите від цього екрана. Якщо це трапиться з вами, просто додайте код, щоб переконатися, що розмір геометрії перевищує нуль (geometry.size.width> 0 && geometry.size.height> 0), перш ніж присвоювати значення self.rect
Julio Bailon

1
@JulioBailon Я не знаю чому, але, переїхавши geometry.frameза DispatchQueue.main.asyncдопомогою SIGNAL ABORT, зараз перевіримо ваше рішення. Оновлення: if geometry.size.width > 0 && geometry.size.height > 0перед призначенням self.rectдопомогло.
Роман Васильєв

2
це перерви для мене, а на self.rect = geometry.frame (в: .global) отримання SIGNAL ABORT і випробували всі запропоновані рішення для усунення цієї помилки
Марван Roushdy

55

Щоб побудувати рішення @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.


6
Ця відповідь мені подобається простотою. Я додав, .animation(.easeOut(duration: 0.16))щоб спробувати відповідати швидкості сповзання клавіатури.
Марк Мойкенс,

Чому ви встановили максимальну висоту клавіатури 340?
Даніель Райан,

1
@DanielRyan Іноді висота клавіатури повертала неправильні значення в симуляторі. На даний момент я не можу знайти спосіб визначити проблему
Майкл Ніас,

1
Я сам цього питання не бачив. Можливо, це виправлено в останніх версіях. Я не хотів блокувати розмір на випадок, якщо є (або будуть) більші клавіатури.
Даніель Райан,

1
Можна спробувати keyboardFrameEndUserInfoKey. Це повинно містити заключний кадр для клавіатури.
Матіас

50

Я спробував багато із запропонованих рішень, і, хоча вони працюють у більшості випадків, у мене були деякі проблеми - в основному з безпечною зоною (у мене є форма всередині вкладки 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())
    }
}

7
Ого, це найвідоміша версія SwiftUI з усіх, з GeometryReader та ViewModifier. Любіть це.
Матеуш

3
Це так корисно та елегантно. Щиро дякую, що написали це.
Данило Кампос,

2
Я бачу невеликий пустий білий вигляд над моєю клавіатурою. Цей вигляд є GeometryReader View, що підтверджується зміною кольору тла. Будь-яка ідея, чому GeometryReader відображається між моїм фактичним видом та клавіатурою.
user832

6
Я отримую повідомлення про помилку Thread 1: signal SIGABRTв режимі рядка, rect.height - geometry.safeAreaInsets.bottomколи я вдруге переходжу до подання за допомогою клавіатури та натискаю на TextField. Неважливо, натиснув я TextFieldвперше чи ні. Програма все ще аварійно завершує роботу.
JLively

2
Нарешті Щось, що працює! Чому Apple не може цього зробити для нас - це горіхи !!
Дейв Козіковський

35

Я створив подання, яке може обернути будь-який інший вигляд, щоб зменшити його, коли з’явиться клавіатура.

Це досить просто. Ми створюємо видавців для показу / приховування подій на клавіатурі, а потім підписуємось на них за допомогою 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 за допомогою прямокутника.


6
Я думаю, це правильна відповідь. Просто незначний трюк, який я зробив: замість прямокутника я просто модифікую відступ, self.viewі він чудово працює. Немає жодних проблем з анімацією
Тей

5
Дякую! Працює ідеально. Як сказав @Taed, краще використовувати підхід до підбивання. Кінцевим результатом будеvar body: some View { VStack { view .padding(.bottom, keyboardHeight) .animation(.default) } .onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = height } }
fdelafuente

1
Незважаючи на меншу кількість голосів, це найшвидша користувацька відповідь. А попередній підхід, який використовував AnyView, порушує допомогу прискорення металу.
Нельсон Кардачі,

4
Це відмінне рішення, але головна проблема тут полягає в тому, що ви втрачаєте можливість переміщення вгору у вигляді, лише якщо клавіатура приховує текстове поле, яке ви редагуєте. Я маю на увазі: якщо у вас є форма з кількома текстовими полями, і ви починаєте редагувати перше зверху, ви, мабуть, не хочете, щоб він рухався вгору, оскільки він зміститься з екрану.
superpuccio

Мені дуже подобається відповідь, але, як і всі інші відповіді, вона не працює, якщо ваш погляд знаходиться всередині панелі TabBar або View не знаходиться на одному рівні з нижньою частиною екрана.
Бен Патч,

25

Я створив дуже простий у використанні модифікатор подання.

Додайте файл Swift із кодом нижче і просто додайте цей модифікатор у свої подання:

.keyboardResponsive()
import SwiftUI

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

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

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

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


Приємно. Дякую, що поділились.
десятиліття

2
Було б круто, якби це лише компенсувало, якщо це необхідно (тобто не прокручувати, якщо клавіатура не охоплює елемент введення). Приємно мати ...
десятиліття

Це чудово працює, дякую. Дуже чиста реалізація, а для мене - лише сувої, якщо потрібно.
Джошуа

Чудово! Чому ви не надаєте це на Github чи деінде? :) Або ви можете запропонувати це github.com/hackiftekhar/IQKeyboardManager, оскільки вони ще не мають повної підтримки
SwiftUI

Не буде добре грати зі змінами орієнтації і компенсуватиметься незалежно від того, потрібно це чи ні.
GrandSteph

16

Або ви можете просто використовувати IQKeyBoardManagerSwift

і може за бажанням додати це до делегата програми, щоб приховати панель інструментів і увімкнути приховування клавіатури при натисканні на будь-якому поданні, крім клавіатури.

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

Це справді (несподіваний) шлях і для мене. Тверді.
HelloTimo

Цей фреймворк працював навіть краще, ніж очікувалося. Дякую, що поділився!
Річард Путьє,

1
У мене нормально працює SwiftUI - дякую @DominatorVbN - мені в ландшафтному режимі iPad потрібно було збільшити IQKeyboardManager.shared.keyboardDistanceFromTextFieldдо 40, щоб отримати зручний проміжок.
Річард Гроувс,

Також довелося встановити, IQKeyboardManager.shared.enable = trueщоб клавіатура не ховала мої текстові поля. У будь-якому випадку це найкраще рішення. У мене є 4 поля, розташовані вертикально, і інші рішення працювали б для самого нижнього поля, але виштовхували б найвищі.
MisterEd

12

Вам потрібно додати a 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дозволить вам використовувати цей клас як a Stateта ініціювати оновлення подання. Якщо потрібно, перегляньте посібник для BindableObject: Підручник SwiftUI

Отримавши це, вам потрібно налаштувати a, 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)
    }
}

Редагувати: поведінка прокрутки справді дивна, коли клавіатура ховається. Можливо, використання анімації для оновлення заповнення може це виправити, або вам слід подумати про використання чогось іншого, крім paddingдля налаштування розміру перегляду прокрутки.


Гей, здається, у вас є досвід роботи з binvableobject. Я не можу змусити його працювати як хочу. Було б непогано , якби ви могли оглянути: stackoverflow.com/questions/56500147 / ...
SwiftiSwift

Чому ви не використовуєте @ObjectBinding
SwiftiSwift

3
З BindableObjectзастарілими, це більше не працює, на жаль.
LinusGeffarth

2
@LinusGeffarth За те, що воно того варте, BindableObjectпросто перейменовано на ObservableObjectта didChangeв objectWillChange. Об’єкт чудово оновлює вигляд (хоча я тестував, використовуючи @ObservedObjectзамість цього @State)
SeizeTheDay

Привіт, це рішення прокручує вміст, але воно показує деяку білу область над клавіатурою, яка приховує половину текстового поля. Будь ласка, дайте мені знати, як ми можемо видалити білу область.
Шахбаз Саджад,

12

Xcode 12 - Однорядковий код

Додайте цей модифікатор до TextField

.ignoresSafeArea(.keyboard, edges: .bottom)

Демо

Apple додала клавіатуру як область для безпечної зони, тому ви можете використовувати її для переміщення будь-якоїView клавіатури, як і інші регіони.


це працює для TextField, але як щодо TextEditor?
Андрей Первушин

Він працює на будь-якому View , включаючи TextEditor.
Мойтаба Хоссейні,

3
щойно перевірено, Xcode beta 6, TextEditor не працює
Андрей Первушин,

1
Ви можете задати питання та зв’язати його тут. Тож я можу поглянути на ваш відтворюваний код і подивитися, чи можу я допомогти :) @kyrers
Хоссейні

2
Я сам розібрався. Додайте .ignoresSafeArea(.keyboard)до свого подання.
leonboe1

6

Я переглянув та переробив існуючі рішення на зручний пакет SPM, який надає .keyboardAware()модифікатор:

KeyboardAwareSwiftUI

Приклад:

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

Я просто бачу половину висоти текстового перегляду. ти знаєш, як це вирішити?
Матросов Олександр

Добре. це врятувало мій час. Перш ніж використовувати це, ми знаємо, що цей модифікатор обробляє нижню відступ.
Brownsoo Han

5

Як вихідну точку я використав відповідь Бенджаміна Кіндла, але у мене було кілька питань, на які я хотів звернутися.

  1. Більшість відповідей тут не стосуються зміни клавіатури клавіатури, тому вони ламаються, якщо користувач обертає пристрій за допомогою клавіатури на екрані. Додавання keyboardWillChangeFrameNotificationдо списку оброблених повідомлень адрес.
  2. Я не хотів, щоб кілька видавців мали подібні, але різні замикання карт, тому я об’єднав усі три сповіщення на клавіатурі в одного видавця. Це правда довгий ланцюг, але кожен крок досить простий.
  3. Я надав initфункцію, яка приймає @ViewBuilderтак, що ви можете використовувати KeyboardHostподання, як і будь-який інший вигляд, і просто передавати свій вміст у кінцевому закритті, на відміну від передачі подання вмісту як параметра init.
  4. Як Тае та Фделафуенте запропонували в коментарях, я поміняв текст Rectangle на коригування нижньої прокладки.
  5. Замість того, щоб використовувати жорстко закодований рядок "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"))
        }
    }
}


це рішення не працює, воно збільшує Keyboardвисоту
GSerjo

Чи можете ви детальніше розповісти про проблеми, з якими ви бачите @GSerjo? Я використовую цей код у своєму додатку, і він у мене чудово працює.
Тімоті Сандерс

Не могли б ви увімкнути PridictiveiOS keyboard. Settings-> General-> Keyboard-> Pridictive. в цьому випадку він не виправляє розрахунок і додає відступ до клавіатури
GSerjo

@GSerjo: У мене ввімкнено передбачуваний текст на iPad Touch (7-го покоління), на якому запущена бета-версія iOS 13.1. Він правильно додає відступ для висоти рядка передбачення. (Важливо зазначити, що я тут не регулюю висоту клавіатури , а додаю до відступу самого подання.) Спробуйте поміняти місцями на налагоджувальній карті, яка коментується, і пограйте зі значеннями, які ви отримуєте для передбачення клавіатура. Я опублікую журнал в іншому коментарі.
Тімоті Сандерс

З розкоментованою картою "налагодження" ви можете бачити значення, якому присвоюється keyboardHeight. На моєму iPod Touch (у портретному режимі) клавіатура з увімкненим передбаченням становить 254 точки. Без нього 216 балів. Я навіть можу вимкнути інтелектуальне керування за допомогою клавіатури на екрані та належним чином оновлюючи відступ. Додавання клавіатури з передбачуваним: Received UIKeyboardWillChangeFrameNotification with height 254.0 Received UIKeyboardWillShowNotification with height 254.0 Коли я вимикаю інтелектуальний текст:Received UIKeyboardWillChangeFrameNotification with height 216.0
Тімоті Сандерс

4

Це адаптовано до того, що побудував @kontiki. У мене це працює в додатку під назвою beta 8 / GM, де поле, яке потребує прокрутки, є частиною форми всередині 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, тому вся збірка ковзає вгору, коли з’являється клавіатура.

Я не вирішив проблему отримання панелі інструментів "Готово" або клавіші повернення на десятковій клавіатурі за допомогою 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 вище (ні, я теж не перекладав його на використання налаштувань), як він ілюструє на своїх прикладах.


1
Особі, яка голосувала проти: чому? Я спробував додати щось корисне, тому хотів би знати, як, на ваш погляд, я помилився
Фельдур

4

Деякі з наведених вище рішень мали деякі проблеми, і це не обов’язково був найчистіший підхід. Через це я змінив кілька речей для реалізації нижче.

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 є найкращим підходом при застосуванні коду зміщення.


Дуже приємне рішення, настійно рекомендую! Я додав Bool, щоб вказати, чи на даний момент активна клавіатура.
Peanutsmasher

4

Багато з цих відповідей просто здаються справді роздутими, якщо чесно. Якщо ви використовуєте 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)
    }
}

Я додав .animation (.easeIn), щоб відповідати анімації, з якою з'являється клавіатура
Michiel Pelt

(Для iOS 13 перейдіть до історії цієї відповіді)
Лоріс Фо

Привіт, .assign (to: \ .keyboardHeight) видає цю помилку "Не вдається вивести тип шляху ключа з контексту; розгляньте можливість явного вказівки кореневого типу". Будь ласка, дайте мені знати правильне і чисте рішення як для iOS 13, так і для iOS 14.
Шахбаз Саджад,

3

Я не впевнений, що 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())

Мені подобається ця відповідь, але я не зовсім впевнений, звідки береться "ScreenSize.portrait".
Міша Стоун

Привіт, @MishaStone, спасибі, виберіть мій підхід. ScreenSize.portrait - це клас, який я зробив для отримання вимірювань основи екрану за Орієнтацією та відсотками .... але ви можете замінити його будь-яким значенням, яке потрібно для вашого перекладу
blacktiago

3

Xcode 12 beta 4 додає новий модифікатор подання, ignoresSafeAreaякий тепер можна використовувати, щоб уникнути клавіатури.

.ignoresSafeArea([], edges: [])

Це дозволяє уникнути клавіатури та всіх безпечних країв. Ви можете встановити для першого параметра значення, .keyboardякщо не хочете, щоб його уникали. У цьому є деякі химерності, принаймні, на мій погляд, у налаштуваннях ієрархії поглядів, але, схоже, Apple так хоче, щоб ми уникали клавіатури.


2

Відповідь скопійовано звідси: 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)
  }
}

2
Просто FYI, ви володієте об’єктами під час дзвінка NotificationCenter.default.addObserver... вам потрібно зберегти їх і видалити спостерігачів у відповідний час ...
TheCodingArt

Привіт @TheCodingArt, це правильно, я намагався зробити це так ( oleb.net/blog/2018/01/notificationcenter-removeobserver ), але, здається, це не працює для мене, будь-які ідеї?
Рей

2

Як відзначали Марк Кренек та Хейко, Apple, здавалося, нарешті вирішувала цю проблему в бета-версії Xcode 12. Все рухається швидко. Згідно з примітками до випуску Xcode 12 beta 5, опублікованими 18 серпня 2020 р. "Form, List і TextEditor більше не приховують вміст за клавіатурою. (66172025)". Я просто завантажую його та даю швидкий тест у бета-версії 5 симулятора (iPhone SE2) із контейнером форми у програмі, яку я запустив кілька днів тому.

Зараз він "просто працює" для TextField . SwiftUI автоматично надасть відповідне нижнє заповнення інкапсулюючої форми, щоб звільнити місце для клавіатури. І він автоматично прокрутить форму вгору, щоб відобразити поле TextField трохи вище клавіатури. Контейнер ScrollView тепер поводиться добре, коли також з’являється клавіатура.

Однак, як зазначив у коментарі Андрій Первушин, існує проблема з TextEditor . Бета-версії 5 та 6 автоматично надають відповідні відступи нижньої частини інкапсулюючої форми, щоб звільнити місце для клавіатури. Але це НЕ буде автоматично прокручувати форму вгору. Клавіатура закриє TextEditor. Отже, на відміну від TextField, користувач повинен прокручувати форму, щоб зробити TextEditor видимим. Я подам звіт про помилку. Можливо, Beta 7 це виправить. Дуже близько …

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


я бачу примітки до випуску apple, протестовані на бета5 та бета6, TextField працює, TextEditor НЕ, що мені не вистачає? @State var text = "" var body: деякий Перегляд {Форма {Розділ {Текст (текст) .frame (висота: 500)} Розділ {TextField ("5555", текст: $ text) .frame (висота: 50)} Розділ {TextEditor (текст: $ text) .frame (висота: 120)}}}
Андрей Первушин

2

Використання:

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

спробував своє рішення ... подання прокручується лише до половини текстового поля. Спробував все вищезазначене рішення. Отримав те саме питання. Будь ласка, допоможіть!!!
Сона

@Zeona, спробуй у простому додатку, можливо, ти робиш щось інше. Також спробуйте видалити '- (UIWindow.keyWindow? .SafeAreaInsets.bottom ?? 0)', якщо ви використовуєте безпечну зону.
8suhas

видалено (UIWindow.keyWindow? .safeAreaInsets.bottom ?? 0), тоді я отримую пробіл над клавіатурою
Sona,

1

Обробка TabView«пд.ш.

Мені подобається відповідь Бенджаміна Кіндла, але він не підтримує TabViews. Ось моє пристосування до його коду для роботи з TabViews:

  1. Додайте розширення до, UITabViewщоб зберігати розмір tabView, коли встановлено його фрейм. Ми можемо зберігати це в статичній змінній, оскільки зазвичай у проекті є лише одна вкладкаView (якщо у вас більше одного, то вам потрібно буде налаштувати).
extension UITabBar {

    static var size: CGSize = .zero

    open override var frame: CGRect {
        get {
            super.frame
        } set {
            UITabBar.size = newValue.size
            super.frame = newValue
        }
    }
}
  1. Вам потрібно буде змінити його onReceiveвнизу KeyboardHostподання, щоб врахувати висоту панелі вкладок:
.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in
            self.keyboardHeight = max(height - UITabBar.size.height, 0)
        }
  1. І це все! Супер простий 🎉.

1

Я застосував зовсім інший підхід, розширивши 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.

(Примітка: Я ще не використовував цього достатньо для повного розуміння будь-яких наслідків цього!)


1

Це спосіб роботи з клавіатурою в 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() }
    }
  }
}

1

Що стосується iOS 14 (бета-версія 4), то вона працює досить просто:

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

А розмір вигляду підлаштовується під верх клавіатури. З фреймом (.maxHeight: ...) і т. Д. Можливо більше уточнень тощо. Ви зрозумієте це.

На жаль, плаваюча клавіатура на iPad все ще викликає проблеми при переміщенні. Але вищезазначені рішення теж, і це все ще бета-версія, я сподіваюся, вони це зрозуміють.

Thx Apple, нарешті!


Це взагалі не працює (14.1). Яка ідея?
Burgler-dev

0

Мій погляд:

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

В основному, керування нижньою прокладкою залежно від висоти клавіатури.


-3

Найелегантніша відповідь, яку мені вдалося досягти, схожа на рішення ррафаеля. Створіть клас для прослуховування подій на клавіатурі. Замість того, щоб використовувати розмір клавіатури для модифікації заповнення, поверніть негативне значення розміру клавіатури та використовуйте модифікатор .offset (y :), щоб відрегулювати зміщення зовнішнього більшості контейнерів перегляду. Він анімує досить добре і працює з будь-яким видом.


Як ви отримали це для анімації? У мене є .offset(y: withAnimation { -keyboard.currentHeight }), але вміст стрибає замість анімує.
jjatie

Це було декілька бета-версій тому, коли я обдурив цей код, але на момент мого попереднього коментаря модифікація зміщення vstack під час виконання була лише необхідною, SwiftUI анімував би зміни для вас.
pcallycat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.