Переместите TextField вверх, когда клавиатура появилась в SwiftUI

100

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

Выход:

Выход

Хитеш Сурани
источник
Вы можете использовать ScrollView. developer.apple.com/documentation/swiftui/scrollview
Прашант Тукадия 07
1
@PrashantTukadiya Спасибо за быстрый ответ. Я добавил TextField в Scrollview, но все еще сталкиваюсь с той же проблемой.
Hitesh Сурани
1
@DimaPaliychuk Это не сработает. это SwiftUI
Прашант Тукадия 07
20
Отображение клавиатуры и скрытие содержимого на экране появилось с тех пор, как появилось первое приложение для iPhone на Objective C? Это проблема, которую постоянно решают. Я, например, разочарован тем, что Apple не решила эту проблему с помощью SwiftUi. Я знаю, что этот комментарий никому не поможет, но я хотел поднять вопрос о том, что мы действительно должны оказывать давление на Apple, чтобы она предоставила решение, а не полагаться на сообщество, которое всегда будет решать эту наиболее распространенную проблему.
П. Энт
1
Есть очень хорошая статья Вадима vadimbulavin.com/…
Судара

Ответы:

64

Код обновлен для Xcode, beta 7.

Для этого вам не нужны отступы, ScrollViews или списки. Хотя это решение тоже подойдет им. Я привожу здесь два примера.

Первый перемещает все 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() }

}

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

        }
    }
}
контики
источник
1
Можно ли прикрепить GeometryGetterв качестве модификатора представления вместо фона, сделав его совместимым с ViewModifierпротоколом?
Судара
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
Хулио Байлон
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 (in: .global) получал SIGNAL ABORT и пробовал все предлагаемые решения для устранения этой ошибки
Марван Рушди,
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, я подтвердил, изменив цвет фона. Любая идея, почему 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и он отлично работает. Никаких проблем с анимацией
Tae
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, ломает помощь ускорения Metal.
Нельсон Кардачи,
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())
  }
}

Jberlana
источник
Ницца. Спасибо, что поделился.
декады
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

Вам нужно добавить 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. Я не могу заставить его работать так, как хочу. Было бы неплохо, если бы вы могли посмотреть: stackoverflow.com/questions/56500147/…
SwiftiSwift 07
Почему вы не используете @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
Mojtaba Hosseini
2
Я сам разобрался. Добавьте .ignoresSafeArea(.keyboard)к вашему просмотру.
leonboe1
6

Я просмотрел и преобразовал существующие решения в удобный пакет 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())
    }
}
Ральф Эберт
источник
Я просто вижу половину высоты текстового поля. ты знаешь как это решить?
Матросов Александр
Хороший. это сэкономило мне время. Прежде чем использовать это, мы знаем нижнее заполнение этого вида дескриптора модификатора.
Браунсу Хан,
5

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

  1. Большинство ответов здесь не касаются изменения рамки клавиатуры, поэтому они ломаются, если пользователь поворачивает устройство вместе с клавиатурой на экране. keyboardWillChangeFrameNotificationЭтим решается добавление в список обработанных уведомлений.
  2. Мне не нужны были несколько издателей с похожими, но разными закрытием карт, поэтому я связал все три уведомления с клавиатуры в одного издателя. По общему признанию, это длинная цепочка, но каждый шаг довольно прост.
  3. Я предоставил initфункцию, которая принимает a, @ViewBuilderчтобы вы могли использовать KeyboardHostпредставление, как любое другое представление, и просто передавать свой контент в завершающем замыкании, а не передавать представление содержимого в качестве параметра init.
  4. Как предложили Tae и fdelafuente в комментариях, я поменял местами 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? Я использую этот код в своем приложении, и у меня он отлично работает.
Тимоти Сандерс
Не могли бы вы включить Pridictiveв iOS keyboard. Settings-> General-> Keyboard-> Pridictive. в этом случае он не исправляет calclate и добавляет отступ на клавиатуре
GSerjo
@GSerjo: У меня включен интеллектуальный ввод текста на iPad Touch (7-го поколения) с бета-версией iOS 13.1. Он правильно добавляет отступ для высоты строки прогноза. (Важно отметить, что здесь я не регулирую высоту клавиатуры , я добавляю ее к заполнению самого представления.) Попробуйте поменять местами карту отладки, которая закомментирована, и поиграйте со значениями, которые вы получаете для прогнозирующего клавиатура. Я выложу лог в другом комментарии.
Тимоти Сандерс
Если карта «отладка» не прокомментирована, вы можете увидеть присвоенное значение keyboardHeight. На моем iPod Touch (в портретной ориентации) клавиатура с предиктивным включением составляет 254 балла. Без него 216 очков. Я даже могу отключить прогнозирование с экранной клавиатурой, и заполнение обновится правильно. Добавление клавиатуры с интеллектуальным Received UIKeyboardWillChangeFrameNotification with height 254.0 Received UIKeyboardWillShowNotification with height 254.0Received UIKeyboardWillChangeFrameNotification with height 216.0
Тимоти Сандерс,
4

Это адаптировано из того, что построил @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, указанный выше (нет, я тоже не работал, чтобы преобразовать его для использования предпочтений), как он показывает в своих примерах.

Фельдур
источник
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 в целом является лучшим подходом при применении кода смещения.

TheCodingArt
источник
Очень хорошее решение, очень рекомендую! Я добавил 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())
Blacktiago
источник
Мне нравится этот ответ, но я не совсем уверен, откуда взялся 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)
  }
}
VSMelo
источник
2
Просто к сведению, вы владеете объектами при вызове NotificationCenter.default.addObserver... вам нужно сохранить их и удалить наблюдателей в подходящее время ...
TheCodingArt,
Привет, @TheCodingArt, правильно, я пытался сделать это вот так ( oleb.net/blog/2018/01/notificationcenter-removeobserver ), но у меня это не работает, есть идеи?
Рэй
2

Как отметили Марк Кренек и Хейко, 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/

Позитрон
источник
Я вижу примечания к выпуску Apple, протестированы на beta5 и beta6, TextField работает, TextEditor НЕТ, что мне не хватает? @State var text = "" var body: some View {Form {Section {Text (text) .frame (height: 500)} Section {TextField ("5555", text: $ text) .frame (height: 50)} Раздел {TextEditor (text: $ text) .frame (height: 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
    }
}
8сухас
источник
попробовал ваше решение ... вид прокручивается только до половины текстового поля. Пробовал все вышеперечисленное решение. Получил ту же проблему. Пожалуйста помоги!!!
Сона
@Zeona, попробуйте в простом приложении, возможно, вы делаете что-то другое. Также попробуйте удалить '- (UIWindow.keyWindow? .SafeAreaInsets.bottom ?? 0)', если вы используете безопасную область.
8suhas
удалено (UIWindow.keyWindow? .safeAreaInsets.bottom ?? 0), то над клавиатурой появляется пробел
Сона
1

Обработка TabView «ю.ш.

Мне нравится ответ Бенджамина Киндла, но он не поддерживает TabViews. Вот моя корректировка его кода для обработки TabView:

  1. Добавьте расширение, чтобы 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
        }
    }
}
  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 (beta 4), все работает довольно просто:

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

А размер представления подстраивается под верхнюю часть клавиатуры. Конечно, есть больше возможностей для уточнения с frame (.maxHeight: ...) и т. Д. Вы разберетесь с этим.

К сожалению, плавающая клавиатура на iPad по-прежнему вызывает проблемы при перемещении. Но вышеупомянутые решения тоже будут, и это все еще бета, я надеюсь, они разберутся.

Спасибо, 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 :), чтобы настроить смещение самого внешнего контейнера представления. Он достаточно хорошо анимирует и работает с любым видом.

pcallycat
источник
Как вам удалось это оживить? У меня есть .offset(y: withAnimation { -keyboard.currentHeight }), но контент прыгает, а не оживляет.
jjatie
Несколько бета-версий назад я испортил этот код, но во время моего более раннего комментария изменение смещения vstack во время выполнения было всем, что требовалось, SwiftUI анимировал бы это изменение за вас.
pcallycat