TipKit in SwiftUI
TipKit is Apple's framework for showing contextual tips in your app — the little callouts that point at a button or feature a user hasn't discovered yet. It's been around since iOS 17, and I keep meaning to write about it because it's genuinely one of those frameworks that's easier to use than you'd expect.
Defining a tip
Every tip is a struct that conforms to Tip. You give it a title, optionally a message and an image, and TipKit handles the rest — including deciding whether and when to show it.
import TipKit
struct FavoriteTip: Tip {
var title: Text {
Text("Add to Favorites")
}
var message: Text? {
Text("Tap the star icon to save items for later.")
}
var image: Image? {
Image(systemName: "star")
}
}
Showing a tip
There are two ways to present a tip: inline (shifts content down) or a popover (floats over a view). For most cases in SwiftUI, the popover style is what you want.
struct ContentView: View {
private let favoriteTip = FavoriteTip()
var body: some View {
Button {
// add to favorites
} label: {
Image(systemName: "star")
}
.popoverTip(favoriteTip)
}
}
The inline variant uses TipView directly, which is handy if you want the tip to push other content rather than float on top of it.
VStack {
TipView(favoriteTip)
// rest of your content
}
Configuring TipKit
You need to call Tips.configure somewhere early in your app — typically in the App init or in @main. Without this, tips won't appear.
@main
struct MyApp: App {
init() {
try? Tips.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
During development it's useful to reset tips on each launch so you can see them every time without uninstalling the app:
try? Tips.configure([
.displayFrequency(.immediate),
.datastoreLocation(.applicationDefault)
])
Note:
displayFrequency(.immediate)shows tips every launch. Don't ship this — it's for testing only.
Rules and eligibility
TipKit has a built-in rule system so tips only show up when it makes sense. You can gate on a simple boolean, on how many times a user has performed an action, or on a parameter that your app updates.
struct FavoriteTip: Tip {
@Parameter
static var hasViewedItems: Bool = false
var title: Text { Text("Add to Favorites") }
var message: Text? { Text("Tap the star to save items for later.") }
var rules: [Rule] {
#Rule(Self.$hasViewedItems) { $0 == true }
}
}
Then somewhere in your app, when the user has looked at a few items:
FavoriteTip.hasViewedItems = true
The tip will only show once that parameter flips. This keeps tips from appearing for users who clearly already know what they're doing.
Events
For frequency-based eligibility, use Tips.Event. This tracks how many times something happens and lets you show a tip only after a certain threshold.
struct FavoriteTip: Tip {
static let itemViewedEvent = Event(id: "item-viewed")
var title: Text { Text("Add to Favorites") }
var rules: [Rule] {
#Rule(Self.itemViewedEvent) { $0.donations.count >= 3 }
}
}
// call this each time the user views an item
await FavoriteTip.itemViewedEvent.donate()
After three item views, the tip becomes eligible. Combined with TipKit's default displayFrequency of .daily, it'll only show once per day — which feels much less intrusive than shouting at users on first launch.
Dismissing a tip
Tips dismiss themselves when the user taps the close button. If you want to dismiss programmatically (say, the user performed the action the tip was describing), call invalidate:
favoriteTip.invalidate(reason: .actionPerformed)
Wrap up
TipKit takes about ten minutes to add to a project and handles the annoying parts of onboarding tips — persistence, frequency, eligibility — without any custom infrastructure. If you've been putting off adding hints to your app because it felt like too much plumbing, this is worth a look.
I added it to Calendo to nudge users toward a gesture that's easy to miss, and the setup was genuinely straightforward.
Resources:
Read more
- ExpressibleByStringLiteral URL • 2 minutes reading time.
- Swift Package: NetworkMonitor • 3 minutes reading time.
- withAnimation • 2 minutes reading time.
Share
Share Bluesky Mastodon Twitter LinkedIn Facebook