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


100

TextField我的主箱里有七个ContentView。当用户打开键盘时,其中一些TextField隐藏在键盘框架下。所以TextField当键盘出现时,我想分别向上移动。

我使用以下代码TextField在屏幕上添加。

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

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

输出:

输出量



1
@PrashantTukadiya感谢您的快速回复。我已经在Scrollview中添加了TextField,但仍然面临相同的问题。
Hitesh Surani

1
@DimaPaliychuk这行不通。它是SwiftUI
Prashant Tukadiya

28
自从第一个Objective C iPhone应用程序问世以来,键盘的显示及其在屏幕上的内容就变得模糊了。这是一直在解决的问题。我对苹果没有用SwiftUi解决这个问题感到失望。我知道此评论对任何人都无济于事,但我想提出这个问题,我们确实应该向苹果施加压力,要求其提供解决方案,而不是依靠社区来始终解决这一最常见的问题。
P. Ent

Answers:


64

为Xcode beta 7更新了代码。

您无需填充,ScrollViews或List即可实现此目的。尽管此解决方案也可以很好地与他们一起使用。我在这里包括两个例子。

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

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

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

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

打开键盘后,将3个文本字段向上移动足以保持其可见

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

        }
    }
}

1
GeometryGetter通过使其符合ViewModifier协议,是否可以将其作为视图修饰符附加到背景之外?
Sudara

2
可能,但是有什么收获呢?您将像这样附加它:.modifier(GeometryGetter(rect: $kGuardian.rects[1]))而不是.background(GeometryGetter(rect: $kGuardian.rects[1]))。差别不大(仅减少2个字符)。
kontiki

3
在某些情况下,如果您要离开此屏幕,则在分配新矩形时,可以从GeometryGetter内的程序中获取信号终止。如果发生这种情况,您只需添加一些代码以验证几何尺寸是否大于零(geometry.size.width> 0 && geometry.size.height> 0),然后再将值分配给self.rect
Julio Bailon

1
@JulioBailon我不知道为什么,但是geometry.frame摆脱DispatchQueue.main.asyncSIGNAL ABORT的帮助,现在将测试您的解决方案。更新:if geometry.size.width > 0 && geometry.size.height > 0分配self.rect帮助之前。
罗曼·瓦西里耶夫

2
这也对我不利,在self.rect = geometry.frame(in:.global)中获取信号中止并尝试了所有建议的解决方案来解决此错误
Marwan Roushdy

55

为了构建@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。


6
我喜欢这个答案,因为它很简单。我添加.animation(.easeOut(duration: 0.16))了一些尝试以匹配键盘向上滑动的速度。
Mark Moeykens

为什么将键盘的最大高度设置为340?
丹尼尔·瑞安

1
@DanielRyan有时键盘高度在模拟器中返回错误的值。我似乎无法找出解决当前问题的方法
Michael Neas

1
我自己还没有看到这个问题。也许它已在最新版本中修复。如果有(或将会有)更大的键盘,我不想锁定大小。
丹尼尔·瑞安

1
您可以尝试使用keyboardFrameEndUserInfoKey。那应该保留键盘的最后一帧。
Mathias Claassen

51

我尝试了许多建议的解决方案,即使它们在大多数情况下都可以使用,但我还是遇到了一些问题-主要涉及安全区域(我在TabView的选项卡中有一个Form)。

我最终结合了几种不同的解决方案,并使用GeometryReader来获取特定视图的安全区域底部插图并将其用于填充的计算中:

import SwiftUI
import Combine

struct AdaptsToKeyboard: ViewModifier {
    @State var currentHeight: CGFloat = 0

    func body(content: Content) -> some View {
        GeometryReader { geometry in
            content
                .padding(.bottom, self.currentHeight)
                .animation(.easeOut(duration: 0.16))
                .onAppear(perform: {
                    NotificationCenter.Publisher(center: NotificationCenter.default, name: UIResponder.keyboardWillShowNotification)
                        .merge(with: NotificationCenter.Publisher(center: NotificationCenter.default, name: UIResponder.keyboardWillChangeFrameNotification))
                        .compactMap { notification in
                            notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect
                    }
                    .map { rect in
                        rect.height - geometry.safeAreaInsets.bottom
                    }
                    .subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight))

                    NotificationCenter.Publisher(center: NotificationCenter.default, name: UIResponder.keyboardWillHideNotification)
                        .compactMap { notification in
                            CGFloat.zero
                    }
                    .subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight))
                })
        }
    }
}

用法:

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

7
哇,这是其中所有版本中最SwiftUI的版本,带有GeometryReader和ViewModifier。爱它。
Mateusz

3
这是如此有用和优雅。非常感谢您撰写本文。
Danilo Campos

2
我在键盘上看到一个小的空白白色视图。该视图是GeometryReader视图,我通过更改背景色来确认。知道为什么GeometryReader在实际的View和键盘之间显示。
user832'4

6
当我第二次使用键盘进入视图并单击时,出现Thread 1: signal SIGABRT在线错误。我是否第一次单击都没有关系。该应用程序仍然崩溃。rect.height - geometry.safeAreaInsets.bottomTextFieldTextField
JLively

2
终于有什么可行的!苹果为什么不能为我们做到这一点真是太疯狂了!
戴夫·科兹科夫斯基

35

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

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


6
我认为这是正确的答案。我做了一个小小的调整:我不是在修改矩形,而是修改矩形的填充,self.view效果很好。动画完全没有问题

5
谢谢!完美运作。正如@Taed所说,最好使用填充方法。最终结果将是var body: some View { VStack { view .padding(.bottom, keyboardHeight) .animation(.default) } .onReceive(showPublisher.merge(with: hidePublisher)) { (height) in self.keyboardHeight = height } }
fdelafuente

1
尽管票数较少,但这是最快的答复。以前使用AnyView的方法破坏了Metal加速帮助。
尼尔森·卡达奇

4
这是一个很好的解决方案,但是这里的主要问题是,只有当键盘隐藏了您正在编辑的文本字段时,您才失去上移视图的能力。我的意思是:如果您有一个包含多个文本字段的表单,并且开始在顶部编辑第一个表单,则可能不希望它向上移动,因为它将移出屏幕。
matteopuc

我真的很喜欢这个答案,但是和其他所有答案一样,如果您的视图位于TabBar内或视图未与屏幕底部齐平,则此方法将不起作用。
Ben Patch

25

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

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


2
如果有必要的话,如果它只会偏移的话,那将很酷(即,如果键盘没有覆盖输入元素,则不要滚动)。很高兴...
几十年

效果很好,谢谢。同样非常干净的实现,对我来说,只有在需要时才滚动。
约书亚

太棒了!您为什么不在Github或其他地方提供此功能?:)或者您可以向github.com/hackiftekhar/IQKeyboardManager建议,因为他们还没有完全的SwiftUI支持
Schnodderbalken

方向更改不会很好地起作用,并且无论是否需要它都会偏移。
GrandSteph

这里的一个问题是,这根本不是动画……创建了一个非常抖动的动作😢–
Zorayr

22

Xcode 12-一行代码

将此修改器添加到 TextField

.ignoresSafeArea(.keyboard, edges: .bottom)

演示版

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


它适用于Any View,包括TextEditor
Mojtaba Hosseini

@MojtabaHosseini如果我想防止这种行为怎么办?我有一个不希望移动的图像,当我打开键盘时,该图像现在正在向上移动。
kyrers

1
您可以提出问题并在此处链接。因此,我可以看一下您的可复制代码,看看我是否可以提供帮助:) @kyrers
Mojtaba Hosseini

2
我自己弄清楚了。添加.ignoresSafeArea(.keyboard)到您的视图。
leonboe20年

3
不幸的是,这仅是iOS 14 ...
Xaxxus

16

或者,您可以只使用IQKeyBoardManagerSwift

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

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

对于我来说,这的确是(意外的)方式。固体。
HelloTimo

这个框架比预期的还要好。谢谢你的分享!
理查德·普蒂埃

1
我可以在SwiftUI上正常工作-谢谢@DominatorVbN-在iPad横向模式下,我需要增加到IQKeyboardManager.shared.keyboardDistanceFromTextField40以保持舒适的差距。
理查德·格罗夫斯

还必须设置IQKeyboardManager.shared.enable = true为防止键盘隐藏我的文本字段。无论如何,这是最好的解决方案。我有4个垂直排列的字段,其他解决方案适用于我最底层的字段,但会将最上面的字段排除在外。
MisterEd

12

您需要添加aScrollView并设置键盘大小的底部填充,以便当键盘出现时内容可以滚动。

要获取键盘大小,您将需要使用“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,并触发视图更新。如果需要,请查看以下教程BindableObjectSwiftUI教程

收到该消息后,您需要配置aScrollView以在键盘出现时减小其大小。为了方便起见,我将其包装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/…–
SwiftiSwift

您为什么不使用@ObjectBinding
SwiftiSwift

3
随着BindableObject过时了,这是不工作了,很遗憾。
LinusGeffarth '19

2
@LinusGeffarth对于它的价值,BindableObject只是改名为ObservableObject,并didChangeobjectWillChange。对象更新视图就好了(尽管我使用@ObservedObject代替进行了测试@State
SeizeTheDay

嗨,这个解决方案是滚动内容,但是它在键盘上方显示了一些白色区域,隐藏了一半的文本字段。请让我知道如何删除白色区域。
Shahbaz Sajjad

6

我查看了现有解决方案并将其重构为一个方便的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高度。你知道如何解决吗?
Matrosov Alexander

好。这节省了我的时间。在使用此功能之前,我们知道此修改器将处理视图的底部填充。
Brownsoo Han

5

我以本杰明·金德勒(Benjamin Kindle)的回答作为起点,但是我想解决一些问题。

  1. 此处的大多数答案都不涉及键盘更改框架的问题,因此,如果用户在屏幕上显示键盘的情况下旋转设备,则答案会中断。将其添加keyboardWillChangeFrameNotification到已处理的通知列表中可解决此问题。
  2. 我不希望多个发布者使用相似但不同的地图闭合,因此我将所有三个键盘通知都链接到一个发布者中。公认这是一个漫长的过程,但是每个步骤都非常简单。
  3. 我提供了init接受a的函数,@ViewBuilder因此您可以KeyboardHost像使用其他任何View一样使用该视图,并且只需在尾随闭包中传递内容即可,而不是将content view作为参数传递给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"))
        }
    }
}


该解决方案不起作用,它会增加Keyboard高度
GSerjo

您能详细说明@GSerjo遇到的问题吗?我在我的应用程序中使用此代码,对我来说很好用。
蒂莫西·桑德斯

您能否Pridictive在iOS中打开keyboardSettings- > General- > Keyboard- > Pridictive。在这种情况下,它无法正确计算,并向键盘添加了填充
GSerjo

@GSerjo:我在运行iOS 13.1 Beta的iPad Touch(第7代)上启用了预测文本。它会为预测行的高度正确添加填充。(需要注意的是,我没有在这里调整键盘的高度,而是添加了视图本身的填充。)尝试在已注释掉的调试映射中进行交换,并使用为预测得到的值键盘。我将在另一个评论中发布日志。
蒂莫西·桑德斯

取消注释“调试”映射后,您可以看到分配给的值keyboardHeight。在我的iPod Touch(纵向)上,带预测功能的键盘为254点。没有它是216点。我什至可以用屏幕上的键盘关闭预测功能,并且填充正确更新。添加具有预测性的键盘:Received UIKeyboardWillChangeFrameNotification with height 254.0 Received UIKeyboardWillShowNotification with height 254.0 当我关闭预测性文本时:Received UIKeyboardWillChangeFrameNotification with height 216.0
Timothy Sanders

5

说实话,这些答案中的很多似乎真的很肿。如果您正在使用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)来匹配出现键盘的动画
Michiel Pelt

(对于iOS 13,请转到此答案的历史记录)
Loris Foe

1
嗨,.assign(to:\ .keyboardHeight)给出此错误“无法从上下文推断键路径类型;请考虑显式指定根类型”。请让我知道这两个IOS 13和iOS 14.适当的和干净的解决方案
沙巴兹·萨贾德

我必须为UIResponder.keyboardWillHideNotification添加另一个侦听器。除此之外-这是唯一对我有用的解决方案。谢谢!
安东宁Karásek

4

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

//
//  KeyboardGuardian.swift
//
//  /programming/56491881/move-textfield-up-when-thekeyboard-has-appeared-by-using-swiftui-ios
//

import SwiftUI
import Combine

/// The purpose of KeyboardGuardian, is to keep track of keyboard show/hide events and
/// calculate how much space the view needs to be shifted.
final class KeyboardGuardian: ObservableObject {
    let objectWillChange = ObservableObjectPublisher() // PassthroughSubject<Void, Never>()

    public var rects: Array<CGRect>
    public var keyboardRect: CGRect = CGRect()

    // keyboardWillShow notification may be posted repeatedly,
    // this flag makes sure we only act once per keyboard appearance
    private var keyboardIsHidden = true

    var slide: CGFloat = 0 {
        didSet {
            objectWillChange.send()
        }
    }

    public var showField: Int = 0 {
        didSet {
            updateSlide()
        }
    }

    init(textFieldCount: Int) {
        self.rects = Array<CGRect>(repeating: CGRect(), count: textFieldCount)

        NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyBoardDidHide(notification:)), name: UIResponder.keyboardDidHideNotification, object: nil)

    }

    @objc func keyBoardWillShow(notification: Notification) {
        if keyboardIsHidden {
            keyboardIsHidden = false
            if let rect = notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect {
                keyboardRect = rect
                updateSlide()
            }
        }
    }

    @objc func keyBoardDidHide(notification: Notification) {
        keyboardIsHidden = true
        updateSlide()
    }

    func updateSlide() {
        if keyboardIsHidden {
            slide = 0
        } else {
            slide = -keyboardRect.size.height
        }
    }
}

然后,我使用一个枚举来跟踪rects数组中的插槽和总数:

enum KeyboardSlots: Int {
    case kLogPath
    case kLogThreshold
    case kDisplayClip
    case kPingInterval
    case count
}

KeyboardSlots.count.rawValue是必需的阵列容量;其余的作为rawValue给出您将用于.background(GeometryGetter)调用的适当索引。

设置完成后,视图将通过以下方式进入KeyboardGuardian:

@ObservedObject private var kGuardian = KeyboardGuardian(textFieldCount: SettingsFormBody.KeyboardSlots.count.rawValue)

实际的运动是这样的:

.offset(y: kGuardian.slide).animation(.easeInOut(duration: 1))

附加到视图。就我而言,它被附加到整个NavigationView上,因此当键盘出现时,整个组件会向上滑动。

我还没有解决使用SwiftUI在十进制键盘上获取完成工具栏或返回键的问题,因此我将其隐藏在其他位置的水龙头上:

struct DismissingKeyboard: ViewModifier {
    func body(content: Content) -> some View {
        content
            .onTapGesture {
                let keyWindow = UIApplication.shared.connectedScenes
                        .filter({$0.activationState == .foregroundActive})
                        .map({$0 as? UIWindowScene})
                        .compactMap({$0})
                        .first?.windows
                        .filter({$0.isKeyWindow}).first
                keyWindow?.endEditing(true)                    
        }
    }
}

您将其附加到一个视图

.modifier(DismissingKeyboard())

有些视图(例如,选择器)不喜欢附加该视图,因此您可能需要在附加修饰符的方式上有些细化,而不仅仅是将其拍在最外面的视图上。

非常感谢@kontiki的辛勤工作。如他在示例中所示,您仍然需要上面的他的GeometryGetter(不,我也没有做将其转换为使用首选项的工作)。


1
对于不赞成的个人:为什么?我试图添加一些有用的内容,所以我想知道您认为我是怎么做的
Feldur

4

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

extension View {
    func onKeyboard(_ keyboardYOffset: Binding<CGFloat>) -> some View {
        return ModifiedContent(content: self, modifier: KeyboardModifier(keyboardYOffset))
    }
}

struct KeyboardModifier: ViewModifier {
    @Binding var keyboardYOffset: CGFloat
    let keyboardWillAppearPublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
    let keyboardWillHidePublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)

    init(_ offset: Binding<CGFloat>) {
        _keyboardYOffset = offset
    }

    func body(content: Content) -> some View {
        return content.offset(x: 0, y: -$keyboardYOffset.wrappedValue)
            .animation(.easeInOut(duration: 0.33))
            .onReceive(keyboardWillAppearPublisher) { notification in
                let keyWindow = UIApplication.shared.connectedScenes
                    .filter { $0.activationState == .foregroundActive }
                    .map { $0 as? UIWindowScene }
                    .compactMap { $0 }
                    .first?.windows
                    .filter { $0.isKeyWindow }
                    .first

                let yOffset = keyWindow?.safeAreaInsets.bottom ?? 0

                let keyboardFrame = (notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue ?? .zero

                self.$keyboardYOffset.wrappedValue = keyboardFrame.height - yOffset
        }.onReceive(keyboardWillHidePublisher) { _ in
            self.$keyboardYOffset.wrappedValue = 0
        }
    }
}
struct RegisterView: View {
    @State var name = ""
    @State var keyboardYOffset: CGFloat = 0

    var body: some View {

        VStack {
            WelcomeMessageView()
            TextField("Type your name...", text: $name).bordered()
        }.onKeyboard($keyboardYOffset)
            .background(WelcomeBackgroundImage())
            .padding()
    }
}

我本来希望有一种更清洁的方法,并将责任转移到构造的视图(而不是修饰符)上,以如何偏移内容,但是当偏移代码移至视图时,似乎无法让发布者正确触发。 ...

还要注意,由于final class当前会导致未知的异常崩溃(即使它满足界面要求),因此必须在此实例中使用Publisher,并且在应用偏移代码时,最好使用ScrollView整体。


非常好的解决方案,强烈建议!我添加了一个布尔值来指示键盘当前是否处于活动状态。
Peanutsmasher

3

我不确定SwiftUI的transition / animation 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”的来源。
Misha Stone,

嗨@MishaStone谢谢您选择我的方法。ScreenSize.portrait是我用来根据屏幕方向和百分比获取屏幕尺寸的类....但您可以将其替换为翻译所需的任何值
blacktiago

3

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

.ignoresSafeArea([], edges: [])

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


2

从这里复制的答案:TextField始终在SwiftUI的键盘顶部

我尝试了不同的方法,但没有一个对我有用。以下是唯一适用于不同设备的设备。

在文件中添加此扩展名:

import SwiftUI
import Combine

extension View {
    func keyboardSensible(_ offsetValue: Binding<CGFloat>) -> some View {
        
        return self
            .padding(.bottom, offsetValue.wrappedValue)
            .animation(.spring())
            .onAppear {
                NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { notification in
                    
                    let keyWindow = UIApplication.shared.connectedScenes
                        .filter({$0.activationState == .foregroundActive})
                        .map({$0 as? UIWindowScene})
                        .compactMap({$0})
                        .first?.windows
                        .filter({$0.isKeyWindow}).first
                    
                    let bottom = keyWindow?.safeAreaInsets.bottom ?? 0
                    
                    let value = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
                    let height = value.height
                    
                    offsetValue.wrappedValue = height - bottom
                }
                
                NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in
                    offsetValue.wrappedValue = 0
                }
        }
    }
}

在您看来,您需要一个变量来绑定offsetValue:

struct IncomeView: View {

  @State private var offsetValue: CGFloat = 0.0

  var body: some View { 
    
    VStack {
     //...       
    }
    .keyboardSensible($offsetValue)
  }
}

2
仅供参考,您在调用时拥有对象NotificationCenter.default.addObserver...您需要存储这些对象,并在适当的时候删除观察者...
TheCodingArt '20

@TheCodingArt大家好,是的,我已经尝试过这样做(oleb.net/blog/2018/01/notificationcenter-removeobserver),但这似乎对我不起作用,有什么想法吗?

2

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

现在,它对于TextField而言“有效” 。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/


我看到在beta5和beta6上测试过的Apple发行说明,TextField可以正常工作,TextEditor不是,我想念什么?@State var text =“” var body:some View {表格{部分{Text {text).frame(height:500)} Section {TextField(“ 5555”,text:$ text).frame(height:50)}第{文本编辑(文字:$文本).frame(身高:120)}}}
АндрейПервушин

2

用法:

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)。
8suhas

删除(UIWindow.keyWindow?.safeAreaInsets.bottom ?? 0),然后在键盘上方得到一个空白
Sona,2005年

1

处理TabView

我喜欢本杰明·金德勒的答案,但它不支持TabViews。这是我对他处理TabViews的代码的调整:

  1. 添加扩展名以UITabView存储设置框架时tabView的大小。 我们可以将其存储在一个静态变量中,因为在一个项目中通常只有一个tabView(如果您有多个,则需要进行调整)。
extension UITabBar {

    static var size: CGSize = .zero

    open override var frame: CGRect {
        get {
            super.frame
        } set {
            UITabBar.size = newValue.size
            super.frame = newValue
        }
    }
}
  1. 您需要onReceiveKeyboardHost视图底部更改其位置,以说明选项卡栏的高度:
.onReceive(showPublisher.merge(with: hidePublisher)) { (height) in
            self.keyboardHeight = max(height - UITabBar.size.height, 0)
        }
  1. 就是这样!超级简单🎉。

1

通过扩展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代码中的键盘。

(注意:到目前为止,我还没有足够地使用它来完全理解这样做的含义!)


1

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

现在,您可以使用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() }
    }
  }
}

1

对于iOS 14(测试版4),它的工作原理非常简单:

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

并且视图的大小调整到键盘的顶部。当然,使用frame(.maxHeight:...)还有更多可能的改进。您会弄清楚的。

不幸的是,iPad上的浮动键盘在移动时仍会引起问题。但是上述解决方案也可以,并且仍然是beta版,我希望他们能够解决。

最后,苹果!


这根本不起作用(14.1)。有什么想法?
Burgler-dev

0

我的观点:

struct AddContactView: View {
    
    @Environment(\.presentationMode) var presentationMode : Binding<PresentationMode>
    
    @ObservedObject var addContactVM = AddContactVM()
    
    @State private var offsetValue: CGFloat = 0.0
    
    @State var firstName : String
    @State var lastName : String
    @State var sipAddress : String
    @State var phoneNumber : String
    @State var emailID : String
    
  
    var body: some View {
        
        
        VStack{
            
            Header(title: StringConstants.ADD_CONTACT) {
                
                self.presentationMode.wrappedValue.dismiss()
            }
            
           ScrollView(Axis.Set.vertical, showsIndicators: false){
            
            Image("contactAvatar")
                .padding(.top, 80)
                .padding(.bottom, 100)
                //.padding(.vertical, 100)
                //.frame(width: 60,height : 60).aspectRatio(1, contentMode: .fit)
            
            VStack(alignment: .center, spacing: 0) {
                
                
                TextFieldBorder(placeHolder: StringConstants.FIRST_NAME, currentText: firstName, imageName: nil)
                
                TextFieldBorder(placeHolder: StringConstants.LAST_NAME, currentText: lastName, imageName: nil)
                
                TextFieldBorder(placeHolder: StringConstants.SIP_ADDRESS, currentText: sipAddress, imageName: "sipPhone")
                TextFieldBorder(placeHolder: StringConstants.PHONE_NUMBER, currentText: phoneNumber, imageName: "phoneIcon")
                TextFieldBorder(placeHolder: StringConstants.EMAILID, currentText: emailID, imageName: "email")
                

            }
            
           Spacer()
            
        }
        .padding(.horizontal, 20)
        
            
        }
        .padding(.bottom, self.addContactVM.bottomPadding)
        .onAppear {
            
            NotificationCenter.default.addObserver(self.addContactVM, selector: #selector(self.addContactVM.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
            
             NotificationCenter.default.addObserver(self.addContactVM, selector: #selector(self.addContactVM.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
        }
        
    }
}

我的虚拟机:

class AddContactVM : ObservableObject{
    
    @Published var contact : Contact = Contact(id: "", firstName: "", lastName: "", phoneNumbers: [], isAvatarAvailable: false, avatar: nil, emailID: "")
    
    @Published var bottomPadding : CGFloat = 0.0
    
    @objc  func keyboardWillShow(_ notification : Notification){
        
        if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
            let keyboardRectangle = keyboardFrame.cgRectValue
            let keyboardHeight = keyboardRectangle.height
            self.bottomPadding = keyboardHeight
        }
        
    }
    
    @objc  func keyboardWillHide(_ notification : Notification){
        
        
        self.bottomPadding = 0.0
        
    }
    
}

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


-3

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


您是如何获得动画的?我有.offset(y: withAnimation { -keyboard.currentHeight }),但是内容跳了起来,而不是动画。
jjatie

我是在几个beta之前弄混了这段代码,但是在我之前的评论中,只需要在运行时修改vstack的偏移量,SwiftUI就会为您创建变化的动画。
pcallycat '19
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.