【问题标题】:Move TextField up when the keyboard has appeared in SwiftUI当键盘出现在 SwiftUI 中时向上移动 TextField
【发布时间】:2019-10-22 19:36:41
【问题描述】:

我的主要ContentView 中有七个TextField。当用户打开键盘时,一些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"))
            }
    }
}

输出:

【问题讨论】:

  • @PrashantTukadiya 感谢您的快速回复。我在 Scrollview 中添加了 TextField 但仍然面临同样的问题。
  • @DimaPaliychuk 这行不通。它是 SwiftUI
  • 键盘的显示和它遮挡屏幕上的内容从什么时候开始就出现了,第一个 Objective C iPhone 应用程序?这是不断解决的问题。对于 Apple 没有通过 SwiftUi 解决这个问题,我感到很失望。我知道这条评论对任何人都没有帮助,但我想提出这个问题,我们真的应该向 Apple 施加压力,要求其提供解决方案,而不是依赖社区来始终提供这个最常见的问题。
  • 瓦迪姆有一篇很好的文章vadimbulavin.com/…

标签: ios swift swiftui


【解决方案1】:

为 Xcode,beta 7 更新了代码。

您不需要填充、滚动视图或列表来实现这一点。尽管此解决方案也可以很好地解决它们。我在这里包括两个例子。

第一个将 all textField 向上移动,如果其中任何一个出现键盘。但仅在需要时。如果键盘不隐藏文本字段,它们就不会移动。

在第二个示例中,视图仅移动到足以避免隐藏活动文本字段。

两个示例都使用末尾相同的通用代码:GeometryGetterKeyboardGuardian

第一个示例(显示所有文本字段)

struct ContentView: View {
    @ObservedObject private var kGuardian = KeyboardGuardian(textFieldCount: 1)
    @State private var name = Array<String>.init(repeating: "", count: 3)

    var body: some View {

        VStack {
            Group {
                Text("Some filler text").font(.largeTitle)
                Text("Some filler text").font(.largeTitle)
            }

            TextField("enter text #1", text: $name[0])
                .textFieldStyle(RoundedBorderTextFieldStyle())

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

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

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

}

第二个示例(仅显示活动字段)

struct ContentView: View {
    @ObservedObject private var kGuardian = KeyboardGuardian(textFieldCount: 3)
    @State private var name = Array<String>.init(repeating: "", count: 3)

    var body: some View {

        VStack {
            Group {
                Text("Some filler text").font(.largeTitle)
                Text("Some filler text").font(.largeTitle)
            }

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

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

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

            }.offset(y: kGuardian.slide).animation(.easeInOut(duration: 1.0))
    }.onAppear { self.kGuardian.addObserver() } 
.onDisappear { self.kGuardian.removeObserver() }

}

几何获取器

这是一个吸收其父视图的大小和位置的视图。为了实现这一点,它在 .background 修饰符中被调用。这是一个非常强大的修饰符,不仅仅是装饰视图背景的一种方式。将视图传递给 .background(MyView()) 时,MyView 将修改后的视图作为父视图。使用 GeometryReader 可以让视图了解父级的几何形状。

例如:Text("hello").background(GeometryGetter(rect: $bounds)) 将填充变量边界,使用文本视图的大小和位置,并使用全局坐标空间。

struct GeometryGetter: View {
    @Binding var rect: CGRect

    var body: some View {
        GeometryReader { geometry in
            Group { () -> AnyView in
                DispatchQueue.main.async {
                    self.rect = geometry.frame(in: .global)
                }

                return AnyView(Color.clear)
            }
        }
    }
}

更新我添加了 DispatchQueue.main.async,以避免在渲染时修改视图状态的可能性。***

键盘卫士

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

        }
    }
}

【讨论】:

  • 是否可以通过使其符合ViewModifier 协议将GeometryGetter 附加为视图修饰符而不是背景?
  • 有可能,但有什么好处呢?你会像这样附加它:.modifier(GeometryGetter(rect: $kGuardian.rects[1])) 而不是.background(GeometryGetter(rect: $kGuardian.rects[1]))。差别不大(仅少了 2 个字符)。
  • 在某些情况下,如果您正在离开此屏幕,则在分配新矩形时,您可能会从 GeometryGetter 内的程序中获得 SIGNAL ABORT。如果发生这种情况,您只需添加一些代码来验证几何的大小是否大于零(geometry.size.width > 0 && geometry.size.height > 0),然后再将值分配给 self.rect
  • @JulioBailon 我不知道为什么,但将geometry.frame 移出DispatchQueue.main.async 有助于信号中止,现在将测试您的解决方案。更新:if geometry.size.width &gt; 0 &amp;&amp; geometry.size.height &gt; 0 在分配 self.rect 之前有所帮助。
  • 在 self.rect = geometry.frame(in: .global) 获得 SIGNAL ABORT 并尝试了所有建议的解决方案来解决此错误时,这也对我造成了影响
【解决方案2】:

为了构建 @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 将触发 UI 重新渲染,并在键盘显示时将您的 TextField 向上移动,并在关闭时返回。但是我没有使用 ScrollView。

【讨论】:

  • 我喜欢这个答案,因为它很简单。我添加了.animation(.easeOut(duration: 0.16)) 以尝试匹配键盘向上滑动的速度。
  • 为什么把键盘的最大高度设置为 340?
  • @DanielRyan 有时键盘高度在模拟器中返回不正确的值。我似乎无法找到解决当前问题的方法
  • 我自己还没有看到这个问题。也许它已在最新版本中修复。如果有(或将会有)更大的键盘,我不想锁定尺寸。
  • 你可以试试keyboardFrameEndUserInfoKey。这应该是键盘的最后一帧。
【解决方案3】:

我尝试了许多建议的解决方案,尽管它们在大多数情况下都有效,但我遇到了一些问题 - 主要是安全区域(我在 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)
                .onAppear(perform: {
                    NotificationCenter.Publisher(center: NotificationCenter.default, name: UIResponder.keyboardWillShowNotification)
                        .merge(with: NotificationCenter.Publisher(center: NotificationCenter.default, name: UIResponder.keyboardWillChangeFrameNotification))
                        .compactMap { notification in
                            withAnimation(.easeOut(duration: 0.16)) {
                                notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] 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))
                })
        }
    }
}

extension View {
    func adaptsToKeyboard() -> some View {
        return modifier(AdaptsToKeyboard())
    }
}

用法:

struct MyView: View {
    var body: some View {
        Form {...}
        .adaptsToKeyboard()
    }
}

【讨论】:

  • 哇,这是它们中最 SwiftUI 的版本,带有 GeometryReader 和 ViewModifier。喜欢它。
  • 这非常有用和优雅。非常感谢你写这篇文章。
  • 我在键盘上看到一个小的空白白色视图。此视图是 GeometryReader 视图,我通过更改背景颜色确认。知道为什么 GeometryReader 在我的实际视图和键盘之间显示。
  • 当我第二次使用键盘进入视图并单击 TextField 时,在线 rect.height - geometry.safeAreaInsets.bottom 上出现错误 Thread 1: signal SIGABRT。我第一次点击TextField 没关系。应用仍然崩溃。
【解决方案4】:

从 iOS 14.2 开始,TextFields 如果有足够的空间移动,则默认情况下可以识别键盘。例如,如果它在 VStackSpacer 中(请看下面的旧演示代码没有修饰符


⚠️ 对于 +iOS 14.2,以下代码似乎无法正常工作

Xcode 12(至 iOS 14.2)- 一行代码

将此修饰符添加到TextField

.ignoresSafeArea(.keyboard, edges: .bottom)

Apple 将键盘添加为安全区域的区域,因此您可以使用它像其他区域一样使用键盘移动 任何View

【讨论】:

  • 它适用于任何 View,包括TextEditor
  • @MojtabaHosseini 如果我想阻止这种行为怎么办?当我打开键盘时,我有一个图像正在向上移动,我不想移动。
  • 你应该把它应用到正确的视图上。正如您在此预览中看到的那样,红色视图保持静止。 @kyrers
  • 我自己想通了。将.ignoresSafeArea(.keyboard) 添加到您的视图中。
  • 这不是解决方案。这一行实际上告诉编译器不要尊重控件上的安全区域(但这什么也没做)。只需删除该行,您将看到完全相同的行为。在 iOS14 中,键盘回避是默认设置。如果存在,您的视图将缩小到屏幕大小减去键盘。使用 .ignoresSafeArea 您实际上可以防止它发生在视图上。这就是为什么它被称为ignores-safe-area。
【解决方案5】:

我创建了一个可以包裹任何其他视图的视图,以便在键盘出现时将其缩小。

这很简单。我们为键盘显示/隐藏事件创建发布者,然后使用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 中。

【讨论】:

  • 我认为这是正确的答案。我只是做了一个小调整:我只是修改了self.view 的填充而不是矩形,效果很好。动画完全没有问题
  • 谢谢!完美运行。正如@Taed 所说,最好使用填充方法。最终结果将是var body: some View { VStack { view .padding(.bottom, keyboardHeight) .animation(.default) } .onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = height } }
  • 尽管票数较少,但这是最快速的回复。之前使用 AnyView 的方法会破坏 Metal 加速帮助。
  • 这是一个很好的解决方案,但这里的主要问题是,只有当键盘隐藏了您正在编辑的文本字段时,您才会失去向上移动视图的能力。我的意思是:如果您有一个包含多个文本字段的表单,并且您开始编辑顶部的第一个,您可能不希望它向上移动,因为它会移出屏幕。
  • 我真的很喜欢这个答案,但是像所有其他答案一样,如果您的视图位于 TabBar 内或视图未与屏幕底部齐平,则它不起作用。
【解决方案6】:

我创建了一个非常简单易用的视图修饰符。

使用下面的代码添加一个 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())
  }
}

【讨论】:

  • 会很酷,如果它只会偏移,如果必要的话(即不要滚动,如果键盘没有覆盖输入元素)。很高兴...
  • 这很好用,谢谢。实现也非常干净,对我来说,只在需要时滚动。
  • 太棒了!为什么不在 Github 或其他地方提供这个? :) 或者你可以向github.com/hackiftekhar/IQKeyboardManager 提出这个建议,因为他们还没有完整的 SwiftUI 支持
  • 不会很好地适应方向变化,并且无论是否需要都会抵消。
  • 这里的一个问题是这根本不是动画......创建一个非常紧张的动作?
【解决方案7】:

或者 你可以使用IQKeyBoardManagerSwift

并且可以选择将其添加到您的应用程序委托中以隐藏工具栏并启用在单击键盘以外的任何视图时隐藏键盘。

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

【讨论】:

  • 这对我来说确实是(出乎意料的)方式。固体。
  • 这个框架工作得比预期的还要好。谢谢分享!
  • 在 SwiftUI 上工作正常 - 感谢@DominatorVbN - 我在 iPad 横向模式下需要将 IQKeyboardManager.shared.keyboardDistanceFromTextField 增加到 40 以获得舒适的差距。
  • 还必须设置IQKeyboardManager.shared.enable = true 以防止键盘隐藏我的文本字段。无论如何,这是最好的解决方案。我有 4 个垂直排列的字段,其他解决方案适用于我最底部的字段,但会将最顶部的字段推到视野之外。
【解决方案8】:

您需要添加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 tutorial

当你得到它时,你需要配置一个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 以外的其他东西来调整滚动视图大小。

【讨论】:

  • 嘿,看来您在可绑定对象方面有经验。我无法让它按我的意愿工作。如果你能看一下就好了:stackoverflow.com/questions/56500147/…
  • 你为什么不使用@ObjectBinding
  • 不推荐使用BindableObject,不幸的是,这不再有效。
  • @LinusGeffarth 不管怎样,BindableObject 刚刚重命名为 ObservableObjectdidChange 重命名为 objectWillChange。该对象可以很好地更新视图(尽管我使用@ObservedObject 而不是@State 进行了测试)
  • 嗨,这个解决方案是滚动内容,但它在键盘上方显示了一些白色区域,隐藏了一半的文本字段。请告诉我如何去除白色区域。
【解决方案9】:

我审查了现有解决方案并将其重构为一个方便的 SPM 包,该包提供了 .keyboardAware() 修饰符:

KeyboardAwareSwiftUI

例子:

struct KeyboardAwareView: View {
    @State var text = "example"

    var body: some View {
        NavigationView {
            ScrollView {
                VStack(alignment: .leading) {
                    ForEach(0 ..< 20) { i in
                        Text("Text \(i):")
                        TextField("Text", text: self.$text)
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                            .padding(.bottom, 10)
                    }
                }
                .padding()
            }
            .keyboardAware()  // <--- the view modifier
            .navigationBarTitle("Keyboard Example")
        }

    }
}

来源:

import UIKit
import SwiftUI

public class KeyboardInfo: ObservableObject {

    public static var shared = KeyboardInfo()

    @Published public var height: CGFloat = 0

    private init() {
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), name: UIApplication.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), name: UIResponder.keyboardWillHideNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChanged), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
    }

    @objc func keyboardChanged(notification: Notification) {
        if notification.name == UIApplication.keyboardWillHideNotification {
            self.height = 0
        } else {
            self.height = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
        }
    }

}

struct KeyboardAware: ViewModifier {
    @ObservedObject private var keyboard = KeyboardInfo.shared

    func body(content: Content) -> some View {
        content
            .padding(.bottom, self.keyboard.height)
            .edgesIgnoringSafeArea(self.keyboard.height > 0 ? .bottom : [])
            .animation(.easeOut)
    }
}

extension View {
    public func keyboardAware() -> some View {
        ModifiedContent(content: self, modifier: KeyboardAware())
    }
}

【讨论】:

  • 我只看到了 textview 的一半高度。你知道如何解决这个问题吗?
  • 好。这节省了我的时间。在使用它之前,我们知道这个修饰符句柄视图的底部填充。
  • 这个对我来说是最好的。其他导致问题的原因是使用 mvvm。当视图初始化时,视图模型再次初始化,重置文本字段值。谢谢
【解决方案10】:

我使用 Benjamin Kindle 的回答作为起点,但我有一些问题想要解决。

  1. 这里的大多数答案都不涉及键盘更改其框架,因此如果用户在屏幕上使用键盘旋转设备,它们就会中断。将keyboardWillChangeFrameNotification 添加到已处理通知列表中可以解决此问题。
  2. 我不希望多个发布者具有相似但不同的地图闭包,因此我将所有三个键盘通知链接到一个发布者中。诚然,这是一个很长的链条,但每一步都非常简单。
  3. 我提供了接受@ViewBuilderinit 函数,这样您就可以像使用任何其他视图一样使用KeyboardHost 视图,并且只需在尾随闭包中传递您的内容,而不是将内容视图作为参数传递到init
  4. 正如 Tae 和 fdelafuente 在 cmets 中建议的那样,我换掉了 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 看到的问题?我在我的应用程序中使用此代码,它对我来说工作正常。
  • 能否请您在 iOS 中打开Pridictive keyboardSettings -> General -> Keyboard -> Pridictive。在这种情况下,它不会正确计算并向键盘添加填充
  • @GSerjo:我在运行 iOS 13.1 测试版的 iPad Touch(第 7 代)上启用了预测文本。它正确地为预测行的高度添加了填充。 (重要的是要注意,我不是在此处调整 keyboard 的高度,而是在视图本身的 padding 中添加。)尝试在调试映射中交换被注释掉并使用您为预测键盘获得的值。我会在另一条评论中发布日志。
  • 取消注释“调试”映射后,您可以看到分配给keyboardHeight 的值。在我的 iPod Touch(纵向)上,带有预测功能的键盘是 254 点。没有它是216分。我什至可以使用屏幕上的键盘关闭预测功能并正确更新填充。添加带有预测功能的键盘:Received UIKeyboardWillChangeFrameNotification with height 254.0Received UIKeyboardWillShowNotification with height 254.0 当我关闭预测文本时:Received UIKeyboardWillChangeFrameNotification with height 216.0
【解决方案11】:

上面的一些解决方案存在一些问题,不一定是“最干净”的方法。因此,我为下面的实现修改了一些内容。

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 是应用偏移代码时的最佳方法。

【讨论】:

  • 非常好的解决方案,强烈推荐!我添加了一个布尔值来指示键盘当前是否处于活动状态。
  • 最佳和最简单的解决方案,强烈推荐!
【解决方案12】:

这是改编自 @kontiki 构建的内容。我让它在 beta 8 / GM 种子下的应用程序中运行,其中需要滚动的字段是 NavigationView 内表单的一部分。这是键盘卫士:

//
//  KeyboardGuardian.swift
//
//  https://stackoverflow.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 在十进制键盘上获取 Done 工具栏或返回键的问题,所以我使用它在其他地方的水龙头上隐藏它:

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(不,我也没有将其转换为使用首选项)。

【讨论】:

  • 致投反对票的人:为什么?我试图添加一些有用的东西,所以我想知道您认为我是如何出错的
【解决方案13】:

我不确定 SwiftUI 的过渡/动画 API 是否完整,但您可以使用 CGAffineTransform.transformEffect

使用这样的已发布属性创建一个可观察的键盘对象:

    final class KeyboardResponder: ObservableObject {
    private var notificationCenter: NotificationCenter
    @Published var readyToAppear = false

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

    deinit {
        notificationCenter.removeObserver(self)
    }

    @objc func keyBoardWillShow(notification: Notification) {
        readyToAppear = true
    }

    @objc func keyBoardWillHide(notification: Notification) {
        readyToAppear = false
    }

}

那么您可以使用该属性重新排列您的视图,如下所示:

    struct ContentView : View {
    @State var textfieldText: String = ""
    @ObservedObject private var keyboard = KeyboardResponder()

    var body: some View {
        return self.buildContent()
    }

    func buildContent() -> some View {
        let mainStack = VStack {
            TextField("TextField1", text: self.$textfieldText)
            TextField("TextField2", text: self.$textfieldText)
            TextField("TextField3", text: self.$textfieldText)
            TextField("TextField4", text: self.$textfieldText)
            TextField("TextField5", text: self.$textfieldText)
            TextField("TextField6", text: self.$textfieldText)
            TextField("TextField7", text: self.$textfieldText)
        }
        return Group{
            if self.keyboard.readyToAppear {
                mainStack.transformEffect(CGAffineTransform(translationX: 0, y: -200))
                    .animation(.spring())
            } else {
                mainStack
            }
        }
    }
}

或更简单

VStack {
        TextField("TextField1", text: self.$textfieldText)
        TextField("TextField2", text: self.$textfieldText)
        TextField("TextField3", text: self.$textfieldText)
        TextField("TextField4", text: self.$textfieldText)
        TextField("TextField5", text: self.$textfieldText)
        TextField("TextField6", text: self.$textfieldText)
        TextField("TextField7", text: self.$textfieldText)
    }.transformEffect(keyboard.readyToAppear ? CGAffineTransform(translationX: 0, y: -50) : .identity)
            .animation(.spring())

【讨论】:

  • 我喜欢这个答案,但我不太确定 'ScreenSize.portrait' 的来源。
  • 嗨@MishaStone 感谢选择我的方法。 ScreenSize.portrait 是我创建的一个类,用于根据方向和百分比获取屏幕测量值……但您可以将其替换为翻译所需的任何值
【解决方案14】:

Xcode 12 beta 4 添加了一个新的视图修饰符ignoresSafeArea,您现在可以使用它来避开键盘。

.ignoresSafeArea([], edges: [])

这样可以避开键盘和所有安全区域边缘。如果不想避免,可以将第一个参数设置为.keyboard。它有一些怪癖,至少在我的视图层次结构设置中,但它似乎确实是 Apple 希望我们避免使用键盘的方式。

【讨论】:

    【解决方案15】:

    正如 Mark Krenek 和 Heiko 所指出的,Apple 似乎终于在 Xcode 12 beta 4 中解决了这个问题。事情进展很快。根据 2020 年 8 月 18 日发布的 Xcode 12 beta 5 发行说明“Form、List 和 TextEditor 不再将内容隐藏在键盘后面。(66172025)”。我刚刚下载了它,并在 beta 5 模拟器 (iPhone SE2) 中快速测试了它,并在我几天前启动的应用程序中使用了表单容器。

    它现在“只适用于”TextField。 SwiftUI 会自动为封装的 Form 提供适当的底部填充,为键盘腾出空间。它会自动向上滚动表单以在键盘上方显示 TextField。现在,当键盘出现时,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/

    【讨论】:

    • 我看到苹果发行说明,在 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) } Section { TextEditor(text: $text) .frame(height: 120) } } }
    【解决方案16】:

    用法:

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

    代码:

    import SwiftUI
    import Combine
    
    let keyboardSpaceD = KeyboardSpace()
    extension View {
        func keyboardSpace() -> some View {
            modifier(KeyboardSpace.Space(data: keyboardSpaceD))
        }
    }
    
    class KeyboardSpace: ObservableObject {
        var sub: AnyCancellable?
        
        @Published var currentHeight: CGFloat = 0
        var heightIn: CGFloat = 0 {
            didSet {
                withAnimation {
                    if UIWindow.keyWindow != nil {
                        //fix notification when switching from another app with keyboard
                        self.currentHeight = heightIn
                    }
                }
            }
        }
        
        init() {
            subscribeToKeyboardEvents()
        }
        
        private let keyboardWillOpen = NotificationCenter.default
            .publisher(for: UIResponder.keyboardWillShowNotification)
            .map { $0.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect }
            .map { $0.height - (UIWindow.keyWindow?.safeAreaInsets.bottom ?? 0) }
        
        private let keyboardWillHide =  NotificationCenter.default
            .publisher(for: UIResponder.keyboardWillHideNotification)
            .map { _ in CGFloat.zero }
        
        private func subscribeToKeyboardEvents() {
            sub?.cancel()
            sub = Publishers.Merge(keyboardWillOpen, keyboardWillHide)
                .subscribe(on: RunLoop.main)
                .assign(to: \.self.heightIn, on: self)
        }
        
        deinit {
            sub?.cancel()
        }
        
        struct Space: ViewModifier {
            @ObservedObject var data: KeyboardSpace
            
            func body(content: Content) -> some View {
                VStack(spacing: 0) {
                    content
                    
                    Rectangle()
                        .foregroundColor(Color(.clear))
                        .frame(height: data.currentHeight)
                        .frame(maxWidth: .greatestFiniteMagnitude)
    
                }
            }
        }
    }
    
    extension UIWindow {
        static var keyWindow: UIWindow? {
            let keyWindow = UIApplication.shared.connectedScenes
                .filter({$0.activationState == .foregroundActive})
                .map({$0 as? UIWindowScene})
                .compactMap({$0})
                .first?.windows
                .filter({$0.isKeyWindow}).first
            return keyWindow
        }
    }
    

    【讨论】:

    • 尝试了您的解决方案...视图仅滚动到文本字段的一半。尝试了以上所有解决方案。遇到了同样的问题。请帮忙!!!
    • @Zeona,试试一个简单的应用程序,你可能会做一些不同的事情。另外,如果您使用的是安全区域,请尝试删除 '- (UIWindow.keyWindow?.safeAreaInsets.bottom ?? 0)'。
    • removed (UIWindow.keyWindow?.safeAreaInsets.bottom ?? 0) 然后我在键盘上方得到一个空白
    • 它在 SwiftUI 中对我有用。谢谢你:)
    【解决方案17】:

    说实话,很多这些答案看起来真的很臃肿。如果您使用的是 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) 来匹配键盘出现的动画
    • (对于 iOS 13,请转到此答案的历史记录)
    • 嗨,.assign(to: \.keyboardHeight) 出现此错误“无法从上下文中推断键路径类型;考虑明确指定根类型”。请让我知道适用于 ios 13 和 ios 14 的正确且干净的解决方案。
    • 我不得不为 UIResponder.keyboardWillHideNotification 添加另一个监听器。除此之外 - 这是唯一对我有用的解决方案。谢谢!
    • 几个问题:assign 应该是.assign(to: \.keyboardHeight, on: self)(至少在 Xcode 12.5 中)。此外,您还需要监视UIResponder.keyboardWillHideNotification,触发时高度始终返回 0。截图:cln.sh/Sp6zKc
    【解决方案18】:

    至于 iOS 14(beta 4),它的工作原理非常简单:

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

    并且视图的大小会调整到键盘的顶部。 当然还有更多的改进可能 frame(.maxHeight: ...) 等 你会想办法的。

    不幸的是,iPad 上的浮动键盘在移动时仍然会出现问题。 但是上面提到的解决方案也可以, 而且它仍然是测试版,我希望他们能解决这个问题。

    感谢苹果,终于!

    【讨论】:

    • 这根本不起作用(14.1)。想法是什么?
    【解决方案19】:

    从这里复制答案:TextField always on keyboard top with 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)
      }
    }
    

    【讨论】:

    • 仅供参考,您在调用NotificationCenter.default.addObserver 时拥有这些对象...您需要存储这些对象并在适当的时间删除观察者...
    • 嗨@TheCodingArt,是的,我尝试过这样做(oleb.net/blog/2018/01/notificationcenter-removeobserver),但它似乎对我不起作用,有什么想法吗?
    【解决方案20】:

    处理TabView

    我喜欢Benjamin Kindle's answer,但它不支持 TabViews。 这是我对他处理 TabViews 代码的调整:

    1. UITabView添加一个扩展来存储tabView的大小,当它的frame被设置时。 我们可以将它存储在一个静态变量中,因为一个项目中通常只有一个 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. 您需要在KeyboardHost 视图底部更改他的onReceive 以考虑标签栏的高度:
    .onReceive(showPublisher.merge(with: hidePublisher)) { (height) in
                self.keyboardHeight = max(height - UITabBar.size.height, 0)
            }
    
    1. 就是这样!超级简单?。

    【讨论】:

      【解决方案21】:

      我采用了完全不同的方法,扩展了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 代码中的键盘。

      (注意:我还没有充分使用它,无法完全理解这样做的任何含义!)

      【讨论】:

        【解决方案22】:

        这是我在 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)
          }
        }
        

        您现在可以使用 Combine 创建 KeyboardModifier 以避免键盘隐藏 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() }
            }
          }
        }
        

        【讨论】:

          【解决方案23】:

          我的观点:

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

          基本上,根据键盘高度管理底部填充。

          【讨论】:

            【解决方案24】:

            我设法解决的最优雅的答案类似于 rraphael 的解决方案。创建一个类来监听键盘事件。但是,不要使用键盘大小来修改填充,而是返回键盘大小的负值,并使用 .offset(y:) 修饰符来调整最外面的视图容器的偏移量。它的动画效果很好,适用于任何视图。

            【讨论】:

            • 您是如何制作动画的?我有.offset(y: withAnimation { -keyboard.currentHeight }),但内容会跳转而不是动画。
            • 在几个测试版之前,我对这段代码感到厌烦,但在我之前发表评论时,只需要在运行时修改 vstack 的偏移量,SwiftUI 将为更改设置动画你。
            猜你喜欢
            • 1970-01-01
            • 2020-01-18
            • 2021-08-02
            • 1970-01-01
            • 1970-01-01
            • 2021-12-30
            • 1970-01-01
            • 2011-12-18
            • 2021-01-25
            相关资源
            最近更新 更多