当使用iOS 15/Xcode 13的键盘出现时,如何让ScrollView保持其位置? [英] How do I get the ScrollView to keep its position when the keyboard appears with iOS 15/Xcode 13?
本文介绍了当使用iOS 15/Xcode 13的键盘出现时,如何让ScrollView保持其位置?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!
问题描述
我有一个ScrollView,下面有一个Textfield,两者都在VStack中。当使用iOS 15/Xcode 13的键盘出现时,如何让ScrollView保持其位置?更准确地说,如果我在第100行,我希望当键盘在那里时,ScrollView仍然显示第100行,我该怎么做(我不知道)?
当您在对话中并开始输入文本时,我希望与Facebook Messenger具有相同的行为。
请参阅下面的图片以更好地了解我想要的内容。
滚动到第100行
使用键盘得到的结果(这不是我想要的)。
我的代码:
import SwiftUI
struct TestKeyboardScrollView2: View {
@State var textfield: String = ""
var body: some View {
VStack {
ScrollView {
LazyVStack {
ForEach(1...100, id: .self) { index in
Text("Row (index)")
}
}
}
TextField("Write here...", text: $textfield)
}
.padding()
}
}
推荐答案
它实际上是在坚守阵地。只是你正在观看它的窗口已经移动了。如果你看上面,你会发现这些行仍然是一样的。您要做的是将位于底部的行向上移动。如何使用ScrollViewReader
来完成此操作。通过给出的示例,它非常简单:
struct TestKeyboardScrollView2: View {
@State var textfield: String = ""
@FocusState private var keyboardVisible: Bool
var body: some View {
VStack {
ScrollViewReader { scroll in
ScrollView {
LazyVStack {
ForEach(1...100, id: .self) { index in
Text("Row (index)")
}
}
}
.onChange(of: keyboardVisible, perform: { _ in
if keyboardVisible {
withAnimation(.easeIn(duration: 1)) {
scroll.scrollTo(100) // this would be your array.count - 1,
// but you hard coded your ForEach
}
// The scroll has to wait until the keyboard is fully up
// this causes it to wait just a bit.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// this helps make it look deliberate and finished
withAnimation(.easeInOut(duration: 1)) {
scroll.scrollTo(100) // this would be your array.count - 1,
// but you hard coded your ForEach
}
}
}
})
}
TextField("Write here...", text: $textfield)
.focused($keyboardVisible)
}
}
}
编辑:
我已经能够解决这99%的问题。关键是能够确定一个视图何时真正出现在屏幕上。这是通过知道视图何时被滚动,然后测试每个视图以查看它是否在父视图的可见区域内来完成的。此视图还跟踪视图滚动的时间。滚动结束时,视图将获取列表中的最后一行,并将其锚定在屏幕底部。这将导致该行在屏幕上完全锁定。可以通过删除.onReceive(publisher)
来禁用此行为。键盘高度也会被跟踪,任何时候它大于零,屏幕上的行列表就会被锁定,一旦键盘经过一段延迟完全打开,最后一行就会再次固定到底部。当键盘关闭时,同样的事情发生在相反的情况下,当键盘高度达到0时,锁再次被移除。代码已添加注释,但如有任何问题,请提问。
struct ListWithSnapTo: View {
@State var messages = Message.dataArray()
@State var textfield: String = ""
@State var visibileIndex: [Int:Message] = [:]
@State private var keyboardVisible = false
@State private var readCells = true
let scrollDetector: CurrentValueSubject<CGFloat, Never>
let publisher: AnyPublisher<CGFloat, Never>
init() {
// This sets a publisher to keep track of whether the view is scrolling
let detector = CurrentValueSubject<CGFloat, Never>(0)
self.publisher = detector
.debounce(for: .seconds(0.2), scheduler: DispatchQueue.main)
.dropFirst()
.eraseToAnyPublisher()
self.scrollDetector = detector
}
var body: some View {
GeometryReader { outerProxy in
ScrollViewReader { scroll in
List {
ForEach(Array(zip(messages.indices, messages)), id: .1) { (index, message) in
GeometryReader { geometry in
Text(message.messageText)
// These rows fill in from the bottom to the top
.onChange(of: geometry.frame(in: .named("List"))) { innerRect in
if readCells {
if isInView(innerRect: innerRect, isIn: outerProxy) {
visibileIndex[index] = message
} else {
visibileIndex.removeValue(forKey: index)
}
}
}
}
}
// The preferenceKey keeps track of the fact that the view is scrolling.
.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self,
value: -$0.frame(in: .named("List")).origin.y)
})
.onPreferenceChange(ViewOffsetKey.self) { scrollDetector.send($0) }
}
// This tages the list as a coordinate space I want to use in a geometry reader.
.coordinateSpace(name: "List")
.onAppear(perform: {
// Moves the view so that the cells on screen are recorded.
scroll.scrollTo(messages[0], anchor: .top)
})
// This keeps track of whether the keyboard is up or down by its actual appearance on the screen.
// The change in keyboardVisible allows the reset for the last cell to be set just above the keyboard.
// readCells is a flag that prevents the scrolling from changing the last view.
.onReceive(Publishers.keyboardHeight) { keyboardHeight in
if keyboardHeight > 0 {
keyboardVisible = true
readCells = false
} else {
keyboardVisible = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
readCells = true
}
}
}
// This keeps track of whether the view is scrolling. If it is, it waits a bit,
// and then sets the last visible message to the very bottom to snap it into place.
// Remove this if you don't want this behavior.
.onReceive(publisher) { _ in
if !keyboardVisible {
guard let lastVisibleIndex = visibileIndex.keys.max(),
let lastVisibleMessage = visibileIndex[lastVisibleIndex] else { return }
withAnimation(.easeOut) {
scroll.scrollTo(lastVisibleMessage, anchor: .bottom)
}
}
}
.onChange(of: keyboardVisible) { _ in
guard let lastVisibleIndex = visibileIndex.keys.max(),
let lastVisibleMessage = visibileIndex[lastVisibleIndex] else { return }
if keyboardVisible {
// Waits until the keyboard is up. 0.25 seconds seems to be the best wait time.
// Too early, and the last cell hides behind the keyboard.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
// this helps make it look deliberate and finished
withAnimation(.easeOut) {
scroll.scrollTo(lastVisibleMessage, anchor: .bottom)
}
}
} else {
withAnimation(.easeOut) {
scroll.scrollTo(lastVisibleMessage, anchor: .bottom)
}
}
}
TextField("Write here...", text: $textfield)
}
}
.navigationTitle("Scrolling List")
}
private func isInView(innerRect:CGRect, isIn outerProxy:GeometryProxy) -> Bool {
let innerOrigin = innerRect.origin.y
// This is an estimated row height based on the height of the contents plus a basic amount for the padding, etc. of the List
// Have not been able to determine the actual height of the row. This may need to be adjusted.
let rowHeight = innerRect.height + 22
let listOrigin = outerProxy.frame(in: .global).origin.y
let listHeight = outerProxy.size.height
if innerOrigin + rowHeight < listOrigin + listHeight && innerOrigin > listOrigin {
return true
}
return false
}
}
extension Notification {
var keyboardHeight: CGFloat {
return (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
}
}
extension Publishers {
static var keyboardHeight: AnyPublisher<CGFloat, Never> {
let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
.map { $0.keyboardHeight }
let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
return MergeMany(willShow, willHide)
.eraseToAnyPublisher()
}
}
struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
struct Message: Identifiable, Hashable {
let id = UUID()
let messageText: String
let date = Date()
static func dataArray() -> [Message] {
var messArray: [Message] = []
for i in 1...100 {
messArray.append(Message(messageText: "message (i.description)"))
}
return messArray
}
}
这篇关于当使用iOS 15/Xcode 13的键盘出现时,如何让ScrollView保持其位置?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!
查看全文