如何在 SwiftUI 中显示键盘时显示完整列表

How to show complete List when keyboard is showing up in SwiftUI

如何在键盘出现时显示完整的列表?键盘隐藏了列表的下半部分。

我的列表行中有一个文本字段。当键盘出现时,无法向下滚动以查看完整列表。键盘在列表前面,而不是 "under" 列表。这是我的编码:

struct ContentView: View {

    @State private var name = ""

    var body: some View {
        List {
            VStack {
                Text("Begin")
                    .frame(width: UIScreen.main.bounds.width)
                    .padding(.bottom, 400)
                    .background(Color.red)

                TextField($name, placeholder: Text("enter text"), onEditingChanged: { _ in
                    //
                }) {
                    //
                }

                Text("End")
                    .frame(width: UIScreen.main.bounds.width)
                    .padding(.top, 400)
                    .background(Color.green)
            }
            .listRowInsets(EdgeInsets())
        }
    }
}

有人可以帮我怎么做吗?

非常感谢。

有一个 answer here 来处理键盘操作, 您可以像这样订阅键盘事件:

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) {
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
            currentHeight = keyboardSize.height
        }
    }

    @objc func keyBoardWillHide(notification: Notification) {
        currentHeight = 0
    }
}

然后像这样使用它:

@State var keyboard = KeyboardResponder()
var body: some View {
        List {
            VStack {
             ...
             ...
             ...
            }.padding(.bottom, keyboard.currentHeight)
}

使用 ComposeKeyboardResponder 对象的替代实现,如 here.

所示
final class KeyboardResponder: ObservableObject {

    let willChange = PassthroughSubject<CGFloat, Never>()

    private(set) var currentHeight: Length = 0 {
        willSet {
            willChange.send(currentHeight)
        }
    }

    let keyboardWillOpen = NotificationCenter.default
        .publisher(for: UIResponder.keyboardWillShowNotification)
        .first() // keyboardWillShow notification may be posted repeatedly
        .map { [=10=].userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect }
        .map { [=10=].height }

    let keyboardWillHide =  NotificationCenter.default
        .publisher(for: UIResponder.keyboardWillHideNotification)
        .map { _ in CGFloat(0) }

    func listen() {
        _ = Publishers.Merge(keyboardWillOpen, keyboardWillHide)
            .subscribe(on: RunLoop.main)
            .assign(to: \.currentHeight, on: self)
    }

    init() {
        listen()
    }
}

一个更好的方法是将以上内容打包为 ViewModifier(从 here 松散改编):

struct AdaptsToSoftwareKeyboard: ViewModifier {

    @State var currentHeight: Length = 0

    func body(content: Content) -> some View {
        content
            .padding(.bottom, currentHeight)
            .edgesIgnoringSafeArea(currentHeight == 0 ? Edge.Set() : .bottom)
            .onAppear(perform: subscribeToKeyboardEvents)
    }

    private let keyboardWillOpen = NotificationCenter.default
        .publisher(for: UIResponder.keyboardWillShowNotification)
        .map { [=11=].userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect }
        .map { [=11=].height }

    private let keyboardWillHide =  NotificationCenter.default
        .publisher(for: UIResponder.keyboardWillHideNotification)
        .map { _ in Length.zero }

    private func subscribeToKeyboardEvents() {
        _ = Publishers.Merge(keyboardWillOpen, keyboardWillHide)
            .subscribe(on: RunLoop.main)
            .assign(to: \.currentHeight, on: self)
    }
}

然后可以这样使用:

Group {

   ........

}.modifier(AdaptsToSoftwareKeyboard())

将 Bogdan Farca 出色的 Combine 方法更新到 XCode 11.2:

import Combine
import SwiftUI

struct AdaptsToSoftwareKeyboard: ViewModifier {

    @State var currentHeight: CGFloat = 0

    func body(content: Content) -> some View {
        content
            .padding(.bottom, self.currentHeight)
            .edgesIgnoringSafeArea(self.currentHeight == 0 ? Edge.Set() : .bottom)
            .onAppear(perform: subscribeToKeyboardEvents)
    }

    private let keyboardWillOpen = NotificationCenter.default
        .publisher(for: UIResponder.keyboardWillShowNotification)
        .map { [=10=].userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect }
        .map { [=10=].height }

    private let keyboardWillHide =  NotificationCenter.default
        .publisher(for: UIResponder.keyboardWillHideNotification)
        .map { _ in CGFloat.zero }

    private func subscribeToKeyboardEvents() {
        _ = Publishers.Merge(keyboardWillOpen, keyboardWillHide)
            .subscribe(on: RunLoop.main)
            .assign(to: \.self.currentHeight, on: self)
    }
}

让观察者设置 EnvironmentValue。然后在您的视图中将其设为变量:

 @Environment(\.keyboardHeight) var keyboardHeight: CGFloat
import SwiftUI
import UIKit

extension EnvironmentValues {

  var keyboardHeight : CGFloat {
    get { EnvironmentObserver.shared.keyboardHeight }
  }

}

class EnvironmentObserver {

  static let shared = EnvironmentObserver()

  var keyboardHeight: CGFloat = 0 {
    didSet { print("Keyboard height \(keyboardHeight)") }
  }

  init() {

    // MARK: Keyboard Events

    NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidHideNotification, object: nil, queue: OperationQueue.main) { [weak self ] (notification) in
      self?.keyboardHeight = 0
    }

    let handler: (Notification) -> Void = { [weak self] notification in
        guard let userInfo = notification.userInfo else { return }
        guard let frame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }

        // From Apple docs:
        // The rectangle contained in the UIKeyboardFrameBeginUserInfoKey and UIKeyboardFrameEndUserInfoKey properties of the userInfo dictionary should be used only for the size information it contains. Do not use the origin of the rectangle (which is always {0.0, 0.0}) in rectangle-intersection operations. Because the keyboard is animated into position, the actual bounding rectangle of the keyboard changes over time.

        self?.keyboardHeight = frame.size.height
    }

    NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidShowNotification, object: nil, queue: OperationQueue.main, using: handler)

    NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidChangeFrameNotification, object: nil, queue: OperationQueue.main, using: handler)

  }

这是 BindableObject 实施的更新版本(现命名为 ObservableObject)。

import SwiftUI
import Combine

class KeyboardObserver: ObservableObject {

  private var cancellable: AnyCancellable?

  @Published private(set) var keyboardHeight: CGFloat = 0

  let keyboardWillShow = NotificationCenter.default
    .publisher(for: UIResponder.keyboardWillShowNotification)
    .compactMap { ([=10=].userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height }

  let keyboardWillHide = NotificationCenter.default
    .publisher(for: UIResponder.keyboardWillHideNotification)
    .map { _ -> CGFloat in 0 }

  init() {
    cancellable = Publishers.Merge(keyboardWillShow, keyboardWillHide)
      .subscribe(on: RunLoop.main)
      .assign(to: \.keyboardHeight, on: self)
  }
}

以下是如何在您的视图中使用它:

@ObservedObject private var keyboardObserver = KeyboardObserver()

var body: some View {
  ...
  YourViewYouWantToRaise()
    .padding(.bottom, keyboardObserver.keyboardHeight)
    .animation(.easeInOut(duration: 0.3))
  ...
}

这些例子有点老了,我修改了一些代码以使用最近添加到 SwiftUI 的新功能,这个示例中使用的代码的详细解释可以在这篇文章中找到:Article Describing ObservableObject

键盘观察者class:

import SwiftUI
import Combine

final class KeyboardResponder: ObservableObject {
    let objectWillChange = ObservableObjectPublisher()
    private var _center: NotificationCenter
    @Published var currentHeight: CGFloat = 0

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

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

    @objc func keyBoardWillHide(notification: Notification) {
        currentHeight = 0
    }
}

用法:

@ObservedObject private var keyboard = KeyboardResponder()

VStack {
 //Views here
}
//Makes it go up, since negative offset
.offset(y: -self.keyboard.currentHeight)

SwiftUIX

的一行解决方案

如果安装 SwiftUIX,您只需在包含列表的视图上调用 .padding(.keyboard)。这是迄今为止我见过的最好最简单的解决方案!

import SwiftUIX

struct ExampleView: View {
    var body: some View {
        VStack {
           List {
            ForEach(contacts, id: \.self) { contact in
                cellWithContact(contact)
            }
           }
        }.padding(.keyboard) // This is all that's needed, super cool!
    }
}