Skip to content

Instantly share code, notes, and snippets.

@Codelaby
Last active April 2, 2025 21:21
Show Gist options
  • Save Codelaby/a5bd569ef1012c4ed15fd5835a3a2096 to your computer and use it in GitHub Desktop.
Save Codelaby/a5bd569ef1012c4ed15fd5835a3a2096 to your computer and use it in GitHub Desktop.
import SwiftUI
struct ChatMessageModel: Identifiable, Hashable, Sendable {
var id: UUID = .init()
let content: String
let timestamp: Date
let sender: String
}
@MainActor
class ChatMessageObservable: ObservableObject {
@Published var chatMessages: [ChatMessageModel] = []
var cachedMessages: [ChatMessageModel] = []
@Published var isLoading = false
init() {
Task {
await loadDummyData()
}
}
private func loadDummyData() async {
Task { @MainActor in
cachedMessages = generateDummyData()
loadMoreMessages(before:Date.now)
}
}
func fetchChatMessages(before date: Date, limit: Int = 10) async -> [ChatMessageModel] {
// Filter messages older than the specified date
let filtered = cachedMessages
.filter { $0.timestamp < date }
.sorted { $0.timestamp > $1.timestamp } // Newest first
// Return the requested number of messages
return Array(filtered.prefix(limit)).reversed()
}
func loadMoreMessages(before date: Date = Date.now) {
isLoading = true
Task {
let olderMessages = await fetchChatMessages(before: date)
Task { @MainActor in
chatMessages.insert(contentsOf: olderMessages, at: 0)
self.isLoading = false
}
}
}
func addNewMessage(content: String, sender: String) {
let newMessage = ChatMessageModel(
content: content,
timestamp: Date(),
sender: sender
)
cachedMessages.append(newMessage)
}
private func generateDummyData(count: Int = 300) -> [ChatMessageModel] {
let senders = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank", "Grace", "Henry"]
var messages: [ChatMessageModel] = []
// Current date for the most recent message
let now = Date()
// Calendar for date calculations
let calendar = Calendar.current
for i in 0..<count {
// Calculate date by subtracting 30 minutes for each message
let date = calendar.date(byAdding: .minute, value: -30 * i, to: now)!
// Create message
let message = ChatMessageModel(
id: UUID(),
content: "Chat message number \(count - i)",
timestamp: date,
sender: senders.randomElement()!
)
messages.append(message)
}
// Sort by date (oldest first)
return messages.sorted { $0.timestamp < $1.timestamp }
}
}
struct ChatView: View {
@StateObject var viewModel = ChatMessageObservable()
@State private var scrollProxy: ScrollViewProxy?
@State private var scrollPosition: ScrollPosition = .init(idType: UUID.self)
var body: some View {
VStack {
ScrollView(.vertical) {
LazyVStack {
ForEach(viewModel.chatMessages, id: \.self) { message in
ChatMessageView(message: message)
.id(message.id)
}
}
.scrollTargetLayout()
}
.scrollPosition($scrollPosition)
.overlay(
Group {
if viewModel.isLoading {
ProgressView()
.scaleEffect(1.5)
}
}
)
// .onChange(of: scrollPosition) { oldValue, newValue in
// print(newValue)
// }
}
Button("load chat") {
if let swapUID = scrollPosition.viewID(type: UUID.self) {
viewModel.loadMoreMessages(before: viewModel.chatMessages.first?.timestamp ?? Date.now)
scrollPosition.scrollTo(id: swapUID)
print("📌 the scroll position is maintained")
} else {
print("⬇️ load messages and goto bottom")
viewModel.loadMoreMessages(before: viewModel.chatMessages.first?.timestamp ?? Date.now)
scrollPosition.scrollTo(edge: .bottom)
}
}
}
}
struct ChatMessageView: View {
let message: ChatMessageModel
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(message.sender)
.fontWeight(.bold)
Spacer()
Text(message.timestamp, style: .time)
.font(.caption)
}
Text(message.content)
}
.padding(.horizontal)
}
}
#Preview {
ChatView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment