Reduce ForEach overhead with pre-computed data

- Remove unnecessary Array() conversions in messages ForEach
- Optimize URL ForEach to use indices instead of enumerated
- Simplify autocomplete ForEach by removing unneeded enumeration
- Pre-compute all peer display data outside ForEach for sidebar
- Create PeerDisplayData struct to bundle peer properties
- Move sorting and data preparation outside the render loop
This commit is contained in:
jack
2025-07-24 13:43:02 +02:00
parent 7120b11692
commit 2350c48e13
+52 -42
View File
@@ -183,7 +183,7 @@ struct ContentView: View {
// Implement windowing - show last 100 messages for performance // Implement windowing - show last 100 messages for performance
let windowedMessages = messages.suffix(100) let windowedMessages = messages.suffix(100)
ForEach(Array(windowedMessages), id: \.id) { message in ForEach(windowedMessages, id: \.id) { message in
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
// Check if current user is mentioned // Check if current user is mentioned
let _ = message.mentions?.contains(viewModel.nickname) ?? false let _ = message.mentions?.contains(viewModel.nickname) ?? false
@@ -215,7 +215,8 @@ struct ContentView: View {
// Check for plain URLs // Check for plain URLs
let urls = message.content.extractURLs() let urls = message.content.extractURLs()
if !urls.isEmpty { if !urls.isEmpty {
ForEach(Array(urls.prefix(3).enumerated()), id: \.offset) { index, urlInfo in ForEach(urls.prefix(3).indices, id: \.self) { index in
let urlInfo = urls[index]
LinkPreviewView(url: urlInfo.url, title: nil) LinkPreviewView(url: urlInfo.url, title: nil)
.padding(.top, 3) .padding(.top, 3)
.padding(.horizontal, 1) .padding(.horizontal, 1)
@@ -305,7 +306,7 @@ struct ContentView: View {
// @mentions autocomplete // @mentions autocomplete
if viewModel.showAutocomplete && !viewModel.autocompleteSuggestions.isEmpty { if viewModel.showAutocomplete && !viewModel.autocompleteSuggestions.isEmpty {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
ForEach(Array(viewModel.autocompleteSuggestions.enumerated()), id: \.element) { index, suggestion in ForEach(viewModel.autocompleteSuggestions, id: \.self) { suggestion in
Button(action: { Button(action: {
_ = viewModel.completeNickname(suggestion, in: &messageText) _ = viewModel.completeNickname(suggestion, in: &messageText)
}) { }) {
@@ -539,39 +540,49 @@ struct ContentView: View {
// Show all connected peers // Show all connected peers
let peersToShow: [String] = viewModel.connectedPeers let peersToShow: [String] = viewModel.connectedPeers
// Sort peers: favorites first, then alphabetically by nickname // Pre-compute peer data outside ForEach to reduce overhead
let sortedPeers = peersToShow.sorted { peer1, peer2 in struct PeerDisplayData: Identifiable {
let isFav1 = viewModel.isFavorite(peerID: peer1) let id: String
let isFav2 = viewModel.isFavorite(peerID: peer2) let displayName: String
let rssi: Int?
if isFav1 != isFav2 { let isFavorite: Bool
return isFav1 // Favorites come first let isMe: Bool
let hasUnreadMessages: Bool
let encryptionStatus: EncryptionStatus
} }
let name1 = peerNicknames[peer1] ?? "anon\(peer1.prefix(4))" let peerData = peersToShow.map { peerID in
let name2 = peerNicknames[peer2] ?? "anon\(peer2.prefix(4))" PeerDisplayData(
return name1 < name2 id: peerID,
} displayName: peerID == myPeerID ? viewModel.nickname : (peerNicknames[peerID] ?? "anon\(peerID.prefix(4))"),
rssi: peerRSSI[peerID]?.intValue,
isFavorite: viewModel.isFavorite(peerID: peerID),
isMe: peerID == myPeerID,
hasUnreadMessages: viewModel.unreadPrivateMessages.contains(peerID),
encryptionStatus: viewModel.getEncryptionStatus(for: peerID)
)
}.sorted { peer1, peer2 in
// Sort: favorites first, then alphabetically by nickname
if peer1.isFavorite != peer2.isFavorite {
return peer1.isFavorite
}
return peer1.displayName < peer2.displayName
}
ForEach(sortedPeers, id: \.self) { peerID in ForEach(peerData) { peer in
let displayName = peerID == myPeerID ? viewModel.nickname : (peerNicknames[peerID] ?? "anon\(peerID.prefix(4))")
let rssi = peerRSSI[peerID]?.intValue
let isFavorite = viewModel.isFavorite(peerID: peerID)
let isMe = peerID == myPeerID
HStack(spacing: 8) { HStack(spacing: 8) {
// Signal strength indicator or unread message icon // Signal strength indicator or unread message icon
if isMe { if peer.isMe {
Image(systemName: "person.fill") Image(systemName: "person.fill")
.font(.system(size: 10)) .font(.system(size: 10))
.foregroundColor(textColor) .foregroundColor(textColor)
.accessibilityLabel("You") .accessibilityLabel("You")
} else if viewModel.unreadPrivateMessages.contains(peerID) { } else if peer.hasUnreadMessages {
Image(systemName: "envelope.fill") Image(systemName: "envelope.fill")
.font(.system(size: 12)) .font(.system(size: 12))
.foregroundColor(Color.orange) .foregroundColor(Color.orange)
.accessibilityLabel("Unread message from \(displayName)") .accessibilityLabel("Unread message from \(peer.displayName)")
} else if let rssi = rssi { } else if let rssi = peer.rssi {
Image(systemName: "circle.fill") Image(systemName: "circle.fill")
.font(.system(size: 8)) .font(.system(size: 8))
.foregroundColor(viewModel.getRSSIColor(rssi: rssi, colorScheme: colorScheme)) .foregroundColor(viewModel.getRSSIColor(rssi: rssi, colorScheme: colorScheme))
@@ -585,49 +596,48 @@ struct ContentView: View {
} }
// Peer name // Peer name
if isMe { if peer.isMe {
HStack { HStack {
Text(displayName + " (you)") Text(peer.displayName + " (you)")
.font(.system(size: 14, design: .monospaced)) .font(.system(size: 14, design: .monospaced))
.foregroundColor(textColor) .foregroundColor(textColor)
Spacer() Spacer()
} }
} else { } else {
Text(displayName) Text(peer.displayName)
.font(.system(size: 14, design: .monospaced)) .font(.system(size: 14, design: .monospaced))
.foregroundColor(peerNicknames[peerID] != nil ? textColor : secondaryTextColor) .foregroundColor(peerNicknames[peer.id] != nil ? textColor : secondaryTextColor)
// Encryption status icon (after peer name) // Encryption status icon (after peer name)
let encryptionStatus = viewModel.getEncryptionStatus(for: peerID) Image(systemName: peer.encryptionStatus.icon)
Image(systemName: encryptionStatus.icon)
.font(.system(size: 10)) .font(.system(size: 10))
.foregroundColor(encryptionStatus == .noiseVerified ? Color.green : .foregroundColor(peer.encryptionStatus == .noiseVerified ? Color.green :
encryptionStatus == .noiseSecured ? textColor : peer.encryptionStatus == .noiseSecured ? textColor :
encryptionStatus == .noiseHandshaking ? Color.orange : peer.encryptionStatus == .noiseHandshaking ? Color.orange :
Color.red) Color.red)
.accessibilityLabel("Encryption: \(encryptionStatus == .noiseVerified ? "verified" : encryptionStatus == .noiseSecured ? "secured" : encryptionStatus == .noiseHandshaking ? "establishing" : "none")") .accessibilityLabel("Encryption: \(peer.encryptionStatus == .noiseVerified ? "verified" : peer.encryptionStatus == .noiseSecured ? "secured" : peer.encryptionStatus == .noiseHandshaking ? "establishing" : "none")")
Spacer() Spacer()
// Favorite star // Favorite star
Button(action: { Button(action: {
viewModel.toggleFavorite(peerID: peerID) viewModel.toggleFavorite(peerID: peer.id)
}) { }) {
Image(systemName: isFavorite ? "star.fill" : "star") Image(systemName: peer.isFavorite ? "star.fill" : "star")
.font(.system(size: 12)) .font(.system(size: 12))
.foregroundColor(isFavorite ? Color.yellow : secondaryTextColor) .foregroundColor(peer.isFavorite ? Color.yellow : secondaryTextColor)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityLabel(isFavorite ? "Remove \(displayName) from favorites" : "Add \(displayName) to favorites") .accessibilityLabel(peer.isFavorite ? "Remove \(peer.displayName) from favorites" : "Add \(peer.displayName) to favorites")
} }
} }
.padding(.horizontal) .padding(.horizontal)
.padding(.vertical, 8) .padding(.vertical, 8)
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { .onTapGesture {
if !isMe && peerNicknames[peerID] != nil { if !peer.isMe && peerNicknames[peer.id] != nil {
viewModel.startPrivateChat(with: peerID) viewModel.startPrivateChat(with: peer.id)
withAnimation(.easeInOut(duration: 0.2)) { withAnimation(.easeInOut(duration: 0.2)) {
showSidebar = false showSidebar = false
sidebarDragOffset = 0 sidebarDragOffset = 0
@@ -635,9 +645,9 @@ struct ContentView: View {
} }
} }
.onTapGesture(count: 2) { .onTapGesture(count: 2) {
if !isMe { if !peer.isMe {
// Show fingerprint on double tap // Show fingerprint on double tap
viewModel.showFingerprint(for: peerID) viewModel.showFingerprint(for: peer.id)
} }
} }
} }