Agent Skills: What's New Generator

Generates a "What's New" / changelog screen shown after app updates with version tracking, feature highlights, and one-time display per version. Use when user wants release notes UI, update notifications, or feature announcements.

UncategorizedID: rshankras/claude-code-apple-skills/whats-new

Install this agent skill to your local

pnpm dlx add-skill https://github.com/rshankras/claude-code-apple-skills/tree/HEAD/skills/generators/whats-new

Skill Files

Browse the full folder contents for whats-new.

Download Skill

Loading file tree…

skills/generators/whats-new/SKILL.md

Skill Metadata

Name
whats-new
Description
Generates a "What's New" / changelog screen shown after app updates with version tracking, feature highlights, and one-time display per version. Use when user wants release notes UI, update notifications, or feature announcements.

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

  1. WhatsNewView.swift — Paged view with TabView(.page) showing features
  2. 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 selected
  • WhatsNewJSON.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 New
  • CFBundleVersion = 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, lastShownVersion is nil
  • Decide whether first-time users should see What's New (usually NO — show onboarding instead)
  • If lastShownVersion is nil, 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 .numeric option in String.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)