ios: invert scroll view technique

This commit is contained in:
2026-05-03 21:21:03 -07:00
parent aff2531bf3
commit 0b94d5b3fa

View File

@@ -7,7 +7,6 @@ struct SybilChatTranscriptView: View {
var isSending: Bool
var topContentInset: CGFloat = 0
var bottomContentInset: CGFloat = 0
@State private var hasHandledInitialTranscriptScroll = false
private var hasPendingAssistant: Bool {
messages.contains { message in
@@ -16,66 +15,42 @@ struct SybilChatTranscriptView: View {
}
var body: some View {
ScrollViewReader { proxy in
ScrollView {
LazyVStack(alignment: .leading, spacing: 26) {
if isLoading && messages.isEmpty {
Text("Loading messages…")
ScrollView {
LazyVStack(alignment: .leading, spacing: 26) {
if isSending && !hasPendingAssistant {
HStack(spacing: 8) {
ProgressView()
.controlSize(.small)
.tint(SybilTheme.textMuted)
Text("Assistant is typing…")
.font(.sybil(.footnote))
.foregroundStyle(SybilTheme.textMuted)
.padding(.top, 24)
}
ForEach(messages) { message in
MessageBubble(message: message, isSending: isSending)
.frame(maxWidth: .infinity)
.id(message.id)
}
if isSending && !hasPendingAssistant {
HStack(spacing: 8) {
ProgressView()
.controlSize(.small)
.tint(SybilTheme.textMuted)
Text("Assistant is typing…")
.font(.sybil(.footnote))
.foregroundStyle(SybilTheme.textMuted)
}
.id("typing-indicator")
}
Color.clear
.frame(height: 2)
.id("chat-bottom-anchor")
.scaleEffect(x: 1, y: -1)
}
ForEach(messages.reversed()) { message in
MessageBubble(message: message, isSending: isSending)
.frame(maxWidth: .infinity)
.scaleEffect(x: 1, y: -1)
}
if isLoading && messages.isEmpty {
Text("Loading messages…")
.font(.sybil(.footnote))
.foregroundStyle(SybilTheme.textMuted)
.padding(.top, 24)
.scaleEffect(x: 1, y: -1)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 14)
.padding(.top, 18 + topContentInset)
.padding(.bottom, 18 + bottomContentInset)
}
.frame(maxWidth: .infinity, alignment: .leading)
.scrollDismissesKeyboard(.interactively)
.onAppear {
scrollToBottom(with: proxy, animated: false)
}
.onChange(of: messages.map(\.id)) { _, _ in
scrollToBottom(with: proxy, animated: hasHandledInitialTranscriptScroll && !isLoading)
hasHandledInitialTranscriptScroll = true
}
.onChange(of: isSending) { _, _ in
scrollToBottom(with: proxy, animated: hasHandledInitialTranscriptScroll)
}
}
}
private func scrollToBottom(with proxy: ScrollViewProxy, animated: Bool) {
if animated {
withAnimation(.easeOut(duration: 0.22)) {
proxy.scrollTo("chat-bottom-anchor", anchor: .bottom)
}
} else {
proxy.scrollTo("chat-bottom-anchor", anchor: .bottom)
.padding(.horizontal, 14)
.padding(.top, 18 + bottomContentInset)
.padding(.bottom, 18 + topContentInset)
}
.frame(maxWidth: .infinity, alignment: .leading)
.scrollDismissesKeyboard(.interactively)
.scaleEffect(x: 1, y: -1)
}
}