From a9dd9dd98a74138b467427c01ce65a00b18232a6 Mon Sep 17 00:00:00 2001 From: vietlubu Date: Mon, 17 Aug 2026 20:15:50 +0700 Subject: [PATCH 1/2] fix: support Swift 6.3 toolchains --- Sources/PokeTokenBar/PokeTokenBarApp.swift | 1 + Sources/PokeTokenBar/UI/BagView.swift | 2 + Sources/PokeTokenBar/UI/CompanionView.swift | 10 +++++ .../PokeTokenBar/UI/FloatingPetPanel.swift | 2 + Sources/PokeTokenBar/UI/PopoverView.swift | 2 + Sources/PokeTokenBar/UI/SettingsView.swift | 1 + Sources/PokeTokenBar/UI/ShopView.swift | 3 ++ .../SwiftUIIsolationTests.swift | 40 +++++++++++++++++++ docs/reference/defect-log.md | 11 +++++ scripts/test-gate.sh | 11 ++++- 10 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 Tests/PokeTokenBarTests/SwiftUIIsolationTests.swift diff --git a/Sources/PokeTokenBar/PokeTokenBarApp.swift b/Sources/PokeTokenBar/PokeTokenBarApp.swift index da00d6e76..be0ddb364 100644 --- a/Sources/PokeTokenBar/PokeTokenBarApp.swift +++ b/Sources/PokeTokenBar/PokeTokenBarApp.swift @@ -3,6 +3,7 @@ import QuartzCore import SwiftUI @main +@MainActor struct PokeTokenBarApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate diff --git a/Sources/PokeTokenBar/UI/BagView.swift b/Sources/PokeTokenBar/UI/BagView.swift index f7eaa0895..6582a1730 100644 --- a/Sources/PokeTokenBar/UI/BagView.swift +++ b/Sources/PokeTokenBar/UI/BagView.swift @@ -1,6 +1,7 @@ import SwiftUI /// 가방(인벤토리) — 소유 아이템 카드 + 사용. 빈 상태는 움직이는 잠만보(컬렉션의 피카츄 패턴). +@MainActor struct BagView: View { let store: CompanionStore let nav: PopoverNavigation @@ -36,6 +37,7 @@ struct BagView: View { /// 아이템 1장 — 아이콘·이름·개수·설명 + 인라인 확인 사용. /// 확인은 인라인(버튼 morph) — .sheet/.alert 금지: transient 팝오버가 닫힐 때 고아 시트가 /// 이후 클릭을 먹통내는 기존 결함(PopoverView 주석) 회피. +@MainActor private struct ItemCard: View { let store: CompanionStore let nav: PopoverNavigation diff --git a/Sources/PokeTokenBar/UI/CompanionView.swift b/Sources/PokeTokenBar/UI/CompanionView.swift index 0357ca98a..7afc88f20 100644 --- a/Sources/PokeTokenBar/UI/CompanionView.swift +++ b/Sources/PokeTokenBar/UI/CompanionView.swift @@ -14,6 +14,7 @@ func rarityColor(_ r: Rarity?) -> Color { let rarityDisplayOrder: [Rarity] = [.legendary, .rare, .uncommon, .common] /// 아이템 아이콘 — 실제 스프라이트(런타임 로드+캐시) 우선, 로딩 전/미제공/실패 시 이모지 폴백. +@MainActor struct ItemIconView: View { let kind: ItemKind var size: CGFloat = 30 @@ -81,6 +82,7 @@ struct SpriteSubject: Equatable { /// 스프라이트 1개(런타임 로드 + 캐시). 없으면 알 글리프. bob 으로 가벼운 상하 움직임. /// animated=true 면 Gen-V GIF 프레임을 순환(미지원/오프라인이면 정적+bob 으로 폴백). +@MainActor struct SpriteView: View { let speciesID: Int? var size: CGFloat = 84 @@ -235,6 +237,7 @@ struct SpriteView: View { /// 폭 제한 없는 HStack 은 팝오버 콘텐츠 폭(332pt)을 넘고, **넘친 자식이 부모 VStack 폭을 부풀려 /// 팝오버 전체가 좌우로 잘린다**(진화줄뿐 아니라 탭바·합계까지). `maxWidth` 를 주면 그 폭 안에서 /// 가로 스크롤한다 — 썸네일 크기는 유지하고, 가장자리 페이드 + 셰브론으로 스크롤 가능함을 알린다. +@MainActor struct EvoLineView: View { let nodes: [EvoLineItem] let mysteryLabel: String @@ -435,6 +438,7 @@ struct EvoLineView: View { } /// 팝오버 상단 — 현재 포켓몬 + 진화 진행 + 부화/진화 연출. +@MainActor struct CompanionHeader: View { let store: CompanionStore // 연출 상태 — 부화/진화 순간 흰 플래시 + 스프링 스케일(본가 진화 신 오마주) @@ -653,6 +657,7 @@ struct CompanionHeader: View { /// (solid 채움 대신 링+체크 — green/orange 위 흰 텍스트 대비 문제 회피 + 라이트/다크 양쪽 가독. /// 텍스트는 .primary 라 모드 자동 적응, 색 정체성은 점·링·체크로 유지 → 엔트리 배지와 안 어긋남.) /// 0이면 흐리게(필터 불가). +@MainActor struct RarityTally: View { let label: String let count: Int @@ -680,6 +685,7 @@ struct RarityTally: View { /// 포획 로그 요약 헤더 — 총 개체 수 + 희귀도별 개체 수 캡슐. /// 개수 단위가 개체(store.dexCount)라 종 단위인 도감 헤더와 공유하지 않는다. +@MainActor struct DexSummaryHeader: View { let store: CompanionStore let selected: Rarity? // nil = 필터 없음(전체) @@ -715,6 +721,7 @@ struct DexSummaryHeader: View { /// - **로그**: 개체 1마리 = 1행. 같은 라인이 여러 행으로 나오는 게 정상 — 성격·획득 시각처럼 /// 개체에 딸린 정보는 여기에만 있다. /// 상위 탭(PopoverTab)은 그대로 4개 — 세그먼트 폭(332/2)이 넉넉해 탭바를 늘릴 필요가 없다. +@MainActor struct CollectionView: View { let store: CompanionStore let navigation: PopoverNavigation @@ -825,6 +832,7 @@ struct RepresentativeFooterButton: View { /// 우회(고정 높이 + maxHeight)가 아니라 회피로 피한다. 페이지 크기가 고정이라 모든 칸이 항상 /// 렌더되므로 지연 격자(LazyVGrid)도 필요 없다 — 평범한 VStack/HStack 으로 동기 렌더한다. /// 미보유 종은 아예 그리지 않는다(물음표·실루엣 칸 없음). +@MainActor private struct DexGridView: View { let store: CompanionStore @State private var selectedRarity: Rarity? @@ -956,6 +964,7 @@ private struct DexGridView: View { /// 도감 한 칸 — 도감 번호 + 스프라이트 + 종 이름. 종 정보만 담는다(성격·획득 횟수는 로그의 몫). /// 정적 스프라이트만 쓴다(animated 생략) — 한 페이지 24칸을 GIF 로 동시 재생하면 CPU 가 안 된다. +@MainActor private struct DexSpeciesCell: View { let store: CompanionStore let species: CompanionStore.DexSpecies @@ -1075,6 +1084,7 @@ private struct DexSpeciesCell: View { /// 포획 로그 한 항목 — 희귀도·성격 헤더 + 진화 체인 스프라이트(각 밑에 종 이름) + 잡은 시각. /// 체인 각 종의 이름은 저장분이 있으면 body 에서 즉시(플래시 없음), 없으면(구버전) .task 로 조회 후 백필. +@MainActor private struct DexEntryRow: View { let store: CompanionStore let entry: DexEntry diff --git a/Sources/PokeTokenBar/UI/FloatingPetPanel.swift b/Sources/PokeTokenBar/UI/FloatingPetPanel.swift index 050219c8b..0d450743a 100644 --- a/Sources/PokeTokenBar/UI/FloatingPetPanel.swift +++ b/Sources/PokeTokenBar/UI/FloatingPetPanel.swift @@ -410,6 +410,7 @@ final class PetHostingView: NSHostingView { @objc func handleHide(_ sender: Any?) { onHide?() } } +@MainActor struct FloatingPetView: View { static let frameFloor: TimeInterval = 0.4 var animated: Bool = true @@ -448,6 +449,7 @@ struct FloatingPetView: View { } /// Transient limit-alert bubble. Width is capped so copy wraps instead of clipping the panel. +@MainActor private struct SpeechBubbleView: View { let alert: UsageStore.LimitAlert let l: L diff --git a/Sources/PokeTokenBar/UI/PopoverView.swift b/Sources/PokeTokenBar/UI/PopoverView.swift index 2682c1dc6..5e4c1307a 100644 --- a/Sources/PokeTokenBar/UI/PopoverView.swift +++ b/Sources/PokeTokenBar/UI/PopoverView.swift @@ -39,6 +39,7 @@ final class PopoverNavigation { } } +@MainActor struct PopoverView: View { @Environment(UsageStore.self) private var store @Environment(CompanionStore.self) private var companion @@ -623,6 +624,7 @@ struct PopoverView: View { /// "Curso/r"·"Code/x" 처럼 **단어 중간에서** 접고 탭 바가 2~3줄이 된다. /// 가로 스크롤 + `lineLimit(1)`/`fixedSize` 로 각 탭이 항상 자연 폭 한 줄을 유지한다. /// (`Spacer()` 는 가로 ScrollView 안에서 무한 확장하므로 쓰지 않는다.) +@MainActor struct ProviderTabBar: View { let snapshots: [ProviderSnapshot] let selectedID: String? diff --git a/Sources/PokeTokenBar/UI/SettingsView.swift b/Sources/PokeTokenBar/UI/SettingsView.swift index 2e102bf02..bfb99e38b 100644 --- a/Sources/PokeTokenBar/UI/SettingsView.swift +++ b/Sources/PokeTokenBar/UI/SettingsView.swift @@ -1,6 +1,7 @@ import SwiftUI import UniformTypeIdentifiers +@MainActor struct SettingsView: View { @Environment(UsageStore.self) private var store @Environment(CompanionStore.self) private var companion diff --git a/Sources/PokeTokenBar/UI/ShopView.swift b/Sources/PokeTokenBar/UI/ShopView.swift index 416bd2537..3c5706eb1 100644 --- a/Sources/PokeTokenBar/UI/ShopView.swift +++ b/Sources/PokeTokenBar/UI/ShopView.swift @@ -3,6 +3,7 @@ import SwiftUI /// 상점 — 사용한 토큰(재화 = usedSinceInstall − spentTokens)으로 아이템 구매(이상한 사탕·민트). /// 인라인 확인(버튼 morph) — .sheet/.alert 금지(BagView 주석과 동일: transient 팝오버가 닫힐 때 /// 고아 시트가 이후 클릭을 먹통내는 결함 회피). +@MainActor struct ShopView: View { let store: CompanionStore let nav: PopoverNavigation @@ -46,6 +47,7 @@ struct ShopView: View { /// 상점 아이템 1장 — 아이콘·이름·설명(사탕 XP / 민트 "성격 랜덤 변경")·보유수 + 가격/구매(인라인 확인). /// kind 별 store.canBuy(kind)/buy(kind) 로 일반화 — 판매 목록은 store.purchasableItems. +@MainActor private struct ShopItemCard: View { let store: CompanionStore let kind: ItemKind @@ -126,6 +128,7 @@ private struct ShopItemCard: View { /// /// 등급 알의 시각 구분은 **카드의 등급 배지**로만 한다 — 알 스프라이트는 한 장뿐이고, 메뉴바·플로팅 펫은 /// 기존 알 그대로 둔다(새 에셋 없이 구분이 서는 최소 범위). +@MainActor private struct EggCard: View { let store: CompanionStore let nav: PopoverNavigation diff --git a/Tests/PokeTokenBarTests/SwiftUIIsolationTests.swift b/Tests/PokeTokenBarTests/SwiftUIIsolationTests.swift new file mode 100644 index 000000000..0b1628402 --- /dev/null +++ b/Tests/PokeTokenBarTests/SwiftUIIsolationTests.swift @@ -0,0 +1,40 @@ +import XCTest +@testable import PokeTokenBar + +final class SwiftUIIsolationTests: XCTestCase { + func testEverySwiftUIViewAndAppIsMainActorIsolated() throws { + let sources = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() // PokeTokenBarTests + .deletingLastPathComponent() // Tests + .deletingLastPathComponent() // repo root + .appendingPathComponent("Sources/PokeTokenBar") + let enumerator = try XCTUnwrap(FileManager.default.enumerator( + at: sources, includingPropertiesForKeys: nil)) + var offenders: [String] = [] + + for case let url as URL in enumerator where url.pathExtension == "swift" { + let lines = try String(contentsOf: url, encoding: .utf8) + .split(separator: "\n", omittingEmptySubsequences: false) + .map(String.init) + for index in lines.indices { + let declaration = lines[index].trimmingCharacters(in: .whitespaces) + let isStruct = declaration.hasPrefix("struct ") + || declaration.hasPrefix("private struct ") + guard isStruct, + declaration.contains(": View") || declaration.contains(": App") else { continue } + + let previous = lines[../dev/null) +REPORT=$("$LLVM_COV" report "$BIN" -instr-profile="$PROF" "${LOGIC_CORE[@]}" 2>/dev/null) echo "$REPORT" # TOTAL 행의 라인 커버리지(%) 추출 — 컬럼: ... Lines MissedLines Cover(=$10) From ed2404000bb248e86a97d245501fba1a7329c5ed Mon Sep 17 00:00:00 2001 From: vietlubu Date: Wed, 19 Aug 2026 22:18:08 +0700 Subject: [PATCH 2/2] fix: compile main additions with Swift 6.3 --- Sources/PokeTokenBar/UI/CompanionView.swift | 1 + Tests/PokeTokenBarTests/CodexLargeRolloutPerformanceTests.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/PokeTokenBar/UI/CompanionView.swift b/Sources/PokeTokenBar/UI/CompanionView.swift index 7afc88f20..82eb67232 100644 --- a/Sources/PokeTokenBar/UI/CompanionView.swift +++ b/Sources/PokeTokenBar/UI/CompanionView.swift @@ -804,6 +804,7 @@ struct CollectionView: View { /// 도감 하단의 대표 설정 액션. 문구는 툴팁·접근성에 유지하되 시각적으로는 아이콘만 써서, /// 긴 en/es 문구가 선택한 종의 이름·희귀도를 밀어내지 않게 한다. +@MainActor struct RepresentativeFooterButton: View { let localization: L let isRepresentative: Bool diff --git a/Tests/PokeTokenBarTests/CodexLargeRolloutPerformanceTests.swift b/Tests/PokeTokenBarTests/CodexLargeRolloutPerformanceTests.swift index 8fabae8bc..2259b9131 100644 --- a/Tests/PokeTokenBarTests/CodexLargeRolloutPerformanceTests.swift +++ b/Tests/PokeTokenBarTests/CodexLargeRolloutPerformanceTests.swift @@ -1,4 +1,4 @@ -import Darwin +@preconcurrency import Darwin import Foundation import XCTest @testable import PokeTokenBar