import SwiftUI struct WhatsNewFeature { let icon: String let color: Color let title: String let description: String } struct WhatsNewRelease: Identifiable { let version: String let features: [WhatsNewFeature] var id: String { version } } // MARK: - Versioned catalog // // One entry per released version, newest first. The sheet shows the CUMULATIVE // diff between the version the user is coming from (lastSeenWhatsNewVersion) // and the one they just installed — jumping 1.3.x → 1.4.2 shows 1.4.0 + 1.4.1 // + 1.4.2, not just the last step. ADD AN ENTRY HERE WITH EVERY RELEASE. enum WhatsNewCatalog { static var releases: [WhatsNewRelease] { [ WhatsNewRelease(version: "1.5.0", features: [ WhatsNewFeature( icon: "sparkles", color: .appPrimary, title: String(localized: "whats_new_150_redesign_title"), description: String(localized: "whats_new_150_redesign_body") ), WhatsNewFeature( icon: "figure.mind.and.body", color: .positiveGreen, title: String(localized: "whats_new_150_guided_title"), description: String(localized: "whats_new_150_guided_body") ), WhatsNewFeature( icon: "circle.dashed.inset.filled", color: .orange, title: String(localized: "whats_new_150_rings_title"), description: String(localized: "whats_new_150_rings_body") ) ]), WhatsNewRelease(version: "1.4.2", features: [ WhatsNewFeature( icon: "text.viewfinder", color: .appPrimary, title: String(localized: "whats_new_142_ocr_title"), description: String(localized: "whats_new_142_ocr_body") ), WhatsNewFeature( icon: "mic.fill", color: .purple, title: String(localized: "whats_new_142_siri_title"), description: String(localized: "whats_new_142_siri_body") ), WhatsNewFeature( icon: "chart.xyaxis.line", color: .appSecondary, title: String(localized: "whats_new_142_charts_title"), description: String(localized: "whats_new_142_charts_body") ) ]), WhatsNewRelease(version: "1.4.1", features: [ WhatsNewFeature( icon: "square.and.arrow.up.circle.fill", color: .appPrimary, title: String(localized: "whats_new_141_extension_title"), description: String(localized: "whats_new_141_extension_body") ), WhatsNewFeature( icon: "ipad.landscape", color: .appSecondary, title: String(localized: "whats_new_141_ipad_title"), description: String(localized: "whats_new_141_ipad_body") ), WhatsNewFeature( icon: "icloud.fill", color: .cyan, title: String(localized: "whats_new_141_icloud_title"), description: String(localized: "whats_new_141_icloud_body") ) ]), WhatsNewRelease(version: "1.4.0", features: [ WhatsNewFeature( icon: "bolt.circle.fill", color: .appPrimary, title: String(localized: "whats_new_quick_update_title"), description: String(localized: "whats_new_quick_update_body") ), WhatsNewFeature( icon: "flame.fill", color: .orange, title: String(localized: "whats_new_streak_title"), description: String(localized: "whats_new_streak_body") ), WhatsNewFeature( icon: "target", color: .appSecondary, title: String(localized: "whats_new_goals_title"), description: String(localized: "whats_new_goals_body") ) ]) ] } /// Releases the user hasn't seen: newer than `since`, no newer than `current`. /// Newest first. `since` empty (pre-1.4.0 installs, before the key existed) /// is treated as "show everything in the catalog". static func pendingReleases(since: String, current: String) -> [WhatsNewRelease] { releases.filter { release in (since.isEmpty || compare(release.version, isNewerThan: since)) && !compare(release.version, isNewerThan: current) } } /// Numeric semver-style comparison ("1.10.0" > "1.9.0"). static func compare(_ a: String, isNewerThan b: String) -> Bool { let aParts = a.split(separator: ".").map { Int($0) ?? 0 } let bParts = b.split(separator: ".").map { Int($0) ?? 0 } for i in 0.. y } } return false } } // MARK: - Sheet struct WhatsNewView: View { let releases: [WhatsNewRelease] @Environment(\.dismiss) private var dismiss /// Version headers only add value when the user skipped several releases. private var showsVersionHeaders: Bool { releases.count > 1 } var body: some View { ZStack { AppBackground() VStack(spacing: 0) { VStack(spacing: 8) { Text("🎉") .font(.system(size: 48)) Text(String(format: String(localized: "whats_new_title_format"), releases.first?.version ?? "")) .font(.title.weight(.bold)) Text(String(localized: "whats_new_subtitle")) .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) } .padding(.top, 32) .padding(.horizontal) ScrollView { VStack(spacing: 16) { ForEach(releases) { release in VStack(alignment: .leading, spacing: 14) { if showsVersionHeaders { Text(String(format: String(localized: "whats_new_version_header"), release.version)) .font(.caption.weight(.bold)) .foregroundColor(.appPrimary) .padding(.horizontal, 10) .padding(.vertical, 4) .background(Color.appPrimary.opacity(0.12)) .clipShape(Capsule()) } ForEach(release.features, id: \.title) { feature in HStack(spacing: 16) { Image(systemName: feature.icon) .font(.title2) .foregroundColor(feature.color) .frame(width: 40) VStack(alignment: .leading, spacing: 2) { Text(feature.title) .font(.subheadline.weight(.semibold)) Text(feature.description) .font(.caption) .foregroundColor(.secondary) } Spacer() } } } .padding() .frame(maxWidth: .infinity, alignment: .leading) .background(Color(.secondarySystemGroupedBackground)) .cornerRadius(16) } } .padding() } Button { dismiss() } label: { Text(String(localized: "whats_new_continue")) .font(.headline) .foregroundColor(.white) .frame(maxWidth: .infinity) .padding() .background(Color.appPrimary) .cornerRadius(14) } .padding() } } .interactiveDismissDisabled() } }