当键盘出现在 SwiftUI 中时,向上移动 TextField

我的主 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"))
}
}
}

产出:

Output

96839 次浏览

您需要添加一个 ScrollView并设置一个键盘大小的底部填充,这样当键盘出现时内容将能够滚动。

要获得键盘大小,您需要使用 NotificationCenter来注册键盘事件。您可以使用一个自定义类来这样做:

import SwiftUI
import Combine


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


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


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


deinit {
_center.removeObserver(self)
}


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


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

BindableObject一致性允许您将此类用作 State并触发视图更新。如果需要,可以看看 BindableObject: SwiftUI 教程的教程

当您得到这个结果时,您需要配置一个 ScrollView,以便在键盘出现时减小它的大小。为了方便起见,我将这个 ScrollView包装成某种组件:

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


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


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

现在您所要做的就是将内容嵌入到自定义 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以外的其他方法来调整滚动视图的大小。

Xcode 的更新代码 Beta 7。

无需填充、 ScrollView 或 List 即可实现此目的。虽然这个解决方案对他们也有好处。我在这里包括两个例子。

第一个向上移动 所有 textField,如果键盘为其中任何一个显示的话。但只有在必要的时候。如果键盘没有隐藏文本字段,它们将不会移动。

在第二个示例中,视图只移动了足够的位置,以避免隐藏活动文本字段。

这两个示例都使用相同的公共代码: 几何学键盘守护者

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

When the keyboard is opened, the 3 textfields are moved up enough to keep then all visible

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


}

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

When each text field is clicked, the view is only moved up enough to make the clicked text field visible.

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


}

几何学

这是一个吸收了其父视图的大小和位置的视图。为了实现这一点,它在。背景修饰符背景修饰符。这是一个非常强大的修饰符,而不仅仅是一种装饰视图背景的方法。将视图传递给。(MyView ()) ,MyView 将获得修改后的视图作为父视图。使用几何阅读器可以让视图知道父级的几何形状。

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

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.sync,以避免在呈现视图时修改其状态的可能性。***

键盘守护者

KeyboardGuardian 的目的是跟踪键盘显示/隐藏事件,并计算视图需要移动的空间。

更新: < em > 我修改了 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)
}


}
}
}

我得到的最优雅的答案,类似于拉斐尔的解答。创建一个类来监听键盘事件。不过,不要使用键盘大小来修改填充,而是返回键盘大小的负值,并使用。偏移量(y:)修饰符来调整外部大多数视图容器的偏移量。它的动画效果足够好,可以在任何视图下工作。

我创建了一个视图,它可以包装任何其他视图,以便在键盘出现时收缩它。

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

这是改编自@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 中的其他函数提供适当的索引,用于.backing(GeometyGetter)调用。

有了这个设置,在键盘卫报上可以看到以下内容:

@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 的辛勤工作。你仍然需要他上面的几何格式(不,我也没有把它转换成使用首选项) ,正如他在他的例子中所说明的那样。

我以本杰明 · 金德尔的回答作为出发点,但是我有一些问题想要解决。

  1. 这里的大多数答案都不涉及键盘改变框架的问题,所以如果用户在屏幕上旋转键盘,它们就会中断。将 keyboardWillChangeFrameNotification添加到已处理通知的列表中。
  2. 我不想要多个具有相似但不同的 map 闭包的发布者,所以我将所有三个键盘通知链接到一个发布者中。无可否认,这是一条很长的链条,但每一步都非常简单。
  3. 我提供了接受 @ViewBuilderinit函数,这样您就可以像使用其他 View 一样使用 KeyboardHost视图,并且只需将内容传递到尾部闭包中,而不是将内容视图作为参数传递给 init
  4. 正如 Tae 和 fdelafuente 在评论中建议的那样,我调整了 Rectangle来调整底部填充。
  5. 我不想使用硬编码的“ UIKeyboardFrameEndUserInfoKey”字符串,而想使用 UIWindow中提供的字符串作为 UIWindow.keyboardFrameEndUserInfoKey

综合这些,我有:

struct KeyboardHost<Content>: View  where Content: View {
var content: Content


/// The current height of the keyboard rect.
@State private var keyboardHeight = CGFloat(0)


/// A publisher that combines all of the relevant keyboard changing notifications and maps them into a `CGFloat` representing the new height of the
/// keyboard rect.
private let keyboardChangePublisher = NotificationCenter.Publisher(center: .default,
name: UIResponder.keyboardWillShowNotification)
.merge(with: NotificationCenter.Publisher(center: .default,
name: UIResponder.keyboardWillChangeFrameNotification))
.merge(with: NotificationCenter.Publisher(center: .default,
name: UIResponder.keyboardWillHideNotification)
// But we don't want to pass the keyboard rect from keyboardWillHide, so strip the userInfo out before
// passing the notification on.
.map { Notification(name: $0.name, object: $0.object, userInfo: nil) })
// Now map the merged notification stream into a height value.
.map { ($0.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero).size.height }
// If you want to debug the notifications, swap this in for the final map call above.
//        .map { (note) -> CGFloat in
//            let height = (note.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero).size.height
//
//            print("Received \(note.name.rawValue) with height \(height)")
//            return height
//    }


var body: some View {
content
.onReceive(keyboardChangePublisher) { self.keyboardHeight = $0 }
.padding(.bottom, keyboardHeight)
.animation(.default)
}


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


struct KeyboardHost_Previews: PreviewProvider {
static var previews: some View {
KeyboardHost {
TextField("TextField", text: .constant("Preview text field"))
}
}
}


为了构建@rraphael 的解决方案,我将其转换为可以使用的 xcode11 Swift UI 支持。

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。

我不确定 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())

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

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


从这里复制的答案: 使用 SwiftUI 时,TextField 始终位于键盘顶部

我试过不同的方法,没有一个对我有用。下面这个是唯一一个适用于不同设备的。

在文件中添加此扩展名:

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

正在处理 TabView

我喜欢 本杰明 · 金德尔的回答,但它不支持 TabView。 下面是我对他处理 TabView 的代码的调整:

  1. UITabView中添加一个扩展,以便在设置 tabView 的帧时存储它的大小。 我们可以把它存储在一个静态变量中,因为一个项目中通常只有一个 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. 就是这样,超级简单。

我尝试了许多提议的解决方案,尽管它们在大多数情况下都有效,但我还是遇到了一些问题——主要是安全区域的问题(我在 TabView 的选项卡中有一个 Form)。

我最终结合了几个不同的解决方案,并使用几何阅读器,以获得特定视图的安全区底部插入,并使用它在填充的计算:

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

或者 你可以使用 < a href = “ https://github.com/hackiftekhar/IQKeyboardManager”rel = “ norefrer”> IQKeyBoardManagerSwift

并可以选择添加到您的应用程序委托隐藏工具栏,并启用隐藏键盘上点击任何其他视图而不是键盘。

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

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

(注意: 我还没有充分利用这一点,但是,完全理解这样做的任何含义!)

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

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

我希望有一个更干净的方法,并把责任转移到构建的视图(而不是修饰符) ,在如何抵消内容,但似乎我不能让发布者正确地触发时,移动抵消代码的视图..。

还要注意,在这个实例中必须使用 Publisher,因为 final class当前会导致未知的异常崩溃(即使它满足接口要求) ,并且 ScrollView 总体上是应用偏移量代码时的最佳方法。

这是我在 SwiftUI 中处理键盘的方式。需要记住的是,它是在它所连接的 VStack 上进行计算的。

你可以在视图中使用它作为修饰符。这样:

struct LogInView: View {


var body: some View {
VStack {
// Your View
}
.modifier(KeyboardModifier())
}
}

因此,要使用这个修饰符,首先,创建一个 UIResponder 的扩展,以获得 VStack 中选定的 TextField 位置:

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

现在,您可以使用组合键创建 Keyboard 修改器,以避免键盘隐藏 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() }
}
}
}

说实话,很多答案看起来都很浮夸。如果您正在使用 SwiftUI,那么您也可以使用组合。

创建一个如下所示的 KeyboardResponder,然后您可以像前面演示的那样使用它。

为 iOS14更新。

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

我回顾了现有的解决方案,并将其重构为一个方便的 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())
}
}

用法:

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
.first { $0.activationState == .foregroundActive }
.flatMap { $0 as? UIWindowScene }?.windows
.first { $0.isKeyWindow }
return keyWindow
}
}

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

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

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

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

谢谢苹果,终于!

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

.ignoresSafeArea([], edges: [])

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

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

它现在“只是工程”的 文本字段。SwiftUI 将自动为封装的 Form 提供适当的底部填充,为键盘腾出空间。它会自动向上滚动窗体,在键盘上方显示 TextField。现在,当键盘启动时,ScrollView 容器运行良好。

然而,正如评论中指出的,TextEditor 有一个问题。Beta 5 & 6将自动为封装窗体提供适当的底部填充,为键盘腾出空间。但它不会自动向上滚动表单。键盘将覆盖 TextEditor。因此,与 TextField 不同,用户必须滚动 Form 才能使 TextEditor 可见。我会提交一份错误报告。也许 Beta 7会修好它。如此接近..。

Https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-14-beta-release-notes/

从 iOS 14.2开始,如果 TextField有足够的空间移动,它们默认能够识别键盘。例如,如果它是在一个 VStack和一个 Spacer(查看下面 < strong > 没有 修饰符的旧演示代码)


Something 看起来下面的代码在 + iOS 14.2中不能正常工作

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

将此修饰符添加到 TextField

.ignoresSafeArea(.keyboard, edges: .bottom)

Demo

苹果添加了键盘作为一个安全区域,所以你可以使用它来移动 任何 View与键盘一样的其他区域。

我的观点:

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
        

}
    

}

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

如果您使用 iOS14 + 和 scrollview,或者可以选择使用 scrollview。

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)

希望这对某些人有所帮助:)

enter image description here

如果您希望屏幕设计成这样,那么您可以使用如下所示的覆盖图。

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

如果希望在 textField 上重叠键盘,请使用以下代码。

enter image description here

  .ignoresSafeArea(.keyboard, edges: .bottom)

在父 Vstack 后面添加这一行。

这里有一个不同的方法,我必须做的,使它在 iOS15的工作

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的回报,所以我不得不像这样将它们分开。.希望这能帮到某人:)

我已经浏览了这里的每一个解决方案,尽管其中一些解决方案得到了很好的实现,但是它们中的任何一个都没有正确地显示一半的文本字段。而且,除非您偏移了内容的-y 坐标,否则任何解决方案都不能使用 TextEditor 控件,因为这样看起来会很奇怪。用户需要能够滚动通过所有的表单字段,甚至当键盘显示。

该场景是当您有一个视图,其中包含一个带有 ScrollView 的窗体,该窗体具有许多文本字段,包括一个 Text 编辑器字段和一个按钮,该按钮始终可见于窗体的底部。忽略安全区域(。键盘)。我还在研究这个问题。如果有人有一个完整的解决方案,请协助。

我还发现,不幸的是,当使用。忽略安全区域(。如果我使用 ScrollViewReader 和上面的任何解决方案结合使用,scrollTo 根本不起作用。