【问题标题】:How to show complete List when keyboard is showing up in SwiftUI如何在 SwiftUI 中显示键盘时显示完整列表
【发布时间】:2019-11-05 01:33:14
【问题描述】:

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

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

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

任何人都可以帮助我如何做到这一点?

非常感谢。

【问题讨论】:

  • 尝试订阅键盘出现/消失事件并将底部边距应用到VStack
  • 我目前正在做一些非常相似的事情(使用 ScrollView 而不是 List)。我认为订阅键盘显示/隐藏事件是正确的方法。但这不是困难的部分。挑战在于找出活动文本字段在哪里以确定是否需要偏移量,如果需要,需要多少。您的具体示例很简单,因为您有一个固定的 400 像素……但是,我假设这只是一个示例。目标是能够确定文本字段与其父级的相对位置以及它滚动了多少,然后我们知道我们需要移动多少。

标签: ios swift swiftui


【解决方案1】:

使用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 { $0.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect }
        .map { $0.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 { $0.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect }
        .map { $0.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())

【讨论】:

  • 我喜欢 ViewModifier 方法,但是与其将其应用于整个组,不如将其应用于层次结构中的任意视图以说“此视图不能被遮挡”通过键盘”在表单中,它总是处于活动状态的文本字段,但对于登录屏幕,它可能是用户名、密码和登录按钮。
  • 修饰符是一个优雅的解决方案,加上如何使用新的 Combine 框架的一个很好的例子。在 Xcode 版本 11.0 (11A420a) 上,我必须将对 Length 的引用更改为 CGFloat。做得好,谢谢!
  • @jjatie 这会很棒。大多数时候,我们实际上并不希望整个表单向上移动。如果我有一个包含多个字段的表单,并且单击顶部的第一个字段,我真的不想将其向上移动,因为它会移到屏幕外。
  • 在最后一个例子中,什么会导致订阅被删除?我不清楚是什么保留了它。它保留了self,但是订阅不应该因为没有分配给任何东西而消失吗? (我知道它没有;我只是不明白为什么。)
  • 我和@RobNapier 有同样的问题——有人知道订阅是如何保留在这里的吗?
【解决方案2】:

将 Bogdan Farca 的出色组合方法更新到 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 { $0.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect }
        .map { $0.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)
    }
}

【讨论】:

  • 谢谢。这与原始方法相结合,似乎效果很好。
  • 无法在 iPad 上正常工作。添加一个额外的(不知道,但大约 20-40px)填充,显示为内容和键盘之间的空白
  • 同样在 iPhone 上,当我们使用表单内的字段时,它的内边距比预期的低 10px-20px
  • 我将在 iPad 上尝试代码,看看能否找出空白的原因。
  • iPhone 11 Pro Max 的问题与 iPad 相同。我认为键盘打开时需要减去safeAreaInsets.bottom,但我还不知道如何获得这个值。
【解决方案3】:

有一个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)
}

【讨论】:

  • 你试过你的答案了吗?虽然keyboard.currentHeight 改变了,但是视图并没有移动。使用.offset(y: -keyboard.currentHeight) 可以。但是,您的方法完全忽略了 textField 的实际位置。例如,如果列表滚动并且字段太高,当键盘出现时,文本字段会向上移动并远离屏幕。
  • 我喜欢你的回答,是让键盘观察者在一个单独的类上,而不是在视图结构本身上。稍后我可能会更改我发布的答案以遵循该方法。
  • 我已经用填充试过了,它可以工作(用户必须手动滚动,但内容会完全显示)
  • 现在我再次阅读 OP 原始问题,我意识到他想展示整个内容,而不关心文本字段的位置。所以你的答案应该可以正常工作。我会使用偏移量而不是填充。避免手动滚动。
  • 但更改偏移量会隐藏顶部文本视图,用户也无法手动滚动。
【解决方案4】:

这是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 { ($0.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))
  ...
}

【讨论】:

  • 嘿,我注意到你没有添加 ``` storein(:cancellables) ``` ,这不会导致内存泄漏吗?
【解决方案5】:

让观察者设置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)

  }

【讨论】:

    【解决方案6】:

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

    【讨论】:

      【解决方案7】:

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

      键盘观察类:

      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)
      

      【讨论】:

      • @objc 在 SwiftUI 中不可用
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-05-04
      • 2019-03-05
      • 1970-01-01
      • 2015-08-22
      相关资源
      最近更新 更多