Skip to content

Commit 3fca027

Browse files
author
Iakov Senatov
committed
Optimize large directory rendering
- Introduce EquatableRow wrapper to prevent unnecessary SwiftUI row re-renders - Use LazyVStack with stable identity for large file lists - Disable animations for large tables - Add drawingGroup configuration to stabilize rendering pipeline - Improve selection update performance (capture selectedID once) Improves UI responsiveness for directories with 20k+ files.
1 parent ea9768a commit 3fca027

File tree

8 files changed

+1895
-1821
lines changed

8 files changed

+1895
-1821
lines changed
Lines changed: 116 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,124 @@
1-
// FileInfoRow.swift
2-
// MiMiNavigator
3-
//
4-
// Created by Iakov Senatov on 23.01.2026.
5-
// Copyright © 2026 Senatov. All rights reserved.
6-
// Description: Row component displaying file metadata with icon
1+
// FileInfoRow.swift
2+
// MiMiNavigator
3+
//
4+
// Created by Iakov Senatov on 23.01.2026.
5+
// Copyright © 2026 Senatov. All rights reserved.
6+
// Description: Row component displaying file metadata with icon
77

8-
import SwiftUI
9-
import AppKit
8+
import SwiftUI
9+
import AppKit
1010

11-
// MARK: - File Info Row
12-
/// Displays file information including icon, path, name, date and size
13-
struct FileInfoRow: View {
14-
let title: String
15-
let url: URL
16-
let name: String
17-
let size: Int64
18-
let date: Date?
19-
20-
// MARK: - Body
21-
var body: some View {
22-
HStack(alignment: .top, spacing: 12) {
23-
fileIcon
24-
fileDetails
25-
Spacer()
11+
// MARK: - File Info Row
12+
/// Displays file information including icon, path, name, date and size
13+
struct FileInfoRow: View {
14+
let title: String
15+
let url: URL
16+
let name: String
17+
let size: Int64
18+
let date: Date?
19+
20+
// MARK: - Body
21+
var body: some View {
22+
HStack(alignment: .top, spacing: 12) {
23+
fileIcon
24+
fileDetails
25+
Spacer()
26+
}
2627
}
27-
}
28-
29-
// MARK: - Private Views
30-
private var fileIcon: some View {
31-
Image(nsImage: icon)
32-
.resizable()
33-
.frame(width: 48, height: 48)
34-
.cornerRadius(4)
35-
}
36-
37-
private var fileDetails: some View {
38-
VStack(alignment: .leading, spacing: 3) {
39-
titleText
40-
PathWithHighlight(path: parentPath)
41-
nameText
42-
dateText
43-
sizeText
28+
29+
// MARK: - Private Views
30+
private var fileIcon: some View {
31+
Image(nsImage: icon)
32+
.resizable()
33+
.frame(width: 48, height: 48)
34+
.cornerRadius(4)
35+
}
36+
37+
private var fileDetails: some View {
38+
VStack(alignment: .leading, spacing: 3) {
39+
titleText
40+
PathWithHighlight(path: parentPath)
41+
nameText
42+
dateText
43+
sizeText
44+
}
45+
}
46+
47+
private var titleText: some View {
48+
Text(title)
49+
.font(.system(size: 12, weight: .semibold))
50+
}
51+
52+
private var nameText: some View {
53+
Text(name)
54+
.font(.system(size: 11))
55+
.foregroundStyle(.primary)
56+
.lineLimit(2)
57+
}
58+
59+
private var dateText: some View {
60+
Text(formattedDate)
61+
.font(.system(size: 10))
62+
.foregroundStyle(.secondary)
63+
}
64+
65+
private var sizeText: some View {
66+
Text(formattedSize)
67+
.font(.system(size: 10))
68+
.foregroundStyle(.secondary)
4469
}
4570
}
46-
47-
private var titleText: some View {
48-
Text(title)
49-
.font(.system(size: 12, weight: .semibold))
50-
}
51-
52-
private var nameText: some View {
53-
Text(name)
54-
.font(.system(size: 11))
55-
.foregroundStyle(.primary)
56-
.lineLimit(2)
57-
}
58-
59-
private var dateText: some View {
60-
Text(formattedDate)
61-
.font(.system(size: 10))
62-
.foregroundStyle(.secondary)
63-
}
64-
65-
private var sizeText: some View {
66-
Text(formattedSize)
67-
.font(.system(size: 10))
68-
.foregroundStyle(.secondary)
69-
}
70-
}
7171

72-
// MARK: - Computed Properties
73-
private extension FileInfoRow {
74-
var icon: NSImage {
75-
NSWorkspace.shared.icon(forFile: url.path)
76-
}
77-
78-
var parentPath: String {
79-
url.deletingLastPathComponent().path
80-
}
81-
82-
var formattedSize: String {
83-
ByteCountFormatter.string(fromByteCount: size, countStyle: .file)
84-
}
85-
86-
var formattedDate: String {
87-
guard let date = date else { return "" }
88-
return Self.dateFormatter.string(from: date)
72+
// MARK: - Computed Properties
73+
private extension FileInfoRow {
74+
var icon: NSImage {
75+
// Cache icons by file extension to avoid thousands of NSWorkspace icon lookups
76+
let ext = url.pathExtension.lowercased() as NSString
77+
if let cached = Self.iconCache.object(forKey: ext) {
78+
return cached
79+
}
80+
81+
let icon = NSWorkspace.shared.icon(forFile: url.path)
82+
Self.iconCache.setObject(icon, forKey: ext)
83+
return icon
84+
}
85+
86+
var parentPath: String {
87+
url.deletingLastPathComponent().path
88+
}
89+
90+
var formattedSize: String {
91+
ByteCountFormatter.string(fromByteCount: size, countStyle: .file)
92+
}
93+
94+
var formattedDate: String {
95+
guard let date = date else { return "" }
96+
return Self.dateFormatter.string(from: date)
97+
}
98+
99+
/// Cache icons by file extension (massively reduces NSWorkspace calls in large directories)
100+
static let iconCache: NSCache<NSString, NSImage> = {
101+
let cache = NSCache<NSString, NSImage>()
102+
cache.countLimit = 128
103+
return cache
104+
}()
105+
106+
static let dateFormatter: DateFormatter = {
107+
let formatter = DateFormatter()
108+
formatter.dateFormat = "d. MMMM yyyy 'at' HH:mm:ss"
109+
return formatter
110+
}()
89111
}
90-
91-
static let dateFormatter: DateFormatter = {
92-
let formatter = DateFormatter()
93-
formatter.dateFormat = "d. MMMM yyyy 'at' HH:mm:ss"
94-
return formatter
95-
}()
96-
}
97112

98-
// MARK: - Preview
99-
#Preview {
100-
FileInfoRow(
101-
title: "Copying",
102-
url: URL(fileURLWithPath: "/Users/senat/Downloads/test.mp4"),
103-
name: "test.mp4",
104-
size: 1_234_567,
105-
date: Date()
106-
)
107-
.padding()
108-
.frame(width: 400)
109-
}
113+
// MARK: - Preview
114+
#Preview {
115+
FileInfoRow(
116+
title: "Copying",
117+
url: URL(fileURLWithPath: "/Users/senat/Downloads/test.mp4"),
118+
name: "test.mp4",
119+
size: 1_234_567,
120+
date: Date()
121+
)
122+
.padding()
123+
.frame(width: 400)
124+
}

0 commit comments

Comments
 (0)