A reusable SwiftUI welcome sheet for iOS and macOS apps in the HK Softworks portfolio.
Pure SwiftUI — the consumer owns state (loading, error, dismissal). Works with any state management approach (TCA, @Observable, @State).
The package and import name is GreetKit. Public view and content APIs intentionally use the
domain language Greet..., for example GreetView, GreetContent, and
GreetPrimaryRoute.
- iOS 26+ / macOS 26+
- Swift 6.2+
No release tags are published yet, so use the master branch for now:
.package(url: "https://github.com/hksw-io/GreetKit.git", branch: "master")Switch to a semantic version requirement after the first release tag exists:
.package(url: "https://github.com/hksw-io/GreetKit.git", from: "1.0.0")Or in Xcode: File > Add Package Dependencies, enter the URL above, and select the master branch until a release tag is available.
Implement GreetContent with your app's strings and icon, then drive the view with bindings and callbacks:
import SwiftUI
import GreetKit
struct MyGreet: GreetContent {
var appIcon: Image? { Image("AppIconImage") }
var title: Text { Text("Welcome to MyApp") }
var subtitle: Text? { Text("Here's what makes it great.") }
var features: [GreetFeatureItem] {
[
GreetFeatureItem(
id: "tap-to-flip",
systemImage: "hand.tap.fill",
label: "Tap to flip",
description: "Review cards with a simple tap."),
]
}
var primaryRoutes: [GreetPrimaryRoute] {
[
GreetPrimaryRoute(id: "permissions"),
GreetPrimaryRoute(id: "sample-data"),
GreetPrimaryRoute(id: "notifications"),
]
}
var primaryButtonText: Text { Text("Get started") }
var primaryRouteNextButtonText: Text { Text("Next") }
var primaryRouteDoneButtonText: Text { Text("Finish") }
var skipButtonText: Text? { Text("Skip for now") }
var errorAlertTitle: Text { Text("Something went wrong") }
var errorOKText: Text { Text("OK") }
}
struct RootView: View {
@State private var isLoading = false
@State private var errorMessage: String?
var body: some View {
GreetView(
content: MyGreet(),
isLoading: $isLoading,
errorMessage: $errorMessage,
onPrimary: { /* analytics or setup before routes open */ },
onSkip: { /* mark first-run flow complete, dismiss */ },
onPrimaryRoutesComplete: {
/* dismiss the welcome flow or mark setup complete */
},
primaryRouteDestination: { route in
switch route.id {
case "permissions":
PermissionsSetupView()
case "sample-data":
SampleDataSetupView()
case "notifications":
NotificationSetupView()
default:
EmptyView()
}
})
}
}For a simple welcome sheet, omit primaryRoutes and primaryRouteDestination. The primary and skip callbacks can then dismiss the sheet directly.
For a chained setup flow, provide primaryRoutes and primaryRouteDestination. onPrimary fires first, then the library opens the first route with an in-sheet transition. Do not dismiss from onPrimary when using a route chain. Finish in onPrimaryRoutesComplete after the last route. primaryRouteNextButtonText and primaryRouteDoneButtonText customize the route controls.
GreetKit does not include a separate "next steps" card/list primitive. If the primary button should continue into setup, model that as a route chain; if the app needs additional cards, build them in the consuming app or in the destination views.
The default background is the system sheet surface. Use greetBackground(_:) when an app needs a more branded first-run experience:
GreetView(
content: MyGreet(),
isLoading: $isLoading,
errorMessage: $errorMessage,
onPrimary: {},
onSkip: {})
.greetBackground(.animatedGradient())
.greetStyle(GreetStyle(tint: .indigo))Built-in options:
.system— the default platform background..softGradient/.softGradient(brand:palette:)— a restrained brand-derived background tuned for readable first-run content..linearGradient(colors:startPoint:endPoint:)— app-provided colors with the library-managed footer treatment..animatedGradient(brand:palette:motion:)— an opt-in smooth full-surface animated gradient. It uses the style tint by default, adapts its tones for light and dark mode, and automatically becomes static when Reduce Motion is enabled..custom { context in ... }— a fully custom SwiftUI background. Usecontext.reduceMotion,context.brandColor, andcontext.colorSchemeto keep custom backgrounds consistent and accessible.
Destination views can still draw their own backgrounds. If they do, that local destination background appears above the GreetKit background.
Every background spans behind the pinned footer and button area, including .system. Scroll indicators are hidden on supported platforms so branded sheets do not show a macOS scrollbar over the content.
GreetStyle.tint is the default brand color for .softGradient and .animatedGradient(). Pass brand: when the background should use a different brand color from the controls, or pass a full palette when an app needs exact light and dark tones:
let palette = GreetGradientPalette(
light: .init(
base: .white,
primary: .pink,
secondary: .orange,
accent: .yellow),
dark: .init(
base: .black,
primary: .pink,
secondary: .purple,
accent: .cyan))
GreetView(
content: MyGreet(),
isLoading: $isLoading,
errorMessage: $errorMessage,
onPrimary: {},
onSkip: {})
.greetBackground(.animatedGradient(palette: palette))Use motion: when the default dancing gradient should be calmer or more expressive:
GreetView(
content: MyGreet(),
isLoading: $isLoading,
errorMessage: $errorMessage,
onPrimary: {},
onSkip: {})
.greetBackground(.animatedGradient(motion: .expressive))The built-in presets are .subtle, .standard, and .expressive. Stronger motion increases movement, speed, and gradient contrast. For finer control, pass GreetGradientMotion(strength:); values are clamped from 0 to 2, and 0 keeps the animated-gradient color field static.
.animatedMesh(primary:secondary:accent:) remains available as a deprecated compatibility alias for .animatedGradient(palette:motion:).
GreetKit keeps the footer pinned while content scrolls behind it. A measured footer mask fades overflowing content only above the footer; when scrolling reaches the end, visible content is fully opaque again.
Use greetStyle(_:) to override foreground, tint, and button colors while keeping the library's layout, typography, and motion:
GreetView(
content: MyGreet(),
isLoading: $isLoading,
errorMessage: $errorMessage,
onPrimary: {},
onSkip: {})
.greetBackground(.softGradient)
.greetStyle(GreetStyle(
tint: .indigo,
titleColor: .primary,
featureIconColor: .mint,
primaryButtonForegroundColor: .white,
primaryButtonProgressTint: .white,
secondaryButtonColor: .secondary))GreetBackground controls the surface behind the sheet content. GreetStyle controls foreground roles such as title, subtitle, feature rows, primary button text, and secondary button text. Any color you leave as nil uses the standard system treatment.
The view is purely presentational:
- Give every
GreetFeatureItemandGreetPrimaryRoutea stableid. These IDs preserve SwiftUI identity and are used for routing and analytics. isLoading: Binding<Bool>— whentrue, the primary button shows a progress spinner and both buttons are disabled.errorMessage: Binding<String?>— when non-nil, the view presents an alert. Setting it back tonil(or letting the user tap the OK button) dismisses the alert.allowsInteractiveDismissal— defaults totrue. Set it tofalseonly for setup flows that must block swipe or window dismissal.onPrimary/onSkip— fired on tap. Your state layer handles the rest.primaryRoutes/primaryRouteDestination— optional chained follow-up routes opened by the primary button with in-sheet slide transitions. The package supplies customizable Next and Done controls.primaryDestination— convenience API for a single follow-up route.onPrimarystill fires before the route opens.
Route navigation state is intentionally transient and owned inside GreetView; persist only completed setup state in your app. Destination builders are generic at the public API and type-erased internally so call sites can return different SwiftUI views without exposing that plumbing.
GreetFeatureItem has Text and LocalizedStringResource initializers. Prefer the initializer with an explicit id; the old ID-less initializers remain only for compatibility and are deprecated.
Run the package tests from the package root:
swift testMIT. See LICENSE.

