
Whats New
- 3 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
Generates a What's New / changelog screen shown after app updates with version tracking, feature highlights, and one-time display per version.
About
Generates a What's New screen that displays after app updates with feature highlights and version tracking so it shows only once per version. A developer uses it to add release-notes UI or feature announcements to an app.
- Feature highlights with version tracking
- One-time display per update
Whats New by the numbers
- 3 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #887 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill whats-newAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
What it does
Generates a What's New / changelog screen shown after app updates with version tracking, feature highlights, and one-time display per version.
Files
What's New Generator
Generate a complete "What's New" screen that displays after app updates — highlights new features, changes, and improvements with version tracking to ensure it only shows once per update.
When This Skill Activates
Use this skill when the user:
- Asks to "add a what's new screen" or "show what's new"
- Mentions "changelog" or "changelog UI"
- Wants an "app update screen" or "update notification"
- Asks about "new features screen" or "feature announcements"
- Mentions "release notes UI" or "release notes screen"
- Wants a "version update notification" or "post-update screen"
Pre-Generation Checks
1. Project Context Detection
- [ ] Check deployment target (iOS 16+ / macOS 13+ minimum; iOS 17+ / macOS 14+ for @Observable)
- [ ] Check Swift version (requires Swift 5.9+)
- [ ] Identify source file locations and project structure
- [ ] Determine how app version is accessed (
Bundle.main.infoDictionary, custom build config, etc.)
2. Conflict Detection
Search for existing what's new or changelog implementations:
Glob: **/*WhatsNew*.swift, **/*Changelog*.swift, **/*ReleaseNotes*.swift, **/*VersionTracker*.swift
Grep: "WhatsNew" or "whatsNew" or "lastShownVersion" or "changelog"If found, ask user:
- Replace existing implementation?
- Keep existing, integrate alongside?
3. Version Access Pattern
Detect how the app reads its version:
Grep: "CFBundleShortVersionString" or "Bundle.main.infoDictionary" or "appVersion"Use whichever pattern the project already employs. If none found, default to Bundle.main.infoDictionary?["CFBundleShortVersionString"].
Configuration Questions
Ask user via AskUserQuestion:
1. Presentation style?
- Sheet (recommended —
.sheet(item:)auto-dismiss) - Full-screen cover (
.fullScreenCover) - Inline (embedded in a view hierarchy)
2. Content source?
- Hardcoded in Swift (simplest, no network needed)
- Local JSON file bundled in app
- Remote JSON endpoint (fetched on launch)
3. Dismiss behavior?
- "Continue" button (explicit acknowledgment)
- Swipe to dismiss (sheet default)
- Both (button + swipe)
4. Page indicators?
- Dot indicators (TabView page style)
- Page count label ("1 of 3")
- None (single scrollable view)
Generation Process
Step 1: Read Templates
Read templates.md for production Swift code.
Step 2: Create Core Files
Generate these files: 1. WhatsNewFeature.swift — Model for a single feature (title, description, SF Symbol, tint color) 2. WhatsNewRelease.swift — Groups features by version string with date 3. VersionTracker.swift — Tracks last-shown version in UserDefaults, compares against current bundle version 4. WhatsNewProvider.swift — Protocol + local implementation (optional remote)
Step 3: Create UI Files
5. WhatsNewView.swift — Paged view with TabView(.page) showing features 6. WhatsNewSheet.swift — Wrapper that auto-presents via .sheet(item:) when new version detected
Step 4: Create Optional Files
Based on configuration:
RemoteWhatsNewProvider.swift— If remote content source selectedWhatsNewJSON.swift— If local JSON source selected
Step 5: Determine File Location
Check project structure:
- If
Sources/exists ->Sources/WhatsNew/ - If
App/exists ->App/WhatsNew/ - Otherwise ->
WhatsNew/
Output Format
After generation, provide:
Files Created
WhatsNew/
├── WhatsNewFeature.swift # Single feature model
├── WhatsNewRelease.swift # Version-grouped features
├── VersionTracker.swift # Version persistence & comparison
├── WhatsNewProvider.swift # Content provider protocol + local impl
├── WhatsNewView.swift # Paged feature display
├── WhatsNewSheet.swift # Auto-presenting sheet wrapper
└── RemoteWhatsNewProvider.swift # Remote content (optional)Integration Steps
Option 1: View Modifier Style (Recommended)
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.whatsNewSheet() // Automatically shows after updates
}
}
}Option 2: Manual Control in Root View
@main
struct MyApp: App {
@State private var whatsNewRelease: WhatsNewRelease?
var body: some Scene {
WindowGroup {
ContentView()
.sheet(item: $whatsNewRelease) { release in
WhatsNewView(release: release)
}
.task {
let tracker = VersionTracker()
if tracker.shouldShowWhatsNew() {
whatsNewRelease = WhatsNewProvider.local.latestRelease()
}
}
}
}
}Option 3: Inline Embedding
struct HomeView: View {
@State private var tracker = VersionTracker()
var body: some View {
VStack {
if tracker.shouldShowWhatsNew(),
let release = WhatsNewProvider.local.latestRelease() {
WhatsNewView(release: release) {
tracker.markVersionAsShown()
}
}
// Rest of home content...
}
}
}Adding New Releases
// In LocalWhatsNewProvider.swift — add a new entry per release
static let releases: [WhatsNewRelease] = [
WhatsNewRelease(
version: "2.1.0",
date: Date(timeIntervalSince1970: 1_700_000_000),
features: [
WhatsNewFeature(
title: "Dark Mode Support",
description: "Full dark mode across all screens.",
systemImage: "moon.fill",
tintColor: .indigo
),
WhatsNewFeature(
title: "Faster Search",
description: "Search results now appear instantly.",
systemImage: "magnifyingglass",
tintColor: .orange
),
]
),
// Previous releases...
]Testing
@Test
func showsWhatsNewForNewVersion() {
let defaults = UserDefaults(suiteName: "TestWhatsNew")!
defaults.removePersistentDomain(forName: "TestWhatsNew")
let tracker = VersionTracker(
defaults: defaults,
currentVersion: "2.0.0"
)
// First launch — no previous version stored
#expect(tracker.shouldShowWhatsNew() == true)
tracker.markVersionAsShown()
#expect(tracker.shouldShowWhatsNew() == false)
}
@Test
func skipsWhatsNewWhenVersionUnchanged() {
let defaults = UserDefaults(suiteName: "TestWhatsNew2")!
defaults.removePersistentDomain(forName: "TestWhatsNew2")
let tracker = VersionTracker(
defaults: defaults,
currentVersion: "1.5.0"
)
tracker.markVersionAsShown()
// Same version — should not show
let tracker2 = VersionTracker(
defaults: defaults,
currentVersion: "1.5.0"
)
#expect(tracker2.shouldShowWhatsNew() == false)
}
@Test
func showsWhatsNewAfterUpdate() {
let defaults = UserDefaults(suiteName: "TestWhatsNew3")!
defaults.removePersistentDomain(forName: "TestWhatsNew3")
let tracker = VersionTracker(
defaults: defaults,
currentVersion: "1.0.0"
)
tracker.markVersionAsShown()
// Simulate update
let trackerAfterUpdate = VersionTracker(
defaults: defaults,
currentVersion: "2.0.0"
)
#expect(trackerAfterUpdate.shouldShowWhatsNew() == true)
}Common Patterns
Define Features Per Version
Group features by release version so older users who skipped updates still see relevant changes:
// Show features for all versions newer than lastShownVersion
func featuresSinceLastShown() -> [WhatsNewFeature] {
let lastShown = tracker.lastShownVersion ?? "0.0.0"
return releases
.filter { $0.version.compare(lastShown, options: .numeric) == .orderedDescending }
.flatMap(\.features)
}Conditional Display
Only show if there are actually features to display for the current version:
if let release = provider.release(for: currentVersion),
tracker.shouldShowWhatsNew() {
// Present What's New
}Remote Content Loading
Fetch features from a server to update without app releases:
let provider = RemoteWhatsNewProvider(
endpoint: URL(string: "https://api.example.com/whats-new")!,
fallback: LocalWhatsNewProvider() // Offline fallback
)
let release = try await provider.latestRelease()Gotchas
CFBundleShortVersionString vs CFBundleVersion
CFBundleShortVersionString= marketing version (e.g., "2.1.0") — use this for What's NewCFBundleVersion= build number (e.g., "47") — changes every build, NOT suitable for What's New tracking- Always compare the marketing version, never the build number
First Install vs Update Detection
- On first install,
lastShownVersionisnil - Decide whether first-time users should see What's New (usually NO — show onboarding instead)
- If
lastShownVersionisnil, either skip What's New or set the current version without showing:
func shouldShowWhatsNew() -> Bool {
guard let lastShown = lastShownVersion else {
// First install — mark current version, don't show
markVersionAsShown()
return false
}
return currentVersion.compare(lastShown, options: .numeric) == .orderedDescending
}TestFlight vs App Store Versions
- TestFlight builds may have the same marketing version but different build numbers
- If using TestFlight for beta testing, consider including build number in tracking key for testers
- For production, always track by marketing version only
Version String Comparison
- Use
.numericoption inString.compare(_:options:)so "2.0.0" > "1.10.0" (not lexicographic) - Never compare version strings with
<or>operators directly — "9.0" would sort after "10.0" lexicographically
Accessibility
- Ensure all feature images have accessibility labels
- Support Dynamic Type in feature descriptions
- VoiceOver should read features sequentially — use
.accessibilityElement(children: .combine)on feature cards
References
- templates.md — All production Swift templates
- Related:
generators/onboarding-generator— First-launch onboarding (complementary — onboarding for first install, What's New for updates)
What's New Code Templates
Production-ready Swift templates for a "What's New" screen with version tracking. All code targets iOS 16+ / macOS 13+ (iOS 17+ / macOS 14+ for @Observable) and uses modern Swift concurrency.
WhatsNewFeature.swift
import SwiftUI
/// A single feature to highlight in the What's New screen.
///
/// Each feature has a title, description, SF Symbol icon, and tint color.
///
/// Usage:
/// ```swift
/// let feature = WhatsNewFeature(
/// title: "Dark Mode",
/// description: "Full dark mode support across all screens.",
/// systemImage: "moon.fill",
/// tintColor: .indigo
/// )
/// ```
struct WhatsNewFeature: Identifiable, Codable, Sendable {
var id: String { title }
let title: String
let description: String
let systemImage: String
let tintColor: CodableColor
init(title: String, description: String, systemImage: String, tintColor: Color) {
self.title = title
self.description = description
self.systemImage = systemImage
self.tintColor = CodableColor(color: tintColor)
}
}
/// A Codable wrapper for SwiftUI Color.
///
/// Stores color as RGB components for JSON serialization.
struct CodableColor: Codable, Sendable {
let red: Double
let green: Double
let blue: Double
let opacity: Double
init(color: Color) {
// Default to blue if color resolution fails
let resolved = UIColor(color)
var r: CGFloat = 0
var g: CGFloat = 0
var b: CGFloat = 0
var a: CGFloat = 0
resolved.getRed(&r, green: &g, blue: &b, alpha: &a)
self.red = Double(r)
self.green = Double(g)
self.blue = Double(b)
self.opacity = Double(a)
}
var color: Color {
Color(red: red, green: green, blue: blue, opacity: opacity)
}
}
// For macOS, replace UIColor with NSColor:
// #if canImport(UIKit)
// let resolved = UIColor(color)
// #elseif canImport(AppKit)
// let resolved = NSColor(color)
// #endifWhatsNewRelease.swift
import Foundation
/// Groups features for a specific app version.
///
/// Each release corresponds to one app update and contains
/// the features introduced in that version.
///
/// Usage:
/// ```swift
/// let release = WhatsNewRelease(
/// version: "2.1.0",
/// date: .now,
/// features: [feature1, feature2, feature3]
/// )
/// ```
struct WhatsNewRelease: Identifiable, Codable, Sendable {
var id: String { version }
/// Marketing version string (e.g., "2.1.0").
/// Must match CFBundleShortVersionString.
let version: String
/// Release date for display purposes.
let date: Date
/// Features introduced in this version.
let features: [WhatsNewFeature]
}VersionTracker.swift
import Foundation
/// Tracks which app version the user last saw What's New for.
///
/// Compares the stored version against the current bundle version
/// to determine whether the What's New screen should be displayed.
///
/// - Important: Uses `CFBundleShortVersionString` (marketing version),
/// NOT `CFBundleVersion` (build number).
///
/// Usage:
/// ```swift
/// let tracker = VersionTracker()
/// if tracker.shouldShowWhatsNew() {
/// // Present What's New screen
/// tracker.markVersionAsShown()
/// }
/// ```
final class VersionTracker: Sendable {
private let defaults: UserDefaults
private let currentVersion: String
private let storageKey: String
/// Creates a version tracker.
///
/// - Parameters:
/// - defaults: UserDefaults instance (injectable for testing).
/// - currentVersion: Override the bundle version (injectable for testing).
/// - storageKey: UserDefaults key for persistence.
init(
defaults: UserDefaults = .standard,
currentVersion: String? = nil,
storageKey: String = "whatsNew.lastShownVersion"
) {
self.defaults = defaults
self.currentVersion = currentVersion
?? Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
?? "1.0.0"
self.storageKey = storageKey
}
/// The last version for which What's New was shown, or `nil` on first install.
var lastShownVersion: String? {
defaults.string(forKey: storageKey)
}
/// Whether the What's New screen should be displayed.
///
/// Returns `false` on first install (no previous version stored)
/// to avoid showing What's New to brand new users. Sets the current
/// version silently so it shows on the *next* update.
func shouldShowWhatsNew() -> Bool {
guard let lastShown = lastShownVersion else {
// First install — mark current version, show onboarding instead
markVersionAsShown()
return false
}
return currentVersion.compare(lastShown, options: .numeric) == .orderedDescending
}
/// Marks the current version as shown so What's New won't appear again
/// until the next update.
func markVersionAsShown() {
defaults.set(currentVersion, forKey: storageKey)
}
/// Resets tracking — useful for debugging and testing.
func reset() {
defaults.removeObject(forKey: storageKey)
}
}WhatsNewProvider.swift
import Foundation
/// Protocol for providing What's New content.
///
/// Conform to this protocol to supply features from different sources
/// (hardcoded, local JSON, remote endpoint).
protocol WhatsNewProviding: Sendable {
/// Returns all known releases.
func allReleases() async throws -> [WhatsNewRelease]
/// Returns the release matching a specific version, if available.
func release(for version: String) async throws -> WhatsNewRelease?
/// Returns the most recent release.
func latestRelease() async throws -> WhatsNewRelease?
/// Returns all features from versions newer than the given version.
func featuresSince(version: String) async throws -> [WhatsNewFeature]
}
/// Default implementations for common queries.
extension WhatsNewProviding {
func release(for version: String) async throws -> WhatsNewRelease? {
try await allReleases().first { $0.version == version }
}
func latestRelease() async throws -> WhatsNewRelease? {
try await allReleases()
.sorted { $0.version.compare($1.version, options: .numeric) == .orderedDescending }
.first
}
func featuresSince(version: String) async throws -> [WhatsNewFeature] {
try await allReleases()
.filter { $0.version.compare(version, options: .numeric) == .orderedDescending }
.sorted { $0.version.compare($1.version, options: .numeric) == .orderedAscending }
.flatMap(\.features)
}
}
/// Provides What's New content from hardcoded Swift data.
///
/// This is the simplest approach — define releases directly in code.
/// No network required, no file parsing, always available.
///
/// Usage:
/// ```swift
/// let provider = LocalWhatsNewProvider()
/// let latest = try await provider.latestRelease()
/// ```
struct LocalWhatsNewProvider: WhatsNewProviding {
/// Define your releases here, newest first.
///
/// Add a new entry at the top each time you release an update.
static let releases: [WhatsNewRelease] = [
// --- Add new releases at the top ---
WhatsNewRelease(
version: "2.1.0",
date: Date(timeIntervalSince1970: 1_700_000_000),
features: [
WhatsNewFeature(
title: "Dark Mode",
description: "Full dark mode support across every screen.",
systemImage: "moon.fill",
tintColor: .indigo
),
WhatsNewFeature(
title: "Faster Search",
description: "Search results now appear instantly as you type.",
systemImage: "magnifyingglass",
tintColor: .orange
),
WhatsNewFeature(
title: "Widget Support",
description: "Add home screen widgets for quick access.",
systemImage: "rectangle.on.rectangle",
tintColor: .green
),
]
),
WhatsNewRelease(
version: "2.0.0",
date: Date(timeIntervalSince1970: 1_695_000_000),
features: [
WhatsNewFeature(
title: "Redesigned Interface",
description: "A fresh new look with improved navigation.",
systemImage: "sparkles",
tintColor: .blue
),
WhatsNewFeature(
title: "iCloud Sync",
description: "Your data now syncs seamlessly across all devices.",
systemImage: "icloud.fill",
tintColor: .cyan
),
]
),
]
func allReleases() async throws -> [WhatsNewRelease] {
Self.releases
}
}RemoteWhatsNewProvider.swift (Optional)
import Foundation
/// Fetches What's New content from a remote JSON endpoint.
///
/// Falls back to a local provider if the network request fails.
///
/// Expected JSON format:
/// ```json
/// {
/// "releases": [
/// {
/// "version": "2.1.0",
/// "date": "2024-11-14T00:00:00Z",
/// "features": [
/// {
/// "title": "Dark Mode",
/// "description": "Full dark mode support.",
/// "systemImage": "moon.fill",
/// "tintColor": { "red": 0.29, "green": 0.27, "blue": 0.62, "opacity": 1.0 }
/// }
/// ]
/// }
/// ]
/// }
/// ```
actor RemoteWhatsNewProvider: WhatsNewProviding {
private let endpoint: URL
private let session: URLSession
private let fallback: any WhatsNewProviding
private let cacheExpiration: TimeInterval
private var cachedReleases: [WhatsNewRelease]?
private var lastFetchDate: Date?
init(
endpoint: URL,
session: URLSession = .shared,
fallback: any WhatsNewProviding = LocalWhatsNewProvider(),
cacheExpiration: TimeInterval = 3600 // 1 hour
) {
self.endpoint = endpoint
self.session = session
self.fallback = fallback
self.cacheExpiration = cacheExpiration
}
func allReleases() async throws -> [WhatsNewRelease] {
// Return cached data if still fresh
if let cached = cachedReleases,
let lastFetch = lastFetchDate,
Date().timeIntervalSince(lastFetch) < cacheExpiration {
return cached
}
do {
let (data, response) = try await session.data(from: endpoint)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
return try await fallback.allReleases()
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let container = try decoder.decode(ReleasesContainer.self, from: data)
cachedReleases = container.releases
lastFetchDate = Date()
return container.releases
} catch {
// Network failure — use fallback
return try await fallback.allReleases()
}
}
nonisolated func release(for version: String) async throws -> WhatsNewRelease? {
try await allReleases().first { $0.version == version }
}
nonisolated func latestRelease() async throws -> WhatsNewRelease? {
try await allReleases()
.sorted { $0.version.compare($1.version, options: .numeric) == .orderedDescending }
.first
}
nonisolated func featuresSince(version: String) async throws -> [WhatsNewFeature] {
try await allReleases()
.filter { $0.version.compare(version, options: .numeric) == .orderedDescending }
.sorted { $0.version.compare($1.version, options: .numeric) == .orderedAscending }
.flatMap(\.features)
}
}
/// JSON decoding container.
private struct ReleasesContainer: Codable {
let releases: [WhatsNewRelease]
}WhatsNewView.swift
import SwiftUI
/// A paged view displaying new features for a release.
///
/// Shows each feature as a full-page card with an SF Symbol icon,
/// title, and description. Includes a "Continue" button on the last page.
///
/// Usage:
/// ```swift
/// WhatsNewView(release: release) {
/// // Called when user finishes viewing
/// }
/// ```
struct WhatsNewView: View {
let release: WhatsNewRelease
var onDismiss: (() -> Void)?
@State private var currentPage = 0
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack(spacing: 0) {
headerView
TabView(selection: $currentPage) {
ForEach(Array(release.features.enumerated()), id: \.element.id) { index, feature in
FeaturePageView(feature: feature)
.tag(index)
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
bottomBar
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
#if os(iOS)
.background(Color(uiColor: .systemGroupedBackground))
#elseif os(macOS)
.background(Color(nsColor: .windowBackgroundColor))
.frame(minWidth: 480, minHeight: 560)
#endif
}
// MARK: - Header
private var headerView: some View {
VStack(spacing: 4) {
Text("What's New")
.font(.largeTitle.bold())
Text("Version \(release.version)")
.font(.subheadline)
.foregroundStyle(.secondary)
}
.padding(.top, 40)
.padding(.bottom, 16)
}
// MARK: - Bottom Bar
private var bottomBar: some View {
VStack(spacing: 12) {
pageIndicatorLabel
Button(action: handleContinue) {
Text(isLastPage ? "Continue" : "Next")
.font(.headline)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.padding(.horizontal, 24)
}
.padding(.bottom, 32)
}
private var pageIndicatorLabel: some View {
Text("\(currentPage + 1) of \(release.features.count)")
.font(.caption)
.foregroundStyle(.secondary)
}
// MARK: - Logic
private var isLastPage: Bool {
currentPage >= release.features.count - 1
}
private func handleContinue() {
if isLastPage {
onDismiss?()
dismiss()
} else {
withAnimation {
currentPage += 1
}
}
}
}
/// Displays a single feature as a full-page card.
struct FeaturePageView: View {
let feature: WhatsNewFeature
var body: some View {
VStack(spacing: 20) {
Spacer()
Image(systemName: feature.systemImage)
.font(.system(size: 64))
.foregroundStyle(feature.tintColor.color)
.accessibilityHidden(true)
Text(feature.title)
.font(.title2.bold())
.multilineTextAlignment(.center)
Text(feature.description)
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
Spacer()
Spacer()
}
.accessibilityElement(children: .combine)
}
}
#if DEBUG
#Preview {
WhatsNewView(
release: WhatsNewRelease(
version: "2.1.0",
date: .now,
features: [
WhatsNewFeature(
title: "Dark Mode",
description: "Full dark mode support across every screen.",
systemImage: "moon.fill",
tintColor: .indigo
),
WhatsNewFeature(
title: "Faster Search",
description: "Search results now appear instantly as you type.",
systemImage: "magnifyingglass",
tintColor: .orange
),
WhatsNewFeature(
title: "Widgets",
description: "Add home screen widgets for quick access.",
systemImage: "rectangle.on.rectangle",
tintColor: .green
),
]
)
)
}
#endifWhatsNewSheet.swift
import SwiftUI
/// A view modifier that automatically presents the What's New sheet
/// when a new app version is detected.
///
/// Uses `.sheet(item:)` pattern for proper data-driven presentation.
/// Marks the version as shown on dismiss, so it only appears once per update.
///
/// Usage:
/// ```swift
/// ContentView()
/// .whatsNewSheet()
///
/// // With custom provider:
/// ContentView()
/// .whatsNewSheet(provider: myRemoteProvider)
/// ```
struct WhatsNewSheetModifier: ViewModifier {
let provider: any WhatsNewProviding
let tracker: VersionTracker
@State private var releaseToShow: WhatsNewRelease?
init(
provider: any WhatsNewProviding = LocalWhatsNewProvider(),
tracker: VersionTracker = VersionTracker()
) {
self.provider = provider
self.tracker = tracker
}
func body(content: Content) -> some View {
content
.sheet(item: $releaseToShow) { release in
WhatsNewView(release: release) {
tracker.markVersionAsShown()
}
#if os(iOS)
.interactiveDismissDisabled(false)
#endif
}
.task {
await checkForUpdate()
}
}
private func checkForUpdate() async {
guard tracker.shouldShowWhatsNew() else { return }
do {
if let release = try await provider.latestRelease() {
releaseToShow = release
}
} catch {
// Silently fail — don't block the user experience
}
}
}
extension View {
/// Presents a What's New sheet automatically after app updates.
///
/// - Parameters:
/// - provider: Content source for What's New features.
/// - tracker: Version tracker instance (injectable for testing).
/// - Returns: A view that auto-presents What's New when appropriate.
func whatsNewSheet(
provider: any WhatsNewProviding = LocalWhatsNewProvider(),
tracker: VersionTracker = VersionTracker()
) -> some View {
modifier(WhatsNewSheetModifier(provider: provider, tracker: tracker))
}
}
#if DEBUG
/// Preview helper that always shows the sheet.
#Preview {
Text("App Content")
.whatsNewSheet(
tracker: {
let defaults = UserDefaults(suiteName: "PreviewWhatsNew")!
defaults.removePersistentDomain(forName: "PreviewWhatsNew")
// Set a previous version so shouldShowWhatsNew() returns true
defaults.set("1.0.0", forKey: "whatsNew.lastShownVersion")
return VersionTracker(
defaults: defaults,
currentVersion: "2.1.0"
)
}()
)
}
#endif