Skip to content

previewDependencies api - #463

Closed
pschuette22 wants to merge 12 commits into
pointfreeco:mainfrom
pschuette22:preview-ergonomics
Closed

pschuette22 wants to merge 12 commits into
pointfreeco:mainfrom
pschuette22:preview-ergonomics

Conversation

@pschuette22

@pschuette22 pschuette22 commented Aug 8, 2026

Copy link
Copy Markdown

Adds a previewDependencies convenience api. It accomplishes two main things

  1. Allows for avoiding swiftlint rule violations (force_try and redundant_discardable_let)
  2. Displays errors in preview instead of crashing
Before After
#Preview {
  let _ = prepareDependencies {
    $0.defaultDatabase = try! DatabaseQueue(/*...*/)
  }

  FeatureView()
}
#Preview {
  previewDependencies {
    $0.defaultDatabase = try DatabaseQueue(/*...*/)
  }

  FeatureView()
}

Error display can be customized, if needed, with the errorView ViewBuilder argument

#Preview {
  previewDependencies {
    $0.defaultDatabase = try DatabaseQueue(/* ... */)
  } errorView: { error in
    Text("Failed to prepare preview: \(error)")
  }

  FeatureView()
}
Happy path With error
Preview with prepared dependencies Preview showing the thrown error

By default, the PreviewErrorView just displays a scrollable view with the errors localized description. Open to suggestions on ways to make this more useful out of box.

@pschuette22

Copy link
Copy Markdown
Author

I also considered making the api take a preview content closure at the to display either the error or the preview. Open to either or adding both. Curious what y'all think.

We've found this to be a nice little helper for our previews. Once thing that's interesting is the dependencies bleed into the next preview unless they are updated. Not the end of the world, just a callout

@pschuette22

pschuette22 commented Aug 17, 2026

Copy link
Copy Markdown
Author

Why not preview traits?

Preview traits are applied eagerly across all preview instances as they inflate the preview macro. They are not re-evaluated in a deterministic way which can lead to inconsistencies in dependency preparation.

#Preview(
     "with modifier trait", 
     traits: .dependencies { print("PREPARE") }
) {
    MyView() 
}

expands to:

@available(iOS 17.0, macOS 14.0, tvOS 17.0, visionOS 1.0, watchOS 10.0, *)
struct $s1TPreviewRegistryfMu_: DeveloperToolsSupport.PreviewRegistry {
    static var fileID: String { "T/T.swift" }
    static var line: Int { 19 }
    static var column: Int { 1 }

    static func makePreview() throws -> DeveloperToolsSupport.Preview {
        DeveloperToolsSupport.Preview("with modifier trait", traits: .dependencies {
                print("PREPARE")
            }) {
            func __b_buildView(@SwiftUI.ViewBuilder body: () -> any SwiftUI.View) -> any SwiftUI.View {
                body()
            }
            return __b_buildView {
              MyView()
            }
        }
    }
}

which is then handed off to xcode for rendering. Trait invocation is not tied to layout so switching between previews without changing the underlying view file may cause dependencies to pass the preview boundary via static dependency containers.

Why not PrepareDependencies view?

Similar to above, prepareDependencies should be called when buildView is invoked. It should be decoupled from view diffing logic and idempotent.

By keeping as a plain @ViewBuilder function, prepareDependencies is kept at the top of the call stack and ordering is deterministic relative to Previewed views initalizer. If moved to a PrepareDependencies view body, this function would be called at render time and after view struct init causing injected dependencies / default property values to be resolved before prepareDependencies is called. Adding to view init has similar limitations with default value properties.

Xcode invalidates the entire preview when the previewed file changes and the new PreparationSite is used to avoid redundant prepare calls through state changes.

This does introduce a minor inefficiency where changes to the View file will trigger a redraw across all preview instances (when multiple are defined in a file) that will largely be thrown away, but selecting a preview from within the Xcode canvas will ensure view dependencies are up to date.

@pschuette22 pschuette22 reopened this Aug 17, 2026
@pschuette22
pschuette22 marked this pull request as draft August 17, 2026 20:14
@pschuette22
pschuette22 marked this pull request as ready for review August 17, 2026 20:20
@pschuette22 pschuette22 changed the title preparePreviewDependencies api previewDependencies api Aug 17, 2026
@pschuette22
pschuette22 marked this pull request as draft August 17, 2026 21:27
Comment on lines +101 to +113
let site = PreparationSite(fileID: "\(fileID)", line: line, column: column)
guard
lastPreparationSite.withValue({ lastSite -> Bool in
guard lastSite != site else { return false }
lastSite = site
return true
})
else {
// Short circuit to avoid resetting previews across state change
return nil
}

DependencyValues._current.cachedValues.resetPreviewCache()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical bit for intelligently resetting dependency caches across preview instances

@pschuette22

Copy link
Copy Markdown
Author

Preview traits re-rendering concern turns out to be non-true and debunked in #465

Closing this PR in favor of that

@pschuette22
pschuette22 deleted the preview-ergonomics branch August 18, 2026 00:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant