使用SwiftUI在键盘出现时将TextField向上移动?:iOS

Hit*_*esh 31 ios swift swiftui

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"))
            }
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

输出量

Pre*_*zic 62

我尝试了许多建议的解决方案,尽管它们在大多数情况下都有效,但我遇到了一些问题 - 主要是安全区域(我在 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())
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

struct MyView: View {
    var body: some View {
        Form {...}
        .adaptsToKeyboard()
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 哇,这是所有版本中最 SwiftUI 的版本,带有 GeometryReader 和 ViewModifier。爱它。 (7认同)
  • 当我第二次使用键盘进入视图并单击“TextField”时,我在“rect.height - Geometry.safeAreaInsets.bottom”行上收到错误“Thread 1: signal SIGABRT”。我是否第一次单击“TextField”并不重要。该应用程序仍然崩溃。 (6认同)
  • 很酷,但不能与 ScrollView 一起使用,我这里有大表格 (6认同)
  • 这非常有用且优雅。非常感谢您写下这篇文章。 (3认同)
  • 您可以[通过 iOS 14 中的**一行代码**]来实现这一目标(/sf/answers/4450429021/) (3认同)
  • 我在键盘上看到一个小的空白视图。这个视图是GeometryReader View,我通过更改背景颜色来确认。知道为什么 GeometryReader 显示在我的实际视图和键盘之间。 (2认同)
  • 终于有东西可以用了!为什么苹果不能为我们做到这一点真是疯了! (2认同)
  • safeAreaInsets 上的崩溃似乎仅发生在 iOS13 上。@JLively,尝试这段代码: ```if #available(iOS 14, *) { return rect.height - Geometry.safeAreaInsets.bottom // 在 iOS 13 上,safeAreaInsets 为零并导致崩溃。显然目前它在 iOS 14 上仍然运行良好。 } else { return rect.height }``` (Predrag,如果您同意,您可以更新答案吗?) (2认同)

Moj*_*ini 43

从 iOS 14.2 开始,如果TextFields 有足够的移动空间,则默认情况下它们是键盘感知的。例如,如果它在一个VStack带一个Spacer看下面没有修饰符的旧演示代码


?? 似乎以下代码在 +iOS 14.2 上没有按预期工作

Xcode 12(到 iOS 14.2)——一行代码

将此修饰符添加到 TextField

.ignoresSafeArea(.keyboard, edges: .bottom)
Run Code Online (Sandbox Code Playgroud)

演示

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

  • 不幸的是,这只是 iOS 14... (10认同)
  • 这不是一个解决方案。该行实际上告诉编译器不要尊重控件上的安全区域(但这什么也不做)。只需删除该行,您就会看到完全相同的行为。在 iOS14 中,键盘回避是默认的。您的视图将缩小到屏幕大小减去键盘(如果有)。使用 .ignoresSafeArea 您实际上可以防止它在视图上发生。这就是为什么它被称为忽略安全区域。 (9认同)
  • 我自己想出来了。将 ```.ignoresSafeArea(.keyboard)``` 添加到您的视图中。 (2认同)

小智 29

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

使用下面的代码添加一个 Swift 文件,然后将此修饰符添加到您的视图中:

.keyboardResponsive()
Run Code Online (Sandbox Code Playgroud)
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())
  }
}

Run Code Online (Sandbox Code Playgroud)

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

kon*_*iki 27

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

}
Run Code Online (Sandbox Code Playgroud)

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

单击每个文本字段时,视图仅向上移动足以使单击的文本字段可见。

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

}
Run Code Online (Sandbox Code Playgroud)

几何标志

该视图吸收了其父视图的大小和位置。为了实现这一点,它在.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)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

更新我添加了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)
            }

        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 在某些情况下,如果您离开此屏幕,则在分配新矩形时,您可能会从 GeometryGetter 内的程序收到 SIGNAL ABORT。如果发生这种情况,您只需添加一些代码来验证几何体的大小是否大于零(geometry.size.width &gt; 0 &amp;&amp; Geometry.size.height &gt; 0),然后再为 self.rect 赋值 (3认同)
  • 这是可能的,但有什么好处呢?你会像这样附加它:```.modifier(GeometryGetter(rect: $kGuardian.rects[1]))``` 而不是 ```.background(GeometryGetter(rect: $kGuardian.rects[1]) )```。没有太大区别(仅少 2 个字符)。 (2认同)
  • 这对我来说也是如此,因为 self.rect = Geometry.frame(in: .global) 收到 SIGNAL ABORT 并尝试了所有建议的解决方案来解决此错误 (2认同)
  • 您可以[通过 iOS 14 中的**一行代码**]来实现这一目标(/sf/answers/4450429021/) (2认同)
  • @MojtabaHosseini 这行代码解决方案仅在视图中有一堆空白空间时才有效。在您的情况下,您有一个垫片,并且该垫片区域最终会缩小。如果您有多个控件并且没有可用/可收缩空间,则该解决方案不起作用。 (2认同)

Ami*_*ant 19

或者你可以只使用IQKeyBoardManagerSwift

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

IQKeyboardManager.shared.enableAutoToolbar = false
IQKeyboardManager.shared.shouldShowToolbarPlaceholder = false
IQKeyboardManager.shared.shouldResignOnTouchOutside = true
IQKeyboardManager.shared.previousNextDisplayMode = .alwaysHide
Run Code Online (Sandbox Code Playgroud)

  • 在 SwiftUI 上工作正常 - 感谢 @DominatorVbN - 我在 iPad 横向模式下需要将 `IQKeyboardManager.shared.keyboardDistanceFromTextField` 增加到 40 以获得舒适的间隙。 (2认同)

Ben*_*dle 18

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

很简单 我们为键盘显示/隐藏事件创建发布者,然后使用订阅它们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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,您可以使用如下视图:

var body: some View {
    KeyboardHost {
        viewIncludingKeyboard()
    }
}
Run Code Online (Sandbox Code Playgroud)

要向上移动视图的内容而不是缩小视图,可以添加填充或偏移量,view而不是将其放入带有矩形的VStack中。

  • 我认为这是正确的答案。我做了一个小小的调整:我不是在修改矩形,而是在修改self.view的填充,效果很好。动画完全没有问题 (5认同)
  • 这是一个很好的解决方案,但这里的主要问题是,仅当键盘隐藏您正在编辑的文本字段时,您才无法向上移动视图。我的意思是:如果您有一个包含多个文本字段的表单,并且您开始编辑顶部的第一个文本字段,您可能不希望它向上移动,因为它会移出屏幕。 (4认同)
  • 谢谢!完美运作。正如@Taed所说,最好使用填充方法。最终结果将是`var body:some view {VStack {view .padding(.bottom,keyboardHeight).animation(.default)} .onReceive(showPublisher.merge(with:hidePublisher)){(self.keyboardHeight中的高度= height}}` (2认同)
  • 尽管票数较少,但这仍然是最迅速的回复。之前使用 AnyView 的方法破坏了 Metal 加速帮助。 (2认同)

Mic*_*eas 10

为了构建@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
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

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))
    }
}
Run Code Online (Sandbox Code Playgroud)

发布会currentHeight触发UI的重新渲染,并在键盘显示时将TextField上移,而在关闭时将其下移。但是我没有使用ScrollView。

  • 我喜欢这个答案,因为它很简单。我添加了.animation(.easeOut(duration:0.16))以尝试匹配键盘向上滑动的速度。 (3认同)
  • 您可以尝试使用“keyboardFrameEndUserInfoKey”。这应该保存键盘的最终框架。 (2认同)

Ral*_*ert 9

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

键盘感知SwiftUI

例子:

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

    }
}
Run Code Online (Sandbox Code Playgroud)

来源:

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())
    }
}
Run Code Online (Sandbox Code Playgroud)


rra*_*ael 6

您需要添加a 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
    }
}
Run Code Online (Sandbox Code Playgroud)

BindableObject一致性将允许您使用这个类的State,并触发视图更新。如果需要,请查看以下教程BindableObjectSwiftUI教程

When you get that, you need to configure a ScrollView to reduce its size when the keyboard appear. For convenience I wrapped this ScrollView into some kind of component:

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

All you have to do now is to embed your content inside the custom 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

Edit: The scroll behaviour is really weird when the keyboard is hiding. Maybe using an animation to update the padding would fix this, or you should consider using something else than the padding to adjust the scroll view size.

  • 不幸的是,随着BindableObject的弃用,它不再起作用了。 (3认同)
  • @LinusGeffarth 就其价值而言,“BindableObject”刚刚重命名为“ObservableObject”,“didChange”更名为“objectWillChange”。该对象更新视图很好(尽管我使用“@ObservedObject”而不是“@State”进行了测试) (2认同)
  • 您好,这个解决方案正在滚动内容,但它显示键盘上方的一些白色区域,隐藏了一半的文本字段。请告诉我如何去除白色区域。 (2认同)

8su*_*has 6

用法:

import SwiftUI

var body: some View {
    ScrollView {
        VStack {
          /*
          TextField()
          */
        }
    }.keyboardSpace()
}
Run Code Online (Sandbox Code Playgroud)

代码:

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
    }
}
Run Code Online (Sandbox Code Playgroud)


Tim*_*ers 5

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

  1. 这里的大多数答案都没有涉及键盘改变其框架,因此如果用户在屏幕上的键盘上旋转设备,它们就会中断。添加keyboardWillChangeFrameNotification到处理的通知列表解决了这个问题。
  2. 我不希望多个发布者具有相似但不同的地图闭包,因此我将所有三个键盘通知链接到一个发布者中。诚然,这是一条长链,但每一步都非常简单。
  3. 我提供了init接受 a的函数,@ViewBuilder以便您可以KeyboardHost像使用任何其他 View 一样使用该视图,并且只需在尾随闭包中传递您的内容,而不是将内容视图作为参数传递给init.
  4. 正如 Tae 和 fdelafuente 在评论中建议的那样,我换掉了Rectangle调整底部填充的 。
  5. 我想使用UIWindowas 中提供的字符串,而不是使用硬编码的“UIKeyboardFrameEndUserInfoKey”字符串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"))
        }
    }
}

Run Code Online (Sandbox Code Playgroud)


The*_*Art 5

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

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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
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()
    }
}
Run Code Online (Sandbox Code Playgroud)

我本来希望采用更简洁的方法,并将如何偏移内容的责任转移到构建的视图(而不是修饰符)上,但是在将偏移代码移动到视图时,似乎我无法让发布者正确触发。 ...

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


Edw*_*ard 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 您好, .assign(to: \.keyboardHeight) 给出此错误“无法从上下文推断关键路径类型;考虑显式指定根类型”。请让我知道适用于 ios 13 和 ios 14 的正确且干净的解决方案。 (3认同)