Skip to content

Commit

Permalink
Don't cache preview app entry point dependencies (#321)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
stephencelis authored Dec 26, 2024
1 parent 4a12189 commit 85f89f5
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 0 deletions.
7 changes: 7 additions & 0 deletions Sources/Dependencies/DependencyValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
30 changes: 30 additions & 0 deletions Sources/Dependencies/Internal/AppEntryPoint.swift
Original file line number Diff line number Diff line change
@@ -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())
}
}

0 comments on commit 85f89f5

Please sign in to comment.