so I am trying to write a chat app in swiftui. I’m using firestore as my database, and I’m trying to fetch more messages while maintaining the ui view. I want a ui update that looks like instagram’s effect (https://youtube.com/shorts/ZBGCDmPy8CE, a loading circle then new messages show in the top of the current ui). But in my own app (https://youtu.be/EnemRxESOm4), when I fetch more messages, the ui will re-render then jump to the message that I want, which makes the user experience laggy. How do I make the effect that is same as instagram?
Also, is there a more elegent way to detect wether the user scrolls to the top of the vstack? I’m using ScrollOffsetPreferenceKey to keep monitor where the view is, which is annoying since I need to keep updating the variable.
These are my code:
struct chatview: View {
@State private var scrollPosition: CGPoint = .zero
@State var messages: [Int] = Array(1...30)
@State var nextElement = 31
@State var lastScrollPosition = 1
@State private var lastAddElementTime: Date = Date()
private let addElementInterval: TimeInterval = 3.0 // only update at most once every three seconds
var body: some View {
NavigationView {
ScrollView {
ScrollViewReader { proxy in
VStack {
ForEach(messages.reversed(), id: .self) { message in
Text("message (message)")
.frame(height: 30)
.id(message)
}
}
.background(GeometryReader { geometry in
Color.clear
.preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("scroll")).origin)
})
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
self.scrollPosition = value
if self.scrollPosition.y > 0 {
let now = Date()
if now.timeIntervalSince(self.lastAddElementTime) >= self.addElementInterval {
self.lastAddElementTime = now
self.lastScrollPosition = messages.count
addElement()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
withAnimation {
proxy.scrollTo(self.lastScrollPosition, anchor: .top)
}
}
}
}
}
.onAppear {
proxy.scrollTo(1, anchor: .bottom) // show the most recent message at the bottom
}
}
}
.coordinateSpace(name: "scroll")
.navigationTitle("Scroll offset: (scrollPosition.y) (scrollPosition.x)")
.navigationBarTitleDisplayMode(.inline)
}
}
func addElement() { // add 10 messages in each update
for _ in 0..<10 {
self.messages.append(nextElement)
nextElement += 1
}
}
}
struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGPoint = .zero
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {
}
}
Of course this is not my actual code, but they have the same concept.
I’m wondering is there a way that I can render the view and scroll in the background, then show the rendered ui on the screen. I asked ai, but none of their solutions solve the problem. Thank you guys!
Vince Hsueh is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.