All skills

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.

Use this Skill: https://skilld.dev/gh/rshankras/claude-code-apple-skills/whats-new

This session only. Nothing lands on disk.

templates.md

≈4.9k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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)
// #endif

WhatsNewRelease.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
                ),
            ]
        )
    )
}
#endif

WhatsNewSheet.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

Source: SKILL.md on GitHub

1 warning2mo3 checks · Risk SAFE
  • Gen Agent Trust Hub2mo

    This skill provides a developer utility for generating 'What's New' screens in iOS and macOS applications. It includes standard Swift templates and instructions for project analysis to ensure compatibility. The functionality is consistent with its stated purpose and follows mobile development best practices.

  • Socket2mo

    No alerts

  • Snyk2mo

    Risk: MEDIUM · 1 issue

Signed by skilld at 32566aa. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 3 months ago
What it can do
Reads files Edits files Runs commands
last_verified
2026-07-16
review_by
2027-06-22
os_version
iOS 27 / macOS 27
All 7 allowed tools
ReadWriteEditGlobGrepBashAskUserQuestion

README badge

README badge for rshankras/claude-code-apple-skills/whats-new