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

У меня семь TextField внутри моей главной ContentView, Когда пользователь открывает клавиатуру, некоторые из TextField скрыты под рамой клавиатуры. Поэтому я хочу переместить все TextField вверх соответственно, когда клавиатура появилась.

Я использовал приведенный ниже код для добавления TextField на экране.

struct ContentView : View {
    @State var textfieldText: String = ""

    var body: some View {
            VStack {
                TextField($textfieldText, placeholder: Text("TextField1"))
                TextField($textfieldText, placeholder: Text("TextField2"))
                TextField($textfieldText, placeholder: Text("TextField3"))
                TextField($textfieldText, placeholder: Text("TextField4"))
                TextField($textfieldText, placeholder: Text("TextField5"))
                TextField($textfieldText, placeholder: Text("TextField6"))
                TextField($textfieldText, placeholder: Text("TextField6"))
                TextField($textfieldText, placeholder: Text("TextField7"))
            }
    }
}

Выход:

ht tps:https://stackru.com/images/80d078203740662c9929e9bfc25ee423f56e24c5.png

29 ответов

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

Вам не нужно заполнение, ScrollViews или списки для достижения этой цели. Хотя это решение будет хорошо с ними играть. Я включаю два примера здесь.

Первый перемещает все текстовое поле вверх, если для любого из них появляется клавиатура. Но только при необходимости. Если клавиатура не скрывает текстовые поля, они не будут двигаться.

Во втором примере представление только перемещается достаточно просто, чтобы избежать скрытия активного текстового поля.

Оба примера используют один и тот же общий код, найденный в конце: GeometryGetter и KeyboardGuardian

Первый пример (показать все текстовые поля)

struct ContentView: View {
    @ObjectBinding 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($name[0], placeholder: Text("enter text #1"))
                .textFieldStyle(.roundedBorder)

            TextField($name[1], placeholder: Text("enter text #2"))
                .textFieldStyle(.roundedBorder)

            TextField($name[2], placeholder: Text("enter text #3"))
                .textFieldStyle(.roundedBorder)
                .background(GeometryGetter(rect: $kGuardian.rects[0]))

        }.offset(y: kGuardian.slide).animation(.basic(duration: 1.0))
    }

}

Второй пример (показать только активное поле)

struct ContentView: View {
    @ObjectBinding 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($name[0], placeholder: Text("enter text #1"), onEditingChanged: { if $0 { self.kGuardian.showField = 0 } })
                .textFieldStyle(.roundedBorder)
                .background(GeometryGetter(rect: $kGuardian.rects[0]))

            TextField($name[1], placeholder: Text("enter text #2"), onEditingChanged: { if $0 { self.kGuardian.showField = 1 } })
                .textFieldStyle(.roundedBorder)
                .background(GeometryGetter(rect: $kGuardian.rects[1]))

            TextField($name[2], placeholder: Text("enter text #3"), onEditingChanged: { if $0 { self.kGuardian.showField = 2 } })
                .textFieldStyle(.roundedBorder)
                .background(GeometryGetter(rect: $kGuardian.rects[2]))

            }.offset(y: kGuardian.slide).animation(.basic(duration: 1.5))
    }

}

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 { () -> ShapeView<Rectangle, Color> in
                DispatchQueue.main.async {
                    self.rect = geometry.frame(in: .global)
                }

                return Rectangle().fill(Color.clear)
            }
        }
    }
}

Обновление Я добавил DispatchQueue.main.async, чтобы избежать возможности изменения состояния представления во время его визуализации. ***

KeyboardGuardian

Цель KeyboardGuardian - отслеживать события отображения / скрытия клавиатуры и вычислять, сколько места необходимо сместить.

Обновление: я изменил KeyboardGuardian, чтобы обновить слайд, когда пользователь вкладывает из одного поля в другое

import SwiftUI
import Combine

final class KeyboardGuardian: BindableObject {
    let didChange = 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
    public var keyboardIsHidden = true

    public var slide: Length = 0 {
        didSet {
            didChange.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)

    }

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

        }
    }
}

Чтобы построить на основе решения @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.

iOS 14 - однострочный код

Добавьте этот модификатор в TextField

.ignoresSafeArea(.keyboard, edges: .bottom)

Apple добавила клавиатуру как область для безопасной зоны, так что вы можете использовать ее для перемещения любогоView с клавиатурой, как и в других регионах.

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

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

Я создал действительно простой в использовании модификатор вида.

Добавьте файл 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())
  }
}

Или вы можете просто использовать IQKeyBoardManagerSwift

и при желании можно добавить это в делегат вашего приложения, чтобы скрыть панель инструментов и включить скрытие клавиатуры при щелчке на любом представлении, кроме клавиатуры.

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

Я просмотрел и преобразовал существующие решения в удобный пакет 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())
    }
}

Вам нужно добавить ScrollView и установите нижний отступ размера клавиатуры, чтобы содержимое могло прокручиваться при появлении клавиатуры.

Чтобы получить размер клавиатуры, вам нужно будет использовать NotificationCenter зарегистрироваться на событие клавиатуры. Вы можете использовать собственный класс для этого:

final class KeyboardResponder: BindableObject {
    let didChange = PassthroughSubject<CGFloat, Never>()

    private var _center: NotificationCenter
    private(set) var currentHeight: CGFloat = 0 {
        didSet {
            didChange.send(currentHeight)
        }
    }

    init(center: NotificationCenter = .default) {
        _center = center
        _center.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        _center.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    deinit {
        _center.removeObserver(self)
    }

    @objc func keyBoardWillShow(notification: Notification) {
        print("keyboard will show")
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
            currentHeight = keyboardSize.height
        }
    }

    @objc func keyBoardWillHide(notification: Notification) {
        print("keyboard will hide")
        currentHeight = 0
    }
}

BindableObject соответствие позволит вам использовать этот класс как State и вызвать обновление представления. Если нужно, посмотрите учебник для BindableObject: SwiftUI учебник

Когда вы получите это, вам нужно настроить ScrollView уменьшить его размер при появлении клавиатуры. Для удобства я завернул это ScrollView в какой-то компонент:

struct KeyboardScrollView<Content: View>: View {
    @State var keyboard = KeyboardResponder()
    private var content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        ScrollView {
            VStack {
                content
            }
        }
        .padding(.bottom, keyboard.currentHeight)
    }
}

Все, что вам нужно сделать сейчас, это встроить свой контент в пользовательский ScrollView,

struct ContentView : View {
    @State var textfieldText: String = ""

    var body: some View {
        KeyboardScrollView {
            ForEach(0...10) { index in
                TextField(self.$textfieldText, placeholder: Text("TextField\(index)")) {
                    // Hide keyboard when uses tap return button on keyboard.
                    self.endEditing(true)
                }
            }
        }
    }

    private func endEditing(_ force: Bool) {
        UIApplication.shared.keyWindow?.endEditing(true)
    }
}

Изменить: поведение прокрутки действительно странно, когда клавиатура прячется. Возможно, использование анимации для обновления отступов исправит это, или вам следует рассмотреть возможность использования чего-то другого, кроме padding настроить размер вида прокрутки.

Применение:

var body: some View {
    ScrollView {
        VStack {
          /*
          TextField()
          */
        }
    }.keyboardSpace()
}

Код:

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

У некоторых из вышеперечисленных решений были некоторые проблемы, и они не обязательно были "самым чистым" подходом. Из-за этого я изменил несколько вещей для реализации ниже.

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

Мне бы хотелось более четкого подхода и перенести ответственность на сконструированное представление (а не на модификатор) в том, как смещать контент, но, похоже, я не мог заставить издателей правильно запускать при перемещении кода смещения в представление....

Также обратите внимание, что издатели должны были использоваться в этом случае как final class в настоящее время вызывает сбои с неизвестными исключениями (даже если это соответствует требованиям интерфейса), и ScrollView в целом является лучшим подходом при применении кода смещения.

Честно говоря, многие из этих ответов кажутся действительно раздутыми. Если вы используете SwiftUI, вы также можете использовать Combine.

Создать KeyboardResponder как показано ниже, вы можете использовать, как показано ранее.

Обновлено для iOS 14.

import Combine
import UIKit

final class KeyboardResponder: ObservableObject {

    @Published var keyboardHeight: CGFloat = 0

    init() {
        NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)
            .compactMap { notification in
                (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height
            }
            .receive(on: DispatchQueue.main)
            .assign(to: \.keyboardHeight)
    }
}


struct ExampleView: View {
    @ObservedObject private var keyboardResponder = KeyboardResponder()
    @State private var text: String = ""

    var body: some View {
        VStack {
            Text(text)
            Spacer()
            TextField("Example", text: $text)
        }
        .padding(.bottom, keyboardResponder.keyboardHeight)
    }
}

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

  1. Большинство ответов здесь не относятся к клавиатуре, меняющей рамку, поэтому они ломаются, если пользователь поворачивает устройство вместе с клавиатурой на экране. ДобавлениеkeyboardWillChangeFrameNotification к списку обработанных уведомлений обращается этот.
  2. Мне не нужны были несколько издателей с похожими, но разными закрытием карт, поэтому я связал все три уведомления с клавиатуры в одного издателя. По общему признанию, это длинная цепочка, но каждый шаг довольно прост.
  3. Я предоставил init функция, которая принимает @ViewBuilder так что вы можете использовать KeyboardHost view, как и любой другой View, и просто передайте свой контент в завершающем закрытии, вместо того, чтобы передавать представление контента в качестве параметра в 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"))
        }
    }
}

Это адаптировано из того, что построил @kontiki. У меня он работает в приложении под бета-версией 8 / GM seed, где поле, требующее прокрутки, является частью формы внутри NavigationView. Вот KeyboardGuardian:

//
//  KeyboardGuardian.swift
//
//  https://stackru.com/questions/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, указанный выше (нет, я тоже не работал, чтобы преобразовать его для использования предпочтений), как он иллюстрирует в своих примерах.

Если вы используете iOS 14+ с прокруткой или у вас есть возможность использовать прокрутку.

https://developer.apple.com/documentation/swiftui/scrollviewproxy https://developer.apple.com/documentation/swiftui/scrollviewreader

Ниже может помочь

              ScrollViewReader { (proxy: ScrollViewProxy) in
            ScrollView {
                view1().frame(height: 200)
                view2().frame(height: 200)

                view3() <-----this has textfields 
                    .onTapGesture {
                        proxy.scrollTo(1, anchor: .center)
                    }
                    .id(1)

                view4() <-----this has text editor
                    .onTapGesture {
                        proxy.scrollTo(2, anchor: .center)
                    }
                    .id(2)

                view5().frame(height: 200)
                view6().frame(height: 200)
                submtButton().frame(height: 200)
            }
        }

имп часть сверху есть

               anyView().onTapGesture {
              proxy.scrollTo(_ID, anchor: .center)
         }.id(_ID)

Надеюсь, это кому-то поможет :)

Я не уверен, что 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())

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

Xcode 12 beta 4 добавляет новый модификатор вида ignoresSafeArea что теперь вы можете использовать, чтобы избежать клавиатуры.

.ignoresSafeArea([], edges: [])

Это позволяет избежать клавиатуры и всех краев безопасной зоны. Вы можете установить первый параметр на.keyboardесли вы не хотите, чтобы этого избегали. В этом есть некоторые причуды, по крайней мере, в моей настройке иерархии представлений, но похоже, что именно так Apple хочет, чтобы мы избегали клавиатуры.

Если вы хотите, чтобы экран был оформлен так, то вы можете использовать наложения, как показано ниже.

      struct LoginView: View {

var body: some View {
    
    VStack(spacing: 0) {
        
        Color.clear
            .overlay {
                LogoImageView() 
              // Here you can add your any Logo image
            }
        
        Text("Login to your account")
        
        Color.clear
        
            .overlay {
                TextFieldView()
                // Here you can add multiple text field in separate 
              VStack.
            }
           
        Text("version text")   
     }
  }
}

Если вы хотите, чтобы клавиатура перекрывалась с текстовым полем, используйте следующий код.

        .ignoresSafeArea(.keyboard, edges: .bottom)

добавьте эту строку после родительского Vstack.

Что касается iOS 14 (beta 4), все работает довольно просто:

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

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

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

Спасибо, Apple, наконец!

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

      import SwiftUI

struct MyView: View {
@State  var titlesArray = ["ATitle" , "BTitle" , "CTitle" , "DTitle"
                           , "ETitle" , "FTitle" , "GTitle", "HTitle", "ITitle", "JTitle", "KTitle", "LTitle", "MTitle", "NTitle", "OTitle", "PTitle", "QTitle", "RTitle", "STitle", "TTitle", "UTitle", "VTitle", "WTitle", "XTitle", "YTitle", "ZTitle"]
@State  var name = ""

@State private var isKeyboardVisible = false


var body: some View {
    
    
    
    VStack {
        ScrollViewReader { proxy in // Use a ScrollViewReader to scroll to fields
            
            ScrollView {
                LazyVStack(spacing : 20) {
                    
                    ForEach(Array(titlesArray.indices), id: \.self) { index in
                        
                        TextField("Text Field \(index+1)", text: $name, onEditingChanged: { isFocused in
                            if isFocused {
                                
                                withAnimation {
                                    proxy.scrollTo(index,anchor : .top)// scroll the selected textfield
                                    
                                }
                            }
                        })
                        .id(index) // provide the unique id for ScrollViewReader to read which text field should go on top
                        
                        
                        .frame(height: 45)
                        .padding([.leading,.trailing],20)
                        .disableAutocorrection(true)
                        .keyboardType(.alphabet)
                        .submitLabel(.return)
                        
                        .overlay(
                            RoundedRectangle(cornerRadius: 5)
                                .stroke(Colors().mheroon, lineWidth: 1)
                        )
                        .padding([.leading,.trailing],20)
                    }
                }
                .padding(.bottom, isKeyboardVisible ? 180 : 0) // to give some extra space for scorll view else last text field will not scroll on top
                
            }
        }
        .padding(.top,20)
        
        Spacer()
        
        VStack {
            Spacer()
            Button {
                
            } label: {
                Text("continue")
                    .padding()
            }
            Spacer()
            
        }
        .frame(height: 80)
        
        
        
    }
    .ignoresSafeArea(.keyboard, edges: .bottom)
    //if you provide such padding .ignoresSafeArea(.keyboard, edges: .bottom) this line of code willn't work and default scrolling will go on
    //        .padding(.top,50)
    //        .padding()
    .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in
        self.isKeyboardVisible = true
    }
    .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
        self.isKeyboardVisible = false
    }
 
 }
}

struct MyView_Previews: PreviewProvider {
static var previews: some View {
    MyView()
 }
}

Ответ скопирован отсюда: 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)
  }
}

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

(Примечание: я еще не использовал это достаточно, чтобы полностью понять любые последствия этого!)

Обработка 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 view для учета высоты панели вкладок:
.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in
            self.keyboardHeight = max(height - UITabBar.size.height, 0)
        }
  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() }
    }
  }
}

Самый элегантный ответ, который мне удалось найти, похож на решение Рафаэля. Создайте класс для прослушивания событий клавиатуры. Вместо того чтобы использовать размер клавиатуры для изменения отступов, верните отрицательное значение размера клавиатуры и используйте модификатор.offset(y:), чтобы отрегулировать смещение внешнего внешнего контейнера. Он оживляет достаточно хорошо и работает с любым видом.

Я рассмотрел здесь каждое решение, и хотя некоторые из них хорошо реализованы, ни одно из них не работало правильно, отображая половину текстового поля. Кроме того, ни одно из решений вообще не работает с элементом управления TextEditor, если вы не сместите координату -y содержимого, что в любом случае будет выглядеть странно. Пользователь должен иметь возможность прокручивать все поля формы, даже когда отображается клавиатура.

Сценарий заключается в том, что у вас есть представление, содержащее форму с ScrollView, которая имеет ряд текстовых полей, включая поле текстового редактора, и кнопку, которая всегда видна внизу формы с помощью .ignoresSafeArea(.keyboard). Я все еще работаю над этим вопросом. Если у кого-то есть полное решение, пожалуйста, помогите.

Также я обнаружил, что, к сожалению, при использовании .ignoresSafeArea(.keyboard), чтобы кнопка всегда отображалась внизу, если я использую ScrollViewReader в сочетании с любым из приведенных выше решений, scrollTo просто не работает вообще.

Вот другой подход, который мне пришлось использовать, чтобы заставить его работать в iOS 15.

      import Combine
import UIKit

public final class KeyboardResponder: ObservableObject {

@Published public var keyboardHeight: CGFloat = 0
var showCancellable: AnyCancellable?
var hideCancellable: AnyCancellable?

public init() {
    showCancellable = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
    .map { notification in
        (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height ?? 0.0
    }
    .receive(on: DispatchQueue.main)
    .sink(receiveValue: { height in
        print(height)
        self.keyboardHeight = height
    })
    
    hideCancellable = NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
    .receive(on: DispatchQueue.main)
    .sink(receiveValue: { _ in
        self.keyboardHeight = 0
    })
}

}

А затем используйте его следующим образом:

      @ObservedObject private var keyboardResponder = KeyboardResponder()

SomeView
   .padding(.bottom, keyboardResponder.keyboardHeight)

Это не самое чистое решение, но я не смог получить 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
        
    }
    
}

По сути, управление нижним отступом в зависимости от высоты клавиатуры.

Другие вопросы по тегам