Skip to content

What's New Sheet

A sheet that lists what a new version brings and comes up once for each version.

What's New Sheet, playing
Download .swift
Origin
Inspired by The What's New screens of Apple's apps, written from scratch
Found by
whats new · release notes · update sheet · new features · changelog · version · welcome · novedades · notas de versión · hoja de novedades · nuevas funciones · actualización · versión · bienvenida

Shows WhatsNewSheet the first time the app runs a version newer than the last one it was shown for, and remembers it in UserDefaults when the sheet closes.

The page that tells what a new version brings: a title, a row per feature and a button.

Show it yourself in a sheet, or let whatsNewSheet(version:features:) show it once per version. The rows come in one after another. With Reduce Motion on, they fade in together.

struct WhatsNewSheetDemo: View {
private let storageKey = "WhatsNewSheetDemo.seenVersion"
private let features = [
WhatsNewSheetFeature(symbol: "square.and.pencil", title: "Quick Notes", text: "Start a note from anywhere and find it later in its own folder.", tint: .orange),
WhatsNewSheetFeature(symbol: "tag.fill", title: "Smart Tags", text: "Add a tag as you write and browse every note that shares it.", tint: .teal),
WhatsNewSheetFeature(symbol: "person.2.fill", title: "Shared Folders", text: "Invite people to a folder and see their changes as they make them.", tint: .indigo),
WhatsNewSheetFeature(symbol: "lock.fill", title: "Locked Notes", text: "Keep a note behind Face ID, so only you can open it.", tint: .pink),
]
var body: some View {
VStack(spacing: 16) {
Image(systemName: "note.text")
.font(.system(size: 56))
.foregroundStyle(.tint)
.accessibilityHidden(true)
Text("Notes 2.4")
.font(.title2.bold())
Text("The sheet comes up once for each new version.")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
Button("Show Again", action: forget)
.buttonStyle(.bordered)
.padding(.top, 8)
}
.padding(32)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemGroupedBackground))
.whatsNewSheet(version: "2.4", features: features, storageKey: storageKey)
.onAppear(perform: forget)
}
/// Forgets the version the sheet was shown for, as if the app had just been updated.
private func forget() {
UserDefaults.standard.removeObject(forKey: storageKey)
}
}

whatsNewSheet(version:title:features:storageKey:)

ParameterTypeDefault
versionStringRequired
titleLocalizedStringResource"What’s New"
features[WhatsNewSheetFeature]Required
storageKeyString"WhatsNewSheet.seenVersion"

WhatsNewSheet

ParameterTypeDefault
titleLocalizedStringResource"What’s New"
features[WhatsNewSheetFeature]Required
buttonTitleString"Continue"
onContinue() -> VoidRequired

Copy this code into your project, or download the file.

WhatsNewSheet.swift
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
nonisolated enum WhatsNewSheetLogic {
/// Whether `version` comes after the one that was already shown, comparing number by number,
/// so 2.10 is newer than 2.9 and 2.0 is the same as 2. Nothing shown yet counts as older.
static func isNewer(_ version: String, than seen: String) -> Bool {
let new = numbers(in: version)
let old = numbers(in: seen)
for index in 0..<max(new.count, old.count) {
let left = index < new.count ? new[index] : 0
let right = index < old.count ? old[index] : 0
if left != right { return left > right }
}
return false
}
static func numbers(in version: String) -> [Int] {
version.split(separator: ".").map { Int($0) ?? 0 }
}
/// How long a row waits before it comes in, so they arrive one after another without the
/// last ones of a long list taking too long.
static func delay(for index: Int, step: Double = 0.12, limit: Double = 0.6) -> Double {
min(Double(max(index, 0)) * step, limit)
}
}
struct WhatsNewSheetFeature: Identifiable {
var symbol: String
var title: LocalizedStringResource
var text: LocalizedStringResource
var tint: Color = .accentColor
var id: String { title.key }
}
/// The page that tells what a new version brings: a title, a row per feature and a button.
///
/// Show it yourself in a sheet, or let `whatsNewSheet(version:features:)` show it once per version.
/// The rows come in one after another. With Reduce Motion on, they fade in together.
struct WhatsNewSheet: View {
var title: LocalizedStringResource = "What’s New"
let features: [WhatsNewSheetFeature]
var buttonTitle = "Continue"
var onContinue: () -> Void
private let margin: CGFloat = 32
@State private var hasAppeared = false
@ScaledMetric(relativeTo: .title) private var symbolWidth = 44.0
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
ScrollView {
VStack(spacing: 40) {
Text(title)
.font(.largeTitle.bold())
.multilineTextAlignment(.center)
.accessibilityAddTraits(.isHeader)
VStack(alignment: .leading, spacing: 28) {
ForEach(Array(features.enumerated()), id: \.element.id) { index, feature in
row(feature)
.opacity(hasAppeared ? 1 : 0)
.offset(y: hasAppeared || reduceMotion ? 0 : 18)
.animation(animation(for: index), value: hasAppeared)
}
}
}
.padding(.horizontal, margin)
.padding(.top, 56)
.padding(.bottom, 24)
.frame(maxWidth: 520)
.frame(maxWidth: .infinity)
}
.scrollBounceBehavior(.basedOnSize)
.safeAreaInset(edge: .bottom) {
Button(action: onContinue) {
Text(buttonTitle)
.font(.headline)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.padding(.horizontal, margin)
.padding(.vertical, 12)
.frame(maxWidth: 520)
.frame(maxWidth: .infinity)
.background(.bar)
}
.onAppear { hasAppeared = true }
}
private func row(_ feature: WhatsNewSheetFeature) -> some View {
HStack(alignment: .top, spacing: 16) {
Image(systemName: feature.symbol)
.font(.title)
.foregroundStyle(feature.tint)
.frame(width: symbolWidth)
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 2) {
Text(feature.title)
.font(.headline)
Text(feature.text)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
.accessibilityElement(children: .combine)
}
private func animation(for index: Int) -> Animation {
reduceMotion
? .easeOut(duration: 0.25)
: .spring(duration: 0.5).delay(WhatsNewSheetLogic.delay(for: index))
}
}
struct WhatsNewSheetModifier: ViewModifier {
let version: String
let title: LocalizedStringResource
let features: [WhatsNewSheetFeature]
@AppStorage private var seenVersion: String
@State private var isPresented = false
init(version: String, title: LocalizedStringResource, features: [WhatsNewSheetFeature], storageKey: String) {
self.version = version
self.title = title
self.features = features
_seenVersion = AppStorage(wrappedValue: "", storageKey)
}
func body(content: Content) -> some View {
content
.onChange(of: seenVersion, initial: true) {
if WhatsNewSheetLogic.isNewer(version, than: seenVersion) { isPresented = true }
}
.sheet(isPresented: $isPresented) {
seenVersion = version
} content: {
WhatsNewSheet(title: title, features: features) {
isPresented = false
}
}
}
}
extension View {
/// Shows `WhatsNewSheet` the first time the app runs a `version` newer than the last one it
/// was shown for, and remembers it in `UserDefaults` when the sheet closes.
func whatsNewSheet(version: String, title: LocalizedStringResource = "What’s New", features: [WhatsNewSheetFeature], storageKey: String = "WhatsNewSheet.seenVersion") -> some View {
modifier(WhatsNewSheetModifier(version: version, title: title, features: features, storageKey: storageKey))
}
}
// MARK: - Demo
struct WhatsNewSheetDemo: View {
private let storageKey = "WhatsNewSheetDemo.seenVersion"
private let features = [
WhatsNewSheetFeature(symbol: "square.and.pencil", title: "Quick Notes", text: "Start a note from anywhere and find it later in its own folder.", tint: .orange),
WhatsNewSheetFeature(symbol: "tag.fill", title: "Smart Tags", text: "Add a tag as you write and browse every note that shares it.", tint: .teal),
WhatsNewSheetFeature(symbol: "person.2.fill", title: "Shared Folders", text: "Invite people to a folder and see their changes as they make them.", tint: .indigo),
WhatsNewSheetFeature(symbol: "lock.fill", title: "Locked Notes", text: "Keep a note behind Face ID, so only you can open it.", tint: .pink),
]
var body: some View {
VStack(spacing: 16) {
Image(systemName: "note.text")
.font(.system(size: 56))
.foregroundStyle(.tint)
.accessibilityHidden(true)
Text("Notes 2.4")
.font(.title2.bold())
Text("The sheet comes up once for each new version.")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
Button("Show Again", action: forget)
.buttonStyle(.bordered)
.padding(.top, 8)
}
.padding(32)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemGroupedBackground))
.whatsNewSheet(version: "2.4", features: features, storageKey: storageKey)
.onAppear(perform: forget)
}
/// Forgets the version the sheet was shown for, as if the app had just been updated.
private func forget() {
UserDefaults.standard.removeObject(forKey: storageKey)
}
}
#Preview {
WhatsNewSheetDemo()
}