Private
Public Access
1
0

osx: better scroll view management

This commit is contained in:
2025-09-12 15:58:34 -07:00
parent 6261351598
commit bc51bf03a1
4 changed files with 71 additions and 42 deletions

View File

@@ -14,7 +14,7 @@ struct ConversationView: View
@Binding var entryModel: MessageEntryView.ViewModel
var body: some View {
VStack {
VStack(spacing: 0.0) {
TranscriptView(model: $transcriptModel)
MessageEntryView(viewModel: $entryModel)
}

Binary file not shown.

View File

@@ -10,7 +10,7 @@ import SwiftUI
extension TranscriptView.ViewModel
{
internal func rebuildDisplayItems(animated: Bool = false) {
internal func rebuildDisplayItems(animated: Bool = false, completion: () -> Void = {}) {
var displayItems: [DisplayItem] = []
var lastDate: Date = .distantPast
var lastSender: Display.Sender? = nil
@@ -53,6 +53,7 @@ extension TranscriptView.ViewModel
let animation: Animation? = animated ? .default : nil
withAnimation(animation) {
self.displayItems = displayItems
completion()
}
}
}

View File

@@ -12,25 +12,57 @@ struct TranscriptView: View
@Binding var model: ViewModel
@Environment(\.xpcClient) private var xpcClient
init(model: Binding<ViewModel>) {
self._model = model
}
var body: some View {
ScrollView {
LazyVStack(spacing: 6.0) {
ForEach($model.displayItems.reversed()) { item in
displayItemView(item.wrappedValue)
.id(item.id)
.scaleEffect(CGSize(width: 1.0, height: -1.0))
.transition(
.push(from: .top)
.combined(with: .opacity)
)
ScrollViewReader { proxy in
ScrollView {
// For resetting scroll position to the "bottom"
EmptyView()
.id(ViewID.bottomAnchor)
LazyVStack(spacing: 6.0) {
ForEach($model.displayItems.reversed()) { item in
displayItemView(item.wrappedValue)
.id(item.id)
.scaleEffect(CGSize(width: 1.0, height: -1.0))
.transition(
.push(from: .top)
.combined(with: .opacity)
)
}
}
.padding()
}
// Flip vertically so newest messages are at the bottom.
.scaleEffect(CGSize(width: 1.0, height: -1.0))
// Watch for xpc events
.task { await watchForMessageListChanges() }
// On conversation change, reload displayed messages and mark as read.
.onChange(of: model.displayedConversation) { oldValue, newValue in
Task {
guard oldValue != newValue else { return }
// Reload NOW
await model.reloadMessages(animated: false) {
// Once that's done, scroll to the "bottom" (actually top)
proxy.scrollTo(ViewID.bottomAnchor, anchor: .top)
}
}
Task.detached {
// Mark as read on server, and trigger a sync.
await model.markAsRead()
await model.triggerSync()
}
}
.padding()
}
.scaleEffect(CGSize(width: 1.0, height: -1.0))
.id(model.displayedConversation?.id)
.task { await watchForMessageListChanges() }
}
private func watchForMessageListChanges() async {
@@ -79,9 +111,16 @@ struct TranscriptView: View
}
}
}
// MARK: - Types
enum ViewID: String
{
case bottomAnchor
}
// MARK: - View Model
@Observable
class ViewModel
{
@@ -94,7 +133,6 @@ struct TranscriptView: View
init(messages: [Display.Message] = []) {
self.messages = messages
observeDisplayedConversation()
rebuildDisplayItems()
}
@@ -106,7 +144,7 @@ struct TranscriptView: View
needsReload = .yes(animated)
Task { @MainActor [weak self] in
guard let self else { return }
await reloadMessages()
await reloadIfNeeded()
}
}
@@ -115,22 +153,6 @@ struct TranscriptView: View
setNeedsReload(animated: false)
}
private func observeDisplayedConversation() {
withObservationTracking {
_ = displayedConversation
} onChange: {
Task { @MainActor [weak self] in
guard let self else { return }
await markAsRead()
await triggerSync()
setNeedsReload(animated: false)
observeDisplayedConversation()
}
}
}
func markAsRead() async {
guard let displayedConversation else { return }
@@ -150,11 +172,15 @@ struct TranscriptView: View
print("Error triggering sync: \(error)")
}
}
private func reloadMessages() async {
func reloadIfNeeded(completion: () -> Void = {}) async {
guard case .yes(let animated) = needsReload else { return }
needsReload = .no
await reloadMessages(animated: animated, completion: completion)
}
func reloadMessages(animated: Bool, completion: () -> Void) async {
guard let displayedConversation else { return }
do {
@@ -167,8 +193,10 @@ struct TranscriptView: View
// Only animate for incoming messages.
let shouldAnimate = (newIds.count == 1)
self.messages = clientMessages
self.rebuildDisplayItems(animated: animated && shouldAnimate)
await MainActor.run {
self.messages = clientMessages
self.rebuildDisplayItems(animated: animated && shouldAnimate, completion: completion)
}
} catch {
print("Message fetch error: \(error)")
}