summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-18 15:55:25 -0800
committerFuwn <[email protected]>2026-02-18 16:15:30 -0800
commit2a5e530af2d8e1ceb64ac384bcef0ba5ca139930 (patch)
tree7270312261633096e9a9ab0a36c2041e0f7ec1a3
parentchore: finalise performance optimisation batch and metrics (diff)
downloadsora-testing-2a5e530af2d8e1ceb64ac384bcef0ba5ca139930.tar.xz
sora-testing-2a5e530af2d8e1ceb64ac384bcef0ba5ca139930.zip
fix: prevent thumbnail grid launch crash from column and data drift
-rw-r--r--Sora/Views/Shared/ThumbnailGridView.swift21
-rw-r--r--SoraTests/ViewDerivedDataTests.swift34
2 files changed, 37 insertions, 18 deletions
diff --git a/Sora/Views/Shared/ThumbnailGridView.swift b/Sora/Views/Shared/ThumbnailGridView.swift
index a7130c2..ec8cd10 100644
--- a/Sora/Views/Shared/ThumbnailGridView.swift
+++ b/Sora/Views/Shared/ThumbnailGridView.swift
@@ -8,12 +8,27 @@ struct ThumbnailGridView<Item: Hashable & Identifiable, Content: View>: View {
let columnsData: [[Item]]
let content: (Item) -> Content
+ private var resolvedColumnCount: Int {
+ max(columnCount, 1)
+ }
+
+ private var resolvedColumnsData: [[Item]] {
+ let clippedColumnsData = Array(columnsData.prefix(resolvedColumnCount))
+ let missingColumnCount = resolvedColumnCount - clippedColumnsData.count
+
+ if missingColumnCount <= 0 {
+ return clippedColumnsData
+ }
+
+ return clippedColumnsData + Array(repeating: [], count: missingColumnCount)
+ }
+
var body: some View {
if useAlternativeGrid {
HStack(alignment: .top) {
- ForEach(0..<columnCount, id: \.self) { columnIndex in
+ ForEach(Array(resolvedColumnsData.enumerated()), id: \.offset) { _, columnItems in
LazyVStack {
- ForEach(columnsData[columnIndex], id: \.id) { item in
+ ForEach(columnItems, id: \.id) { item in
content(item)
.id(item.id)
}
@@ -32,7 +47,7 @@ struct ThumbnailGridView<Item: Hashable & Identifiable, Content: View>: View {
content(item)
.id(item.id)
}
- .gridStyle(columns: columnCount)
+ .gridStyle(columns: resolvedColumnCount)
.transaction { $0.animation = nil }
#if os(macOS)
.padding(8)
diff --git a/SoraTests/ViewDerivedDataTests.swift b/SoraTests/ViewDerivedDataTests.swift
index d8760e7..bc3a998 100644
--- a/SoraTests/ViewDerivedDataTests.swift
+++ b/SoraTests/ViewDerivedDataTests.swift
@@ -267,28 +267,32 @@ final class ViewDerivedDataTests: XCTestCase {
)
}
- private func referenceCount(for symbol: String, in source: String) -> Int {
- let totalMatches = tokenCount(
- matching: #"\b\#(symbol)\b"#,
- in: source
+ func testThumbnailGridViewAvoidsDirectColumnArrayIndexing() throws {
+ let source = try loadSource(at: "Sora/Views/Shared/ThumbnailGridView.swift")
+ let normalizedSource = strippingCommentsAndStrings(from: source)
+ let directColumnIndexingCount = tokenCount(
+ matching: #"\bcolumnsData\s*\[\s*columnIndex\s*\]"#,
+ in: normalizedSource
)
- let declarationMatches = tokenCount(
- matching: #"\b(?:var|let)\s+\#(symbol)\b"#,
- in: source
+
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertEqual(
+ directColumnIndexingCount,
+ 0,
+ "Thumbnail grid should avoid direct column indexing that can crash when column and data counts drift."
)
+ }
+
+ private func referenceCount(for symbol: String, in source: String) -> Int {
+ let totalMatches = tokenCount(matching: #"\b\#(symbol)\b"#, in: source)
+ let declarationMatches = tokenCount(matching: #"\b(?:var|let)\s+\#(symbol)\b"#, in: source)
return max(0, totalMatches - declarationMatches)
}
private func invocationCount(forFunction name: String, in source: String) -> Int {
- let totalMatches = tokenCount(
- matching: #"\b\#(name)\s*\("#,
- in: source
- )
- let declarationMatches = tokenCount(
- matching: #"\bfunc\s+\#(name)\s*\("#,
- in: source
- )
+ let totalMatches = tokenCount(matching: #"\b\#(name)\s*\("#, in: source)
+ let declarationMatches = tokenCount(matching: #"\bfunc\s+\#(name)\s*\("#, in: source)
return max(0, totalMatches - declarationMatches)
}