From 5b526fa6500e44e02fb31dd68842fcda55aa8f88 Mon Sep 17 00:00:00 2001 From: songyc macbook Date: Tue, 11 Aug 2026 16:31:40 +0900 Subject: [PATCH] =?UTF-8?q?feat(compat):=20iOS=20=ED=95=98=ED=95=9C=2018.0?= =?UTF-8?q?=20=EC=9D=B8=ED=95=98=20=E2=80=94=20=EA=B8=80=EB=9D=BC=EC=8A=A4?= =?UTF-8?q?=20=EB=B2=84=EB=B8=94=20=EB=A8=B8=ED=8B=B0=EB=A6=AC=EC=96=BC=20?= =?UTF-8?q?=ED=8F=B4=EB=B0=B1=C2=B7=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=8B=AC?= =?UTF-8?q?=EB=B3=BC=20=EC=95=88=EC=A0=84=20=EB=A0=8C=EB=8D=94=C2=B7Releas?= =?UTF-8?q?e=20=EC=9D=B8=EB=9D=BC=EC=9D=B4=EB=84=88=20=ED=81=AC=EB=9E=98?= =?UTF-8?q?=EC=8B=9C=20=EC=9A=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 배포 타깃 26.0→18.0 (앱·위젯. 워치 10.0 불변) - RadialNavigationView: glassEffect 계열을 @available(iOS 26) 선언 격리 + 18 머티리얼 원 폴백 (배치·연출 동일) - DiaryZoomContainer.Coordinator → 비제네릭 톱레벨 DiaryZoomCoordinator + AnyView 소거 (하한 18 Release wholemodule에서 swift-frontend SILPerformanceInliner 무한 재귀 크래시 실측·우회) - Image(safeSymbol:)/SymbolCompat: 카탈로그의 상위 OS 전용 심볼(18 기준 3개)이 교차 기기에서 빈 아이콘이 되지 않게 사용자 심볼 렌더 48곳+워치 7곳 폴백 - -symbolAuditDump 검증 인자 추가, iOS 18.5 시뮬 QA(빌드·radial 폴백·일기 줌·progressSelfTest 49 ALL PASS) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NNxA172hNDRpDsJ2qztKPf --- .../Haru_Danim.xcodeproj/project.pbxproj | 8 +- .../ContentView.swift | 2 +- .../RateComplications.swift | 8 +- .../StatusComplication.swift | 4 +- .../Haru_DanimWatchWidgets/SymbolCompat.swift | 25 ++++ myApp/HaruDanim/IOS/ContentView.swift | 1 + myApp/HaruDanim/IOS/Core/DebugSeed.swift | 12 ++ myApp/HaruDanim/IOS/Core/SymbolCatalog.swift | 9 ++ myApp/HaruDanim/IOS/Views/ActionViews.swift | 8 +- .../IOS/Views/DiaryDefaultsSheet.swift | 4 +- myApp/HaruDanim/IOS/Views/DiaryNotePage.swift | 38 +++--- myApp/HaruDanim/IOS/Views/DiaryView.swift | 6 +- .../HaruDanim/IOS/Views/ExportImageView.swift | 6 +- myApp/HaruDanim/IOS/Views/GoalViews.swift | 8 +- myApp/HaruDanim/IOS/Views/HistoryView.swift | 10 +- myApp/HaruDanim/IOS/Views/MainView.swift | 12 +- .../HaruDanim/IOS/Views/QuestEditorView.swift | 2 +- .../IOS/Views/RadialNavigationView.swift | 121 +++++++++++++++--- .../IOS/Views/RecordFilterSheet.swift | 2 +- myApp/HaruDanim/IOS/Views/SettingsView.swift | 2 +- myApp/HaruDanim/Shared/SymbolCompat.swift | 29 +++++ .../Widgets/ActionControlWidget.swift | 2 +- myApp/HaruDanim/Widgets/ActionRunWidget.swift | 4 +- myApp/HaruDanim/Widgets/GoalWidgets.swift | 6 +- .../HaruDanim/Widgets/HaruDanimWidgets.swift | 6 +- .../HaruDanim/Widgets/LockScreenWidgets.swift | 6 +- .../HaruDanim/Widgets/NowRunningWidget.swift | 8 +- myApp/HaruDanim/Widgets/WidgetSupport.swift | 4 +- 28 files changed, 258 insertions(+), 95 deletions(-) create mode 100644 myApp/HaruDanim/Haru_DanimWatchWidgets/SymbolCompat.swift create mode 100644 myApp/HaruDanim/Shared/SymbolCompat.swift diff --git a/myApp/HaruDanim/Haru_Danim.xcodeproj/project.pbxproj b/myApp/HaruDanim/Haru_Danim.xcodeproj/project.pbxproj index b6d3b2a..73ff308 100644 --- a/myApp/HaruDanim/Haru_Danim.xcodeproj/project.pbxproj +++ b/myApp/HaruDanim/Haru_Danim.xcodeproj/project.pbxproj @@ -527,7 +527,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -584,7 +584,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -832,7 +832,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Widgets/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "하루 다님 위젯"; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -858,7 +858,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Widgets/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "하루 다님 위젯"; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/myApp/HaruDanim/Haru_DanimWatch Watch App/ContentView.swift b/myApp/HaruDanim/Haru_DanimWatch Watch App/ContentView.swift index cc98f9a..67c0fcf 100644 --- a/myApp/HaruDanim/Haru_DanimWatch Watch App/ContentView.swift +++ b/myApp/HaruDanim/Haru_DanimWatch Watch App/ContentView.swift @@ -203,7 +203,7 @@ struct WatchActionListView: View { store.run(action) } label: { HStack(spacing: 8) { - Image(systemName: action.symbolName) + Image(safeSymbol: action.symbolName) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(.white) .frame(width: 28, height: 28) diff --git a/myApp/HaruDanim/Haru_DanimWatchWidgets/RateComplications.swift b/myApp/HaruDanim/Haru_DanimWatchWidgets/RateComplications.swift index b5f1df5..00c2a7f 100644 --- a/myApp/HaruDanim/Haru_DanimWatchWidgets/RateComplications.swift +++ b/myApp/HaruDanim/Haru_DanimWatchWidgets/RateComplications.swift @@ -116,7 +116,7 @@ struct GoalRateComplicationView: View { case .accessoryRectangular: VStack(alignment: .leading, spacing: 2) { HStack(spacing: 4) { - Image(systemName: entry.symbolName) + Image(safeSymbol: entry.symbolName) .font(.system(size: 11)) Text(title) .font(.headline) @@ -130,7 +130,7 @@ struct GoalRateComplicationView: View { } default: Gauge(value: min(max(entry.ratio, 0), 1)) { - Image(systemName: entry.symbolName) + Image(safeSymbol: entry.symbolName) } currentValueLabel: { Text("\(percent)") .font(.system(size: 13, weight: .bold).monospacedDigit()) @@ -261,7 +261,7 @@ struct QuestRateComplicationView: View { // 크기가 큰 패밀리는 하루/주간/월간 세 개를 모두 표기 (스펙 §9.2-3) VStack(alignment: .leading, spacing: 2) { HStack(spacing: 4) { - Image(systemName: quest.symbolName) + Image(safeSymbol: quest.symbolName) .font(.system(size: 11)) Text(quest.name) .font(.headline) @@ -306,7 +306,7 @@ struct QuestRateComplicationView: View { } } else { Gauge(value: min(max(quest.ratio(forSpanRaw: entry.spanRaw), 0), 1)) { - Image(systemName: quest.symbolName) + Image(safeSymbol: quest.symbolName) } currentValueLabel: { Text("\(Int((quest.ratio(forSpanRaw: entry.spanRaw) * 100).rounded()))") .font(.system(size: 13, weight: .bold).monospacedDigit()) diff --git a/myApp/HaruDanim/Haru_DanimWatchWidgets/StatusComplication.swift b/myApp/HaruDanim/Haru_DanimWatchWidgets/StatusComplication.swift index 65938fe..fe1e2ac 100644 --- a/myApp/HaruDanim/Haru_DanimWatchWidgets/StatusComplication.swift +++ b/myApp/HaruDanim/Haru_DanimWatchWidgets/StatusComplication.swift @@ -114,7 +114,7 @@ struct StatusComplicationView: View { case .accessoryRectangular: VStack(alignment: .leading, spacing: 1) { HStack(spacing: 4) { - Image(systemName: entry.runningSymbol ?? "record.circle") + Image(safeSymbol: entry.runningSymbol ?? "record.circle") .font(.system(size: 11)) Text(name) .font(.headline) @@ -136,7 +136,7 @@ struct StatusComplicationView: View { } default: VStack(spacing: 1) { - Image(systemName: entry.runningSymbol ?? "record.circle") + Image(safeSymbol: entry.runningSymbol ?? "record.circle") .font(.system(size: 14, weight: .semibold)) if let start = entry.runningStartAt { Text(start, style: .timer) diff --git a/myApp/HaruDanim/Haru_DanimWatchWidgets/SymbolCompat.swift b/myApp/HaruDanim/Haru_DanimWatchWidgets/SymbolCompat.swift new file mode 100644 index 0000000..3a6ec37 --- /dev/null +++ b/myApp/HaruDanim/Haru_DanimWatchWidgets/SymbolCompat.swift @@ -0,0 +1,25 @@ +// +// SymbolCompat.swift +// Haru_DanimWatchWidgets +// +// 사용자 지정 SF 심볼의 안전 렌더 — 워치 하한(watchOS 10)에는 없는 신형 심볼을 +// 아이폰(iOS 26)에서 골라 동기화하면 빈 아이콘이 되는 것을 물음표로 대치한다. +// Shared/SymbolCompat.swift(앱·위젯)와 같은 헬퍼의 워치 컴플리케이션 사본 — +// 폴더 동기화 그룹 멤버십이 갈려 한 파일을 공유할 수 없다. +// + +import SwiftUI +import UIKit + +enum WatchSymbolCompat { + static func safe(_ name: String) -> String { + UIImage(systemName: name) != nil ? name : "questionmark.circle.fill" + } +} + +extension Image { + /// 사용자 데이터의 심볼 렌더 전용 — 이 OS에 없는 이름은 물음표로 대치 + init(safeSymbol name: String) { + self.init(systemName: WatchSymbolCompat.safe(name)) + } +} diff --git a/myApp/HaruDanim/IOS/ContentView.swift b/myApp/HaruDanim/IOS/ContentView.swift index 42609de..4d6e191 100644 --- a/myApp/HaruDanim/IOS/ContentView.swift +++ b/myApp/HaruDanim/IOS/ContentView.swift @@ -68,6 +68,7 @@ struct ContentView: View { DebugSeed.watchGoalsIfRequested(context: context) DebugSeed.endGoalYesterdayIfRequested(context: context) DebugSeed.dumpStreaksIfRequested(context: context) + DebugSeed.dumpMissingSymbolsIfRequested() DebugSeed.runProgressSelfTestIfRequested() CSVImport.selfTestIfRequested() await DebugSeed.runIntentSmokeTestIfRequested() diff --git a/myApp/HaruDanim/IOS/Core/DebugSeed.swift b/myApp/HaruDanim/IOS/Core/DebugSeed.swift index c9250c0..4257386 100644 --- a/myApp/HaruDanim/IOS/Core/DebugSeed.swift +++ b/myApp/HaruDanim/IOS/Core/DebugSeed.swift @@ -485,6 +485,18 @@ enum DebugSeed { try? lines.joined(separator: "\n").write(to: url, atomically: true, encoding: .utf8) } + /// `-symbolAuditDump` 런치 인자: 아이콘 카탈로그에서 현재 OS에 없는 심볼을 + /// Documents/symbol-audit.txt로 기록 — 하위 OS(iOS 18) QA용. 선택기는 어차피 + /// 런타임 필터로 걸러지므로, 이 목록은 "26 기기에서 고른 심볼이 18 기기에서 + /// 빈 아이콘이 될 수 있는 후보"를 뜻한다 (비어 있으면 교차 기기 위험 없음). + static func dumpMissingSymbolsIfRequested() { + guard UserDefaults.standard.bool(forKey: "symbolAuditDump") else { return } + let missing = SymbolCatalog.missingSymbolsOnThisOS + let lines = ["missing=\(missing.count)"] + missing + let url = URL.documentsDirectory.appending(path: "symbol-audit.txt") + try? lines.joined(separator: "\n").write(to: url, atomically: true, encoding: .utf8) + } + /// `-watchGoals ` 런치 인자: 생성순 목표 N개를 '애플워치에서 보기'로 설정 /// (0이면 전체 해제 — 워치 컴플리케이션 필터 검증용. 설정 후 스냅숏을 즉시 푸시) static func watchGoalsIfRequested(context: ModelContext) { diff --git a/myApp/HaruDanim/IOS/Core/SymbolCatalog.swift b/myApp/HaruDanim/IOS/Core/SymbolCatalog.swift index 240058a..1bac665 100644 --- a/myApp/HaruDanim/IOS/Core/SymbolCatalog.swift +++ b/myApp/HaruDanim/IOS/Core/SymbolCatalog.swift @@ -25,6 +25,15 @@ enum SymbolCatalog { return categories.flatMap(\.symbols).filter { seen.insert($0).inserted } } + #if DEBUG + /// 검증용(-symbolAuditDump): 카탈로그 원본 중 현재 OS에 없는 심볼 — 하위 OS QA (§14) + static var missingSymbolsOnThisOS: [String] { + var seen = Set() + return rawCategories.flatMap(\.1) + .filter { seen.insert($0).inserted && UIImage(systemName: $0) == nil } + } + #endif + private static let rawCategories: [(String, [String])] = [ (String(localized: "인기"), [ "star.fill", "heart.fill", "flame.fill", "bolt.fill", "leaf.fill", diff --git a/myApp/HaruDanim/IOS/Views/ActionViews.swift b/myApp/HaruDanim/IOS/Views/ActionViews.swift index 836653f..6c6a1e2 100644 --- a/myApp/HaruDanim/IOS/Views/ActionViews.swift +++ b/myApp/HaruDanim/IOS/Views/ActionViews.swift @@ -170,7 +170,7 @@ struct ActionRow: View { var body: some View { HStack(spacing: 12) { - Image(systemName: action.symbolName) + Image(safeSymbol: action.symbolName) .font(.system(size: 16, weight: .semibold)) .foregroundStyle(.white) .frame(width: 34, height: 34) @@ -263,7 +263,7 @@ struct FavoriteOrderSheet: View { Section { ForEach(favorites) { action in HStack(spacing: 10) { - Image(systemName: action.symbolName) + Image(safeSymbol: action.symbolName) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(.white) .frame(width: 28, height: 28) @@ -347,7 +347,7 @@ struct ActionDetailView: View { HStack { Text("아이콘") Spacer() - Image(systemName: action.symbolName) + Image(safeSymbol: action.symbolName) .foregroundStyle(.white) .frame(width: 30, height: 30) .background(action.color, in: RoundedRectangle(cornerRadius: 8)) @@ -461,7 +461,7 @@ struct ActionEditorView: View { showingSymbolPicker = true } label: { HStack { - Image(systemName: symbolName) + Image(safeSymbol: symbolName) .font(.system(size: 20)) .foregroundStyle(AppTheme.green) .frame(width: 36, height: 36) diff --git a/myApp/HaruDanim/IOS/Views/DiaryDefaultsSheet.swift b/myApp/HaruDanim/IOS/Views/DiaryDefaultsSheet.swift index 3498ab3..765f017 100644 --- a/myApp/HaruDanim/IOS/Views/DiaryDefaultsSheet.swift +++ b/myApp/HaruDanim/IOS/Views/DiaryDefaultsSheet.swift @@ -281,7 +281,7 @@ struct DiaryDefaultsSheet: View { } )) { HStack(spacing: 10) { - Image(systemName: goal.symbolName) + Image(safeSymbol: goal.symbolName) .font(.system(size: 12, weight: .semibold)) .foregroundStyle(.white) .frame(width: 26, height: 26) @@ -319,7 +319,7 @@ struct DiaryDefaultsSheet: View { } )) { HStack(spacing: 10) { - Image(systemName: action.symbolName) + Image(safeSymbol: action.symbolName) .font(.system(size: 12, weight: .semibold)) .foregroundStyle(.white) .frame(width: 26, height: 26) diff --git a/myApp/HaruDanim/IOS/Views/DiaryNotePage.swift b/myApp/HaruDanim/IOS/Views/DiaryNotePage.swift index f4195cd..0355757 100644 --- a/myApp/HaruDanim/IOS/Views/DiaryNotePage.swift +++ b/myApp/HaruDanim/IOS/Views/DiaryNotePage.swift @@ -465,8 +465,8 @@ private struct DiaryZoomContainer: UIViewRepresentable { let zoomEnabled: Bool @ViewBuilder let content: () -> Content - func makeCoordinator() -> Coordinator { - Coordinator() + func makeCoordinator() -> DiaryZoomCoordinator { + DiaryZoomCoordinator() } func makeUIView(context: Context) -> DiaryZoomScrollView { @@ -490,7 +490,7 @@ private struct DiaryZoomContainer: UIViewRepresentable { NSNumber(value: UITouch.TouchType.direct.rawValue) ] - let hosting = UIHostingController(rootView: content()) + let hosting = UIHostingController(rootView: AnyView(content())) hosting.view.backgroundColor = .clear hosting.safeAreaRegions = [] hosting.view.frame = CGRect(origin: .zero, size: pageSize) @@ -516,27 +516,33 @@ private struct DiaryZoomContainer: UIViewRepresentable { } func updateUIView(_ scroll: DiaryZoomScrollView, context: Context) { - context.coordinator.hosting?.rootView = content() + context.coordinator.hosting?.rootView = AnyView(content()) scroll.pinchGestureRecognizer?.isEnabled = zoomEnabled // 배치 모드에서는 한 손가락 드래그를 요소 이동에 양보 (페이지 이동은 두 손가락) scroll.panGestureRecognizer.minimumNumberOfTouches = zoomEnabled ? 1 : 2 } +} - final class Coordinator: NSObject, UIScrollViewDelegate { - var hosting: UIHostingController? +/// ⚠️ 비제네릭·톱레벨 + AnyView 소거 유지(1.4 하한 18 컴파일러 우회): 예전처럼 +/// DiaryZoomContainer 안에 중첩해 UIHostingController를 들면, +/// 하한 18 Release(-O wholemodule) 빌드에서 swift-frontend가 이 클래스의 deinit을 +/// 최적화하다 크래시한다(SILPerformanceInliner의 +/// isCallerAndCalleeLayoutConstraintsCompatible 무한 재귀 — 하한 26에서는 정상, 실측). +/// rootView는 원래 매 갱신 통째로 교체되므로 AnyView 소거의 동작 차이는 없다. +private final class DiaryZoomCoordinator: NSObject, UIScrollViewDelegate { + var hosting: UIHostingController? - func viewForZooming(in scrollView: UIScrollView) -> UIView? { - (scrollView as? DiaryZoomScrollView)?.zoomedView - } + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + (scrollView as? DiaryZoomScrollView)?.zoomedView + } - func scrollViewDidZoom(_ scrollView: UIScrollView) { - (scrollView as? DiaryZoomScrollView)?.centerZoomedView() - } + func scrollViewDidZoom(_ scrollView: UIScrollView) { + (scrollView as? DiaryZoomScrollView)?.centerZoomedView() + } - func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { - // 핀치가 끝난 배율에 맞춰 캔버스 래스터 해상도를 다시 맞춘다 (확대 시 픽셀 번짐 방지) - (scrollView as? DiaryZoomScrollView)?.updateCanvasSharpness() - } + func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { + // 핀치가 끝난 배율에 맞춰 캔버스 래스터 해상도를 다시 맞춘다 (확대 시 픽셀 번짐 방지) + (scrollView as? DiaryZoomScrollView)?.updateCanvasSharpness() } } diff --git a/myApp/HaruDanim/IOS/Views/DiaryView.swift b/myApp/HaruDanim/IOS/Views/DiaryView.swift index f7a8ec7..65e02b9 100644 --- a/myApp/HaruDanim/IOS/Views/DiaryView.swift +++ b/myApp/HaruDanim/IOS/Views/DiaryView.swift @@ -1565,7 +1565,7 @@ private struct DiaryGoalsCard: View { let dayRatio = goal.combinedSpanRatio(.day, math: math, now: referenceNow) VStack(alignment: .leading, spacing: 10) { HStack(spacing: 10) { - Image(systemName: goal.symbolName) + Image(safeSymbol: goal.symbolName) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(.white) .frame(width: 28, height: 28) @@ -1627,7 +1627,7 @@ private struct DiaryGoalsCard: View { let target = quest.targetValue let limit = quest.direction == .atMost HStack(alignment: .firstTextBaseline, spacing: 8) { - Image(systemName: quest.targetSymbol) + Image(safeSymbol: quest.targetSymbol) .font(.system(size: 10, weight: .semibold)) .foregroundStyle(.white) .frame(width: 20, height: 20) @@ -1798,7 +1798,7 @@ private struct DiaryGoalFilterSheet: View { toggle(id) } label: { HStack(spacing: 10) { - Image(systemName: goal.symbolName) + Image(safeSymbol: goal.symbolName) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(.white) .frame(width: 28, height: 28) diff --git a/myApp/HaruDanim/IOS/Views/ExportImageView.swift b/myApp/HaruDanim/IOS/Views/ExportImageView.swift index 221771e..91fc0b5 100644 --- a/myApp/HaruDanim/IOS/Views/ExportImageView.swift +++ b/myApp/HaruDanim/IOS/Views/ExportImageView.swift @@ -978,7 +978,7 @@ struct ExportSectionView: View { private func recordRow(_ row: ExportRecordRow) -> some View { HStack(spacing: 10) { - Image(systemName: row.symbol) + Image(safeSymbol: row.symbol) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(.white) .frame(width: 28, height: 28) @@ -1082,7 +1082,7 @@ struct ExportSectionView: View { ForEach(group.rows) { row in GridRow { HStack(spacing: 8) { - Image(systemName: row.symbol) + Image(safeSymbol: row.symbol) .font(.system(size: 11, weight: .semibold)) .foregroundStyle(.white) .frame(width: 24, height: 24) @@ -1195,7 +1195,7 @@ private struct ExportTimetableView: View { .lineLimit(1) .padding(4) } else if height >= 12 { - Image(systemName: block.symbol) + Image(safeSymbol: block.symbol) .font(.system(size: 7, weight: .bold)) .foregroundStyle(.white) .padding(2) diff --git a/myApp/HaruDanim/IOS/Views/GoalViews.swift b/myApp/HaruDanim/IOS/Views/GoalViews.swift index c8a3588..473086a 100644 --- a/myApp/HaruDanim/IOS/Views/GoalViews.swift +++ b/myApp/HaruDanim/IOS/Views/GoalViews.swift @@ -197,7 +197,7 @@ struct GoalListView: View { /// 순서 변경 모드에서 보여줄 간략한 목표 행 private func reorderRow(_ goal: Goal) -> some View { HStack(spacing: 10) { - Image(systemName: goal.symbolName) + Image(safeSymbol: goal.symbolName) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(.white) .frame(width: 28, height: 28) @@ -249,7 +249,7 @@ struct GoalRow: View { var body: some View { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 10) { - Image(systemName: goal.symbolName) + Image(safeSymbol: goal.symbolName) .font(.system(size: 16, weight: .semibold)) .foregroundStyle(.white) .frame(width: 34, height: 34) @@ -339,7 +339,7 @@ struct QuestRow: View { var body: some View { VStack(alignment: .leading, spacing: 6) { HStack(spacing: 8) { - Image(systemName: quest.targetSymbol) + Image(safeSymbol: quest.targetSymbol) .font(.system(size: 12, weight: .semibold)) .foregroundStyle(.white) .frame(width: 24, height: 24) @@ -641,7 +641,7 @@ struct GoalEditorView: View { showingSymbolPicker = true } label: { HStack { - Image(systemName: symbolName) + Image(safeSymbol: symbolName) .font(.system(size: 18)) .foregroundStyle(.white) .frame(width: 34, height: 34) diff --git a/myApp/HaruDanim/IOS/Views/HistoryView.swift b/myApp/HaruDanim/IOS/Views/HistoryView.swift index c2c5cec..43387b6 100644 --- a/myApp/HaruDanim/IOS/Views/HistoryView.swift +++ b/myApp/HaruDanim/IOS/Views/HistoryView.swift @@ -473,7 +473,7 @@ private struct HistoryRecordsView: View { onTapSession(item.session) } label: { HStack(spacing: 10) { - Image(systemName: item.action.symbolName) + Image(safeSymbol: item.action.symbolName) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(.white) .frame(width: 30, height: 30) @@ -512,7 +512,7 @@ private struct HistoryRecordsView: View { onTapEntry(item.entry) } label: { HStack(spacing: 10) { - Image(systemName: item.action.symbolName) + Image(safeSymbol: item.action.symbolName) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(.white) .frame(width: 30, height: 30) @@ -667,13 +667,13 @@ struct TimetableView: View { .fill(item.action.color.opacity(0.85)) .overlay(alignment: .topLeading) { if !weekly && height >= 18 { - Label(item.action.name, systemImage: item.action.symbolName) + Label(item.action.name, systemImage: SymbolCompat.safe(item.action.symbolName)) .font(.caption2.weight(.semibold)) .foregroundStyle(.white) .lineLimit(1) .padding(4) } else if height >= 14 { - Image(systemName: item.action.symbolName) + Image(safeSymbol: item.action.symbolName) .font(.system(size: 8, weight: .bold)) .foregroundStyle(.white) .padding(2) @@ -701,7 +701,7 @@ struct TimetableView: View { Circle() .strokeBorder(AppTheme.surface, lineWidth: 2) if !weekly { - Image(systemName: item.action.symbolName) + Image(safeSymbol: item.action.symbolName) .font(.system(size: 8, weight: .bold)) .foregroundStyle(.white) } diff --git a/myApp/HaruDanim/IOS/Views/MainView.swift b/myApp/HaruDanim/IOS/Views/MainView.swift index 81c3dad..17f353e 100644 --- a/myApp/HaruDanim/IOS/Views/MainView.swift +++ b/myApp/HaruDanim/IOS/Views/MainView.swift @@ -721,7 +721,7 @@ struct GoalSummaryCard: View { } label: { VStack(alignment: .leading, spacing: 10) { HStack(spacing: 10) { - Image(systemName: goal.symbolName) + Image(safeSymbol: goal.symbolName) .font(.system(size: 15, weight: .semibold)) .foregroundStyle(.white) .frame(width: 32, height: 32) @@ -807,7 +807,7 @@ struct GoalSummaryCard: View { let progress = QuestProgress(quest: quest) return VStack(alignment: .leading, spacing: 4) { HStack(spacing: 6) { - Image(systemName: quest.targetSymbol) + Image(safeSymbol: quest.targetSymbol) .font(.system(size: 9, weight: .semibold)) .foregroundStyle(.white) .frame(width: 18, height: 18) @@ -905,7 +905,7 @@ struct SessionMemoSheet: View { VStack(alignment: .leading, spacing: 12) { if let action = session.action { HStack(spacing: 10) { - Image(systemName: action.symbolName) + Image(safeSymbol: action.symbolName) .font(.system(size: 15, weight: .semibold)) .foregroundStyle(.white) .frame(width: 32, height: 32) @@ -964,7 +964,7 @@ struct CountMemoSheet: View { VStack(alignment: .leading, spacing: 12) { if let action = entry.action { HStack(spacing: 10) { - Image(systemName: action.symbolName) + Image(safeSymbol: action.symbolName) .font(.system(size: 15, weight: .semibold)) .foregroundStyle(.white) .frame(width: 32, height: 32) @@ -1093,7 +1093,7 @@ struct RunningSessionRow: View { var body: some View { HStack(spacing: 10) { - Image(systemName: action.symbolName) + Image(safeSymbol: action.symbolName) .font(.system(size: 16, weight: .semibold)) .foregroundStyle(.white) .frame(width: 34, height: 34) @@ -1212,7 +1212,7 @@ struct ActionButtonCell: View { } label: { VStack(alignment: .leading, spacing: compact ? 3 : 6) { HStack { - Image(systemName: action.symbolName) + Image(safeSymbol: action.symbolName) .font(.system(size: compact ? 17 : 26, weight: .medium)) .foregroundStyle(.white) .symbolEffect(.bounce, value: bounce) diff --git a/myApp/HaruDanim/IOS/Views/QuestEditorView.swift b/myApp/HaruDanim/IOS/Views/QuestEditorView.swift index 53f263f..39c2e7a 100644 --- a/myApp/HaruDanim/IOS/Views/QuestEditorView.swift +++ b/myApp/HaruDanim/IOS/Views/QuestEditorView.swift @@ -98,7 +98,7 @@ struct QuestEditorView: View { Picker("행동", selection: $selectedAction) { Text("선택").tag(Action?.none) ForEach(allActions) { action in - Label(action.name, systemImage: action.symbolName) + Label(action.name, systemImage: SymbolCompat.safe(action.symbolName)) .tag(Optional(action)) } } diff --git a/myApp/HaruDanim/IOS/Views/RadialNavigationView.swift b/myApp/HaruDanim/IOS/Views/RadialNavigationView.swift index 51f8aa2..7039631 100644 --- a/myApp/HaruDanim/IOS/Views/RadialNavigationView.swift +++ b/myApp/HaruDanim/IOS/Views/RadialNavigationView.swift @@ -21,6 +21,9 @@ // - 등장: 스태거(순차) 바운스 스프링 + 스월 회전. 닫힘: 지연 없이 FAB로 스냅 흡수. // 펼치면 메뉴 뒤로 은은한 초록 빛 무리(정적 그라데이션이라 렌더 비용 없음)가 퍼진다. // - 아치 반경이 엄지 반경(~150pt) 안이라 세로 스택과 달리 모든 탭이 한 손에 닿는다. +// - iOS 18 폴백(1.4 하위 OS 지원): glassEffect 계열은 26 전용이라, 18에서는 같은 구슬을 +// 머티리얼 원(블러+하이라이트 테두리+그림자)으로 그린다 — 굴절·액체 모핑만 빠지고 +// 배치·스태거 스프링·더블탭 등 동작은 완전히 동일하다 (LiquidBallBackground 참고). // // iPhone(Compact) 전용 — iPad·Mac은 SidebarRootView, watch는 별도 타깃이라 이 파일과 무관하다. // @@ -129,16 +132,15 @@ struct GlassBubbleMenu: View { // FAB(하단 중앙)를 기준점으로 빛 무리와 버블들이 부채꼴로 퍼진다. ZStack { glow(radius: radius) - // 모든 유리 요소를 한 컨테이너에 담아 한 패스로 렌더링 + 근접 시 액체 모핑 - GlassEffectContainer(spacing: glassBlendSpacing) { - ZStack { - if expanded { - ForEach(Array(tabs.enumerated()), id: \.element.id) { index, tab in - bubble(tab, index: index, count: tabs.count, radius: radius) - } - } - fab + // 모든 유리 요소를 한 컨테이너에 담아 한 패스로 렌더링 + 근접 시 액체 모핑. + // iOS 18은 컨테이너가 없어 같은 클러스터를 그대로 그린다(모핑만 없음). + // 26 API 직접 호출 금지 — LiquidGlassBall 주석의 컴파일러 우회 규칙 참고 + if #available(iOS 26.0, *) { + GlassClusterContainer(spacing: glassBlendSpacing) { + bubbleCluster(radius: radius) } + } else { + bubbleCluster(radius: radius) } } .frame(width: fabSize, height: fabSize) @@ -196,6 +198,18 @@ struct GlassBubbleMenu: View { .allowsHitTesting(false) } + /// FAB + 버블 클러스터 — 26의 GlassEffectContainer 안팎에서 같은 계층을 쓴다 + private func bubbleCluster(radius: CGFloat) -> some View { + ZStack { + if expanded { + ForEach(Array(tabs.enumerated()), id: \.element.id) { index, tab in + bubble(tab, index: index, count: tabs.count, radius: radius) + } + } + fab + } + } + // MARK: 유리 구슬 버튼 /// 탭 하나를 리퀴드 글라스 구슬로 렌더링 — 아이콘과 이름이 한 구슬 안에 있어 @@ -230,15 +244,8 @@ struct GlassBubbleMenu: View { .contentShape(Circle()) } .buttonStyle(.plain) - // 진짜 유리: 배경 굴절·빛 반사에 더해 .interactive가 눌림 시머·바운스까지 그려 준다. - // 현재 탭은 초록 틴트 유리로 강조. - .glassEffect( - isCurrent - ? .regular.tint(AppTheme.green.opacity(0.85)).interactive() - : .regular.interactive(), - in: .circle - ) - .glassEffectID(tab.rawValue, in: glassSpace) + // 진짜 유리(26) 또는 머티리얼 원(18): 현재 탭은 양쪽 모두 초록으로 강조. + .modifier(LiquidBallBackground(tinted: isCurrent, morphID: tab.rawValue, namespace: glassSpace)) .offset(x: bloomed ? dx : 0, y: bloomed ? dy : 0) // 펼칠 때만 안쪽부터 차례로 맺히는 스태거 + 감쇠 낮은 스프링(오버슈트로 액체 출렁임). // 되돌아갈 땐 지연 없이 한 번에 FAB로 흡수된다. @@ -266,8 +273,7 @@ struct GlassBubbleMenu: View { .contentShape(Circle()) } .buttonStyle(.plain) - .glassEffect(.regular.interactive(), in: .circle) - .glassEffectID("fab", in: glassSpace) + .modifier(LiquidBallBackground(tinted: false, morphID: "fab", namespace: glassSpace)) // 펼치면 살짝 커져 '유리가 부풀며 방울을 내보내는' 기준점이 된다 .scaleEffect(expanded ? 1.07 : 1) .animation(.spring(response: 0.3, dampingFraction: 0.7), value: expanded) @@ -322,3 +328,78 @@ struct GlassBubbleMenu: View { close() } } + +// MARK: - 구슬 배경 (26 리퀴드 글라스 / 18 머티리얼 폴백) + +/// 26 전용 컨테이너 래퍼 — LiquidGlassBall과 같은 이유로 선언을 @available로 격리 +@available(iOS 26.0, *) +private struct GlassClusterContainer: View { + let spacing: CGFloat + @ViewBuilder let content: Content + + var body: some View { + GlassEffectContainer(spacing: spacing) { content } + } +} + +/// 유리 구슬 배경 — iOS 26: 시스템 리퀴드 글라스(.regular.interactive — 배경 굴절·빛 반사· +/// 눌림 시머) + glassEffectID로 FAB와 한 덩어리 모핑. 현재 탭은 초록 틴트 유리. +/// iOS 18 폴백: 머티리얼 블러 원 + 하이라이트 테두리 + 그림자 — 굴절·모핑만 없고 +/// 크기·배치·애니메이션은 26과 동일해 사용성 차이가 없다. 현재 탭은 초록 채움 원. +/// 26 전용 유리 모디파이어 — 선언을 @available로 격리해 가용성 분기(하위 18 폴백)와 +/// glassEffect 제네릭이 뒤섞이지 않게 한다 (하한 18 Release 인라이너 크래시 조사 때 +/// 정리한 방어적 형태 — 실제 크래시 원인은 DiaryZoomCoordinator 쪽이었다, §6.7) +@available(iOS 26.0, *) +private struct LiquidGlassBall: ViewModifier { + let tinted: Bool + let morphID: String + let namespace: Namespace.ID + + func body(content: Content) -> some View { + content + .glassEffect( + tinted + ? .regular.tint(AppTheme.green.opacity(0.85)).interactive() + : .regular.interactive(), + in: .circle + ) + .glassEffectID(morphID, in: namespace) + } +} + +/// iOS 18 폴백 구슬 — 머티리얼 블러 원 + 하이라이트 테두리 + 그림자 +private struct MaterialBall: ViewModifier { + let tinted: Bool + + func body(content: Content) -> some View { + content + .background { + ZStack { + Circle().fill(.ultraThinMaterial) + if tinted { + Circle().fill(AppTheme.green.opacity(0.85)) + } + Circle().strokeBorder(.white.opacity(0.28), lineWidth: 0.6) + } + .shadow(color: .black.opacity(0.2), radius: 9, y: 4) + } + } +} + +private struct LiquidBallBackground: ViewModifier { + let tinted: Bool + let morphID: String + let namespace: Namespace.ID + + // 분기는 OS로 고정되어 런타임에 바뀌지 않으므로 AnyView 소거가 + // 애니메이션·모핑 정체성에 영향을 주지 않는다 + func body(content: Content) -> AnyView { + if #available(iOS 26.0, *) { + return AnyView(content.modifier( + LiquidGlassBall(tinted: tinted, morphID: morphID, namespace: namespace) + )) + } else { + return AnyView(content.modifier(MaterialBall(tinted: tinted))) + } + } +} diff --git a/myApp/HaruDanim/IOS/Views/RecordFilterSheet.swift b/myApp/HaruDanim/IOS/Views/RecordFilterSheet.swift index b56856b..1087322 100644 --- a/myApp/HaruDanim/IOS/Views/RecordFilterSheet.swift +++ b/myApp/HaruDanim/IOS/Views/RecordFilterSheet.swift @@ -189,7 +189,7 @@ struct RecordFilterSheet: View { } } label: { HStack(spacing: 10) { - Image(systemName: action.symbolName) + Image(safeSymbol: action.symbolName) .font(.system(size: 12, weight: .semibold)) .foregroundStyle(.white) .frame(width: 24, height: 24) diff --git a/myApp/HaruDanim/IOS/Views/SettingsView.swift b/myApp/HaruDanim/IOS/Views/SettingsView.swift index 97b3897..38306c8 100644 --- a/myApp/HaruDanim/IOS/Views/SettingsView.swift +++ b/myApp/HaruDanim/IOS/Views/SettingsView.swift @@ -352,7 +352,7 @@ struct SettingsView: View { .foregroundStyle(.primary) .lineLimit(1) } icon: { - Image(systemName: goal.symbolName) + Image(safeSymbol: goal.symbolName) .foregroundStyle(goal.color) } Spacer() diff --git a/myApp/HaruDanim/Shared/SymbolCompat.swift b/myApp/HaruDanim/Shared/SymbolCompat.swift new file mode 100644 index 0000000..f23401c --- /dev/null +++ b/myApp/HaruDanim/Shared/SymbolCompat.swift @@ -0,0 +1,29 @@ +// +// SymbolCompat.swift +// Haru_Danim +// +// 사용자 지정 SF 심볼의 안전 렌더 (1.4 iOS 18 하위 지원). +// 아이콘 카탈로그에는 상위 OS 전용 신형 심볼(예: SF 7의 broom.fill)이 들어 있어, +// 26 기기에서 고른 심볼이 iCloud 동기화로 iOS 18 기기(또는 구형 watchOS)에 오면 +// Image(systemName:)이 아무것도 그리지 않는다. 사용자 데이터(행동·목표)에서 온 +// 심볼 이름은 반드시 이 경유지로 렌더할 것 — 코드 리터럴 심볼(전부 구형)은 비대상. +// 워치 앱·워치 위젯 타깃에는 같은 헬퍼의 사본이 있다(WatchSymbolCompat — +// 폴더 동기화 그룹 멤버십이 갈려 한 파일을 공유할 수 없음). +// + +import SwiftUI +import UIKit + +enum SymbolCompat { + /// 이 OS에 없는 심볼 이름을 안전한 폴백으로 바꾼다 + static func safe(_ name: String) -> String { + UIImage(systemName: name) != nil ? name : "questionmark.circle.fill" + } +} + +extension Image { + /// 사용자 데이터의 심볼 렌더 전용 — 이 OS에 없는 이름은 물음표로 대치 + init(safeSymbol name: String) { + self.init(systemName: SymbolCompat.safe(name)) + } +} diff --git a/myApp/HaruDanim/Widgets/ActionControlWidget.swift b/myApp/HaruDanim/Widgets/ActionControlWidget.swift index 86c8c0d..4591cb6 100644 --- a/myApp/HaruDanim/Widgets/ActionControlWidget.swift +++ b/myApp/HaruDanim/Widgets/ActionControlWidget.swift @@ -90,7 +90,7 @@ struct ActionRunControl: ControlWidget { : value.isCount ? String(localized: "탭해서 +1") : isOn ? String(localized: "측정 중") : String(localized: "시작"), - systemImage: value.symbolName + systemImage: SymbolCompat.safe(value.symbolName) ) } } diff --git a/myApp/HaruDanim/Widgets/ActionRunWidget.swift b/myApp/HaruDanim/Widgets/ActionRunWidget.swift index 5e938f0..50f2cca 100644 --- a/myApp/HaruDanim/Widgets/ActionRunWidget.swift +++ b/myApp/HaruDanim/Widgets/ActionRunWidget.swift @@ -196,7 +196,7 @@ struct ActionRunCellView: View { private var tileContent: some View { VStack(alignment: .leading, spacing: 4) { HStack { - Image(systemName: cell.symbolName) + Image(safeSymbol: cell.symbolName) .font(.system(size: 18, weight: .semibold)) Spacer() runningDot @@ -219,7 +219,7 @@ struct ActionRunCellView: View { /// 가로형 (중형 2×2 / 대형 작은 셀 8개) private var rowContent: some View { HStack(spacing: 8) { - Image(systemName: cell.symbolName) + Image(safeSymbol: cell.symbolName) .font(.system(size: 15, weight: .semibold)) VStack(alignment: .leading, spacing: 1) { Text(cell.name) diff --git a/myApp/HaruDanim/Widgets/GoalWidgets.swift b/myApp/HaruDanim/Widgets/GoalWidgets.swift index 69d787e..1a1ef7a 100644 --- a/myApp/HaruDanim/Widgets/GoalWidgets.swift +++ b/myApp/HaruDanim/Widgets/GoalWidgets.swift @@ -139,7 +139,7 @@ struct GoalBarsCellView: View { var body: some View { VStack(alignment: .leading, spacing: 5) { HStack(spacing: 5) { - Image(systemName: goal.symbolName) + Image(safeSymbol: goal.symbolName) .font(.system(size: 11, weight: .semibold)) .foregroundStyle(goal.color) Text(goal.title) @@ -173,7 +173,7 @@ struct QuestBarRow: View { var body: some View { HStack(spacing: 6) { - Image(systemName: quest.symbolName) + Image(safeSymbol: quest.symbolName) .font(.system(size: 10, weight: .semibold)) .foregroundStyle(quest.color) .frame(width: 14) @@ -596,7 +596,7 @@ struct QuestStatusWidgetView: View { private func goalSection(_ goal: GoalSnapshot, maxCount: Int, columns: Int, ringSize: CGFloat) -> some View { VStack(spacing: 10) { HStack(spacing: 4) { - Image(systemName: goal.symbolName) + Image(safeSymbol: goal.symbolName) .font(.system(size: 10, weight: .semibold)) .foregroundStyle(goal.color) Text(goal.title) diff --git a/myApp/HaruDanim/Widgets/HaruDanimWidgets.swift b/myApp/HaruDanim/Widgets/HaruDanimWidgets.swift index b63a8f7..696b75f 100644 --- a/myApp/HaruDanim/Widgets/HaruDanimWidgets.swift +++ b/myApp/HaruDanim/Widgets/HaruDanimWidgets.swift @@ -33,7 +33,7 @@ struct TrackingLiveActivity: Widget { DynamicIsland { DynamicIslandExpandedRegion(.leading) { HStack(spacing: 8) { - Image(systemName: context.state.symbolName) + Image(safeSymbol: context.state.symbolName) .font(.system(size: 16, weight: .semibold)) .foregroundStyle(.white) .frame(width: 32, height: 32) @@ -65,7 +65,7 @@ struct TrackingLiveActivity: Widget { } } } compactLeading: { - Image(systemName: context.state.symbolName) + Image(safeSymbol: context.state.symbolName) .foregroundStyle(context.state.tintColor) } compactTrailing: { HStack(spacing: 2) { @@ -80,7 +80,7 @@ struct TrackingLiveActivity: Widget { } } } minimal: { - Image(systemName: context.state.symbolName) + Image(safeSymbol: context.state.symbolName) .foregroundStyle(context.state.tintColor) } } diff --git a/myApp/HaruDanim/Widgets/LockScreenWidgets.swift b/myApp/HaruDanim/Widgets/LockScreenWidgets.swift index 59a3f90..2f7d1b8 100644 --- a/myApp/HaruDanim/Widgets/LockScreenWidgets.swift +++ b/myApp/HaruDanim/Widgets/LockScreenWidgets.swift @@ -154,7 +154,7 @@ struct LockGoalWidgetView: View { switch entry.style { case .gauge: Gauge(value: min(max(entry.ratio, 0), 1)) { - Image(systemName: goal.symbolName) + Image(safeSymbol: goal.symbolName) .widgetAccentable() } currentValueLabel: { Text("\(Int((entry.ratio * 100).rounded()))") @@ -180,7 +180,7 @@ struct LockGoalWidgetView: View { ZStack { AccessoryWidgetBackground() VStack(spacing: 3) { - Image(systemName: goal.symbolName) + Image(safeSymbol: goal.symbolName) .font(.system(size: 12, weight: .semibold)) .widgetAccentable() questDots(goal, size: 6) @@ -192,7 +192,7 @@ struct LockGoalWidgetView: View { private func rectangularView(_ goal: GoalSnapshot) -> some View { VStack(alignment: .leading, spacing: 2) { HStack(spacing: 4) { - Image(systemName: goal.symbolName) + Image(safeSymbol: goal.symbolName) .font(.system(size: 11, weight: .semibold)) Text(goal.title) .font(.caption.weight(.semibold)) diff --git a/myApp/HaruDanim/Widgets/NowRunningWidget.swift b/myApp/HaruDanim/Widgets/NowRunningWidget.swift index 6197557..04f2401 100644 --- a/myApp/HaruDanim/Widgets/NowRunningWidget.swift +++ b/myApp/HaruDanim/Widgets/NowRunningWidget.swift @@ -234,7 +234,7 @@ struct NowRunningSmallView: View { var body: some View { VStack(alignment: .leading, spacing: 3) { HStack { - Image(systemName: primary.symbolName) + Image(safeSymbol: primary.symbolName) .font(.system(size: 18, weight: .semibold)) Spacer() Image(systemName: "record.circle") @@ -285,7 +285,7 @@ struct NowRunningHeroView: View { var body: some View { HStack(spacing: 12) { - Image(systemName: primary.symbolName) + Image(safeSymbol: primary.symbolName) .font(.system(size: 22, weight: .semibold)) .frame(width: 46, height: 46) .background( @@ -404,7 +404,7 @@ struct NowRunningRowView: View { var body: some View { HStack(spacing: 10) { - Image(systemName: item.symbolName) + Image(safeSymbol: item.symbolName) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(isFullColor ? AnyShapeStyle(.white) : AnyShapeStyle(.primary)) .frame(width: 30, height: 30) @@ -458,7 +458,7 @@ struct LockScreenActivityView: View { var body: some View { HStack(spacing: 12) { - Image(systemName: state.symbolName) + Image(safeSymbol: state.symbolName) .font(.system(size: 18, weight: .semibold)) .foregroundStyle(.white) .frame(width: 40, height: 40) diff --git a/myApp/HaruDanim/Widgets/WidgetSupport.swift b/myApp/HaruDanim/Widgets/WidgetSupport.swift index d4e8a7b..480a415 100644 --- a/myApp/HaruDanim/Widgets/WidgetSupport.swift +++ b/myApp/HaruDanim/Widgets/WidgetSupport.swift @@ -248,7 +248,7 @@ struct WidgetEmptyView: View { var body: some View { VStack(spacing: 6) { - Image(systemName: symbolName) + Image(safeSymbol: symbolName) .font(.title3) .foregroundStyle(AppTheme.green) Text(message) @@ -297,7 +297,7 @@ struct QuestRingView: View { style: StrokeStyle(lineWidth: lineWidth, lineCap: .round) ) .rotationEffect(.degrees(-90)) - Image(systemName: snapshot.symbolName) + Image(safeSymbol: snapshot.symbolName) .font(.system(size: iconSize, weight: .semibold)) .foregroundStyle(snapshot.color) }