SwiftUI 将部分列表固定到底部

SwiftUI pin partial list to bottom

我有一个聊天消息列表,当在末尾添加新消息时,该列表会向上滚动。但如果视图中的消息太少,它们会固定在顶部。

有没有办法把内容固定在底部?

请注意,在这种情况下,视图位于 UIHostingController.

struct OverlayChatView: View {
    @ObservedObject public  var stream: ChatStream
    
    var body: some View {
        ScrollViewReader { scrollView in
            ScrollView {
                LazyVStack(alignment: .leading, spacing: 0.0) {
                    ForEach(self.stream.messages) { inMsg in
                        ChatMessageCell(message: inMsg)
                    }
                    .padding(8)
                }
            }
            .onAppear {
                if self.stream.messages.count > 0
                {
                    scrollView.scrollTo(self.stream.messages.last!.id, anchor: .bottom)
                }
            }
            .onChange(of: self.stream.messages, perform: { inMessages in
                if inMessages.count < 1 { return }
                
                withAnimation(.linear(duration: 0.25)) {
                    scrollView.scrollTo(inMessages.last!.id, anchor: .bottom)
                }
            })
        }
    }
}

您可以根据内容高度与整个滚动视图高度之间的差异使用动态大小 Spacer。如果内容较小,则显示一个Spacer -- 如果不是,则只显示内容。


struct ContentView: View {
    @State private var contentHeight : CGFloat = 0
    @State private var totalHeight : CGFloat = 0
    
    var body: some View {
        ScrollViewReader { scroller in
            ScrollView {
                if contentHeight < totalHeight {
                    Spacer().frame(height: totalHeight - contentHeight)
                }
                VStack {
                    Text("My content")
                    Text("More content")
                }
                .frame(maxWidth: .infinity)
                .background(GeometryReader {
                    Color.clear.preference(key: ContentHeight.self,
                                           value: [=10=].frame(in: .local).size.height)
                })
                .onPreferenceChange(ContentHeight.self) { contentHeight = [=10=] }
            }
            .background(GeometryReader {
                Color.clear.preference(key: TotalHeight.self,
                                       value: [=10=].frame(in: .local).size.height)
            })
            .border(Color.red)
            .onPreferenceChange(TotalHeight.self) { totalHeight = [=10=] }
        }
    }
    
    struct TotalHeight : PreferenceKey {
        static var defaultValue: CGFloat { 0 }
        static func reduce(value: inout Value, nextValue: () -> Value) {
            value = value + nextValue()
        }
    }

    struct ContentHeight : PreferenceKey {
        static var defaultValue: CGFloat { 0 }
        static func reduce(value: inout Value, nextValue: () -> Value) {
            value = value + nextValue()
        }
    }
}

我对这个答案不是很满意,因为它需要一些硬编码,而且不像 ScrollView 中的直接支持那样优雅,只允许滚动到内容的末尾,但它是相当简单。

@jnpdx 的上述回答是正确的。

在我发布这个问题后的几周里,我学到了更多的 SwiftUI 技术。在这种情况下,您可以在 ScrollView 中添加几乎任意的填充和视图,在包含的 list/stack/whatever 内部和外部(但不在 ForEach 内部,这不会做您想要的!).

我将 ScrollViewReader 包装在 GeometryReader 中,以使内部视图可以使用整个聊天视图高度,然后我在 LazyVStack 顶部添加了等于高度减去等于一行聊天高度的硬编码量(这是我不喜欢的硬编码部分)。

struct OverlayChatView: View {
    @ObservedObject public  var stream: ChatStream
    
    var body: some View {
        GeometryReader
        { inChatView in
            ScrollViewReader { scrollView in
                ScrollView {
                    LazyVStack(alignment: .leading, spacing: 0.0) {
                        ForEach(self.stream.messages) { inMsg in
                            ChatMessageCell(message: inMsg)
                        }
                        .padding(.top, inChatView.size.height - 26.0)
                        .padding([.horizontal, .bottom], 8)
                    }
                }
                .onAppear {
                    if self.stream.messages.count > 0
                    {
                        scrollView.scrollTo(self.stream.messages.last!.id, anchor: .bottom)
                    }
                }
                .onChange(of: self.stream.messages, perform: { inMessages in
                    if inMessages.count < 1 { return }
                
                    withAnimation(.linear(duration: 0.25)) {
                        scrollView.scrollTo(inMessages.last!.id, anchor: .bottom)
                    }
                })
            }
        }
    }
}

这提供了 ScrollView 让您一直向下滚动所需的额外内容。