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 selectedMessageSender: String?
@State private var selectedMessageSenderID: String? @State private var selectedMessageSenderID: String?
@FocusState private var isNicknameFieldFocused: Bool @FocusState private var isNicknameFieldFocused: Bool
@State private var lastScrollTime: Date = .distantPast
@State private var scrollThrottleTimer: Timer?
private var backgroundColor: Color { private var backgroundColor: Color {
colorScheme == .dark ? Color.black : Color.white colorScheme == .dark ? Color.black : Color.white
@@ -65,19 +67,18 @@ struct ContentView: View {
} }
.onEnded { value in .onEnded { value in
if value.translation.width > 50 || (value.translation.width > 30 && value.velocity.width > 300) { 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 showPrivateChat = false
backSwipeOffset = 0 backSwipeOffset = 0
viewModel.endPrivateChat() viewModel.endPrivateChat()
} }
} else { } else {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { withAnimation(.easeOut(duration: 0.15)) {
backSwipeOffset = 0 backSwipeOffset = 0
} }
} }
} }
) )
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: showPrivateChat)
} }
// Sidebar overlay // Sidebar overlay
@@ -86,7 +87,7 @@ struct ContentView: View {
Color.clear Color.clear
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { .onTapGesture {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { withAnimation(.easeInOut(duration: 0.2)) {
showSidebar = false showSidebar = false
sidebarDragOffset = 0 sidebarDragOffset = 0
} }
@@ -101,15 +102,14 @@ struct ContentView: View {
.transition(.move(edge: .trailing)) .transition(.move(edge: .trailing))
} }
.offset(x: showSidebar ? -sidebarDragOffset : geometry.size.width - sidebarDragOffset) .offset(x: showSidebar ? -sidebarDragOffset : geometry.size.width - sidebarDragOffset)
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: showSidebar) .animation(.easeInOut(duration: 0.25), value: showSidebar)
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: sidebarDragOffset)
} }
} }
#if os(macOS) #if os(macOS)
.frame(minWidth: 600, minHeight: 400) .frame(minWidth: 600, minHeight: 400)
#endif #endif
.onChange(of: viewModel.selectedPrivateChatPeer) { newValue in .onChange(of: viewModel.selectedPrivateChatPeer) { newValue in
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { withAnimation(.easeInOut(duration: 0.2)) {
showPrivateChat = newValue != nil showPrivateChat = newValue != nil
} }
} }
@@ -132,7 +132,7 @@ struct ContentView: View {
Button("private message") { Button("private message") {
if let peerID = selectedMessageSenderID { if let peerID = selectedMessageSenderID {
viewModel.startPrivateChat(with: peerID) viewModel.startPrivateChat(with: peerID)
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { withAnimation(.easeInOut(duration: 0.2)) {
showSidebar = false showSidebar = false
sidebarDragOffset = 0 sidebarDragOffset = 0
} }
@@ -242,10 +242,19 @@ struct ContentView: View {
} }
.onChange(of: viewModel.messages.count) { _ in .onChange(of: viewModel.messages.count) { _ in
if privatePeer == nil && !viewModel.messages.isEmpty { if privatePeer == nil && !viewModel.messages.isEmpty {
withAnimation { // Throttle scroll animations to prevent excessive UI updates
// Scroll to last message in windowed view let now = Date()
let lastId = viewModel.messages.suffix(100).last?.id if now.timeIntervalSince(lastScrollTime) > 0.5 {
proxy.scrollTo(lastId, anchor: .bottom) // 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, if let peerID = privatePeer,
let messages = viewModel.privateChats[peerID], let messages = viewModel.privateChats[peerID],
!messages.isEmpty { !messages.isEmpty {
withAnimation { // Same throttling for private chats
// Scroll to last message in windowed view let now = Date()
let lastId = messages.suffix(100).last?.id if now.timeIntervalSince(lastScrollTime) > 0.5 {
proxy.scrollTo(lastId, anchor: .bottom) 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 { .onTapGesture {
if !isMe && peerNicknames[peerID] != nil { if !isMe && peerNicknames[peerID] != nil {
viewModel.startPrivateChat(with: peerID) viewModel.startPrivateChat(with: peerID)
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { withAnimation(.easeInOut(duration: 0.2)) {
showSidebar = false showSidebar = false
sidebarDragOffset = 0 sidebarDragOffset = 0
} }
@@ -647,7 +663,7 @@ struct ContentView: View {
} }
} }
.onEnded { value in .onEnded { value in
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { withAnimation(.easeOut(duration: 0.2)) {
if !showSidebar { if !showSidebar {
if value.translation.width < -100 || (value.translation.width < -50 && value.velocity.width < -500) { if value.translation.width < -100 || (value.translation.width < -50 && value.velocity.width < -500) {
showSidebar = true showSidebar = true
@@ -749,7 +765,7 @@ struct ContentView: View {
.foregroundColor(viewModel.isConnected ? textColor : Color.red) .foregroundColor(viewModel.isConnected ? textColor : Color.red)
} }
.onTapGesture { .onTapGesture {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { withAnimation(.easeInOut(duration: 0.2)) {
showSidebar.toggle() showSidebar.toggle()
sidebarDragOffset = 0 sidebarDragOffset = 0
} }
@@ -766,7 +782,7 @@ struct ContentView: View {
let privatePeerNick = viewModel.meshService.getPeerNicknames()[privatePeerID] { let privatePeerNick = viewModel.meshService.getPeerNicknames()[privatePeerID] {
HStack { HStack {
Button(action: { Button(action: {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { withAnimation(.easeInOut(duration: 0.2)) {
showPrivateChat = false showPrivateChat = false
viewModel.endPrivateChat() viewModel.endPrivateChat()
} }