From 85f89f5d0ce5a18945f65371d40ca997da85a41a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 26 Dec 2024 12:36:53 -0800 Subject: [PATCH] Don't cache preview app entry point dependencies (#321) * Don't cache preview app entry point dependencies We have a longstanding gotcha where we tell folks to avoid instantiating their root model on their app entry point because any dependencies on the root model may negatively affect any preview in their application. This PR detects when a dependency is accessed in a SwiftUI preview app entry point and prevents it from influencing the cache using the call stack symbols available. While this isn't a silver bullet, and any async work kicked off from the app entry point could still affect things negatively, this should hopefully be mostly an improvement on the status quo and maybe we won't have to refer folks to this gotcha as often in the future. * wip * wip * Update AppEntryPoint.swift --- Sources/Dependencies/DependencyValues.swift | 7 +++++ .../Dependencies/Internal/AppEntryPoint.swift | 30 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 Sources/Dependencies/Internal/AppEntryPoint.swift diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index eda32d55..889795c3 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -268,6 +268,10 @@ public struct DependencyValues: Sendable { } set { if DependencyValues.isPreparing { + if context == .preview, Thread.isPreviewAppEntryPoint { + reportIssue("Ignoring dependencies prepared in preview app entry point") + return + } let cacheKey = CachedValues.CacheKey(id: TypeIdentifier(key), context: context) guard !cachedValues.cached.keys.contains(cacheKey) else { if cachedValues.cached[cacheKey]?.preparationID != DependencyValues.preparationID { @@ -547,6 +551,9 @@ public final class CachedValues: @unchecked Sendable { case .live: value = (key as? any DependencyKey.Type)?.liveValue as? Key.Value case .preview: + if Thread.isPreviewAppEntryPoint { + return Key.previewValue + } if !CachedValues.isAccessingCachedDependencies { value = CachedValues.$isAccessingCachedDependencies.withValue(true) { #if canImport(SwiftUI) && compiler(>=6) diff --git a/Sources/Dependencies/Internal/AppEntryPoint.swift b/Sources/Dependencies/Internal/AppEntryPoint.swift new file mode 100644 index 00000000..4b66109b --- /dev/null +++ b/Sources/Dependencies/Internal/AppEntryPoint.swift @@ -0,0 +1,30 @@ +import Foundation + +extension Thread { + public static var isPreviewAppEntryPoint: Bool { + guard ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" + else { return false } + + var isPreviewAppEntryPoint = false + for frame in callStackSymbols.reversed() { + if !isPreviewAppEntryPoint, frame.containsSymbol("$s7SwiftUI3AppPAAE4mainyyFZ") { + isPreviewAppEntryPoint = true + } else if isPreviewAppEntryPoint, + frame.containsSymbol("$s7SwiftUI6runAppys5NeverOxAA0D0RzlF") + { + return false + } + } + return isPreviewAppEntryPoint + } +} + +extension String { + fileprivate func containsSymbol(_ symbol: String) -> Bool { + utf8 + .reversed() + .drop(while: { (48...57).contains($0) }) + .dropFirst(3) + .starts(with: symbol.utf8.reversed()) + } +}