当使用iOS 15/Xcode 13的键盘出现时,如何让ScrollView保持其位置? [英] How do I get the ScrollView to keep its position when the keyboard appears with iOS 15/Xcode 13?

查看:10
本文介绍了当使用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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆