Optimization 5: Fix animation overhead

- Removed redundant animation modifiers (multiple animations on same view)
- Replaced spring animations with simpler easeInOut/easeOut curves
- Spring animations are computationally expensive, now using 0.2s easeInOut
- Added scroll throttling to prevent excessive animations on message updates
- Scroll animations limited to once per 0.5 seconds
- Removed withAnimation wrapper from scrollTo calls
- Simplified drag gesture animations from spring to easeOut

This reduces CPU usage during UI transitions and message updates
This commit is contained in:
jack
2025-07-24 13:30:56 +02:00
parent 8d64920f8e
commit 354d3ff6ce
+36 -20
View File
@@ -26,6 +26,8 @@ struct ContentView: View {
@State private var selectedMessageSender: String?
@State private var selectedMessageSenderID: String?
@FocusState private var isNicknameFieldFocused: Bool
@State private var lastScrollTime: Date = .distantPast
@State private var scrollThrottleTimer: Timer?
private var backgroundColor: Color {
colorScheme == .dark ? Color.black : Color.white
@@ -65,19 +67,18 @@ struct ContentView: View {
}
.onEnded { value in
if value.translation.width > 50 || (value.translation.width > 30 && value.velocity.width > 300) {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
withAnimation(.easeOut(duration: 0.2)) {
showPrivateChat = false
backSwipeOffset = 0
viewModel.endPrivateChat()
}
} else {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
withAnimation(.easeOut(duration: 0.15)) {
backSwipeOffset = 0
}
}
}
)
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: showPrivateChat)
}
// Sidebar overlay
@@ -86,7 +87,7 @@ struct ContentView: View {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
withAnimation(.easeInOut(duration: 0.2)) {
showSidebar = false
sidebarDragOffset = 0
}
@@ -101,15 +102,14 @@ struct ContentView: View {
.transition(.move(edge: .trailing))
}
.offset(x: showSidebar ? -sidebarDragOffset : geometry.size.width - sidebarDragOffset)
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: showSidebar)
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: sidebarDragOffset)
.animation(.easeInOut(duration: 0.25), value: showSidebar)
}
}
#if os(macOS)
.frame(minWidth: 600, minHeight: 400)
#endif
.onChange(of: viewModel.selectedPrivateChatPeer) { newValue in
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
withAnimation(.easeInOut(duration: 0.2)) {
showPrivateChat = newValue != nil
}
}
@@ -132,7 +132,7 @@ struct ContentView: View {
Button("private message") {
if let peerID = selectedMessageSenderID {
viewModel.startPrivateChat(with: peerID)
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
withAnimation(.easeInOut(duration: 0.2)) {
showSidebar = false
sidebarDragOffset = 0
}
@@ -242,10 +242,19 @@ struct ContentView: View {
}
.onChange(of: viewModel.messages.count) { _ in
if privatePeer == nil && !viewModel.messages.isEmpty {
withAnimation {
// Scroll to last message in windowed view
let lastId = viewModel.messages.suffix(100).last?.id
proxy.scrollTo(lastId, anchor: .bottom)
// Throttle scroll animations to prevent excessive UI updates
let now = Date()
if now.timeIntervalSince(lastScrollTime) > 0.5 {
// Immediate scroll if enough time has passed
lastScrollTime = now
proxy.scrollTo(viewModel.messages.suffix(100).last?.id, anchor: .bottom)
} else {
// Schedule a delayed scroll
scrollThrottleTimer?.invalidate()
scrollThrottleTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
lastScrollTime = Date()
proxy.scrollTo(viewModel.messages.suffix(100).last?.id, anchor: .bottom)
}
}
}
}
@@ -253,10 +262,17 @@ struct ContentView: View {
if let peerID = privatePeer,
let messages = viewModel.privateChats[peerID],
!messages.isEmpty {
withAnimation {
// Scroll to last message in windowed view
let lastId = messages.suffix(100).last?.id
proxy.scrollTo(lastId, anchor: .bottom)
// Same throttling for private chats
let now = Date()
if now.timeIntervalSince(lastScrollTime) > 0.5 {
lastScrollTime = now
proxy.scrollTo(messages.suffix(100).last?.id, anchor: .bottom)
} else {
scrollThrottleTimer?.invalidate()
scrollThrottleTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
lastScrollTime = Date()
proxy.scrollTo(messages.suffix(100).last?.id, anchor: .bottom)
}
}
}
}
@@ -600,7 +616,7 @@ struct ContentView: View {
.onTapGesture {
if !isMe && peerNicknames[peerID] != nil {
viewModel.startPrivateChat(with: peerID)
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
withAnimation(.easeInOut(duration: 0.2)) {
showSidebar = false
sidebarDragOffset = 0
}
@@ -647,7 +663,7 @@ struct ContentView: View {
}
}
.onEnded { value in
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
withAnimation(.easeOut(duration: 0.2)) {
if !showSidebar {
if value.translation.width < -100 || (value.translation.width < -50 && value.velocity.width < -500) {
showSidebar = true
@@ -749,7 +765,7 @@ struct ContentView: View {
.foregroundColor(viewModel.isConnected ? textColor : Color.red)
}
.onTapGesture {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
withAnimation(.easeInOut(duration: 0.2)) {
showSidebar.toggle()
sidebarDragOffset = 0
}
@@ -766,7 +782,7 @@ struct ContentView: View {
let privatePeerNick = viewModel.meshService.getPeerNicknames()[privatePeerID] {
HStack {
Button(action: {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
withAnimation(.easeInOut(duration: 0.2)) {
showPrivateChat = false
viewModel.endPrivateChat()
}