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