2023 DesignEssentialsApp Services
WWDC23 · 15 min · Design / Essentials / App Services
Make features discoverable with TipKit
Teach people how to use your app with TipKit! Learn how you can create effective educational moments through tips. We’ll share how you can build eligibility rules to reach the ideal audience, control tip frequency, and strategies for testing to ensure successful interactions.
Watch at developer.apple.com ↗Chapters
Code shown on screen · 15 snippets
Create a tip
struct FavoriteBackyardTip: Tip {
var title: Text {
Text("Save as a Favorite")
}
var message: Text {
Text("Your favorite backyards always appear at the top of the list.")
}
} Configure TipsCenter
@main
struct BackyardBirdsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
// ...
init() {
TipsCenter.shared.configure()
}
} Add actions and an asset to a tip
struct FavoriteBackyardTip: Tip {
var title: Text {
Text("Save as a Favorite").foregroundColor(.indigo)
}
var message: Text {
Text("Your favorite backyards always appear at the top of the list.")
}
var asset: Image {
Image(systemName: "star")
}
var actions: [Action] {
[
Tip.Action(
id: "learn-more",
title: "Learn More"
)
]
}
} Create a popover view
private let favoriteBackyardTip = FavoriteBackyardTip()
// ...
.toolbar {
ToolbarItem {
Button {
backyard.isFavorite.toggle()
} label: {
Label("Favorite", systemImage: "star")
.symbolVariant(
backyard.isFavorite ? .fill : .none
)
}
.popoverMiniTip(tip: favoriteBackyardTip)
}
} Add a parameter based rule
struct FavoriteBackyardTip: Tip {
static var isLoggedIn: Bool = false
// ...
var rules: Predicate<RuleInput...> {
// User is logged in
#Rule(Self.$isLoggedIn) { $0 == true }
}
} Add an event based rule
struct FavoriteBackyardTip: Tip {
static var isLoggedIn: Bool = false
static let enteredBackyardDetailView: Event = Event<DetailViewDonation>(
id: "entered-backyard-detail-view"
)
// ...
var rules: Predicate<RuleInput...> {
// User is logged in
#Rule(Self.$isLoggedIn) { $0 == true }
// User has entered any backyard detail view at least 3 times
#Rule(Self.enteredBackyardDetailView) { $0.count >= 3 }
}
} Donate the event when a view appears
.onAppear {
FavoriteBackyardTip.enteredBackyardDetailView.donate()
} Filter event donations in an event based rule
// User has entered any backyard detail view at least 3 times in the past 5 days
#Rule(Self.enteredBackyardDetailView) {
$0.donations.filter {
$0.date > Date.now.addingTimeInterval(-5 * 60 * 60 * 24)
}
.count >= 3
} Create a custom donation
// Create the associated type
extension BackyardDetailTip {
struct DetailViewDonation: DonationValue {
let backyardID: Int
}
}
// Donate the unique id of the backyard detail being viewed
.onAppear {
BackyardFavoriteTip.enteredBackyardDetailView.donate(
with: .init(backyardID: backyard.id)
)
}
struct FavoriteBackyardTip: Tip {
// ...
var rules: Predicate<RuleInput...> {
// Update the rule to specify a backyardID
#Rule(Self.enteredBackyardDetailView) {
$0.donations.filter {
$0.date > Date.now.addingTimeInterval(-5 * 60 * 60 * 24)
}
.largestSubset(by: \.backyardID)
.count >= 3
}
}
} Configure display frequency
// One tip per day.
TipsCenter.shared.configure {
DisplayFrequency(.daily)
}
// One tip per hour.
TipsCenter.shared.configure {
DisplayFrequency(.hourly)
}
// Custom configuration. Only show one tip every five days.
let fiveDays: TimeInterval = 5 * 24 * 60 * 60
TipsCenter.shared.configure {
DisplayFrequency(fiveDays)
}
// No frequency control. Show all tips as soon as eligible.
TipsCenter.shared.configure {
DisplayFrequency(.immediate)
} Turn off display frequency controls for a tip
struct FavoriteBackyardTip: Tip {
// ...
var options: [Option] {
[.ignoresDisplayFrequency(true)]
}
} Invalidate a tip
Button {
backyard.isFavorite.toggle()
// When user taps the favorite button, dismiss the tip
favoriteBackyardTip.invalidate(reason: .userPerformedAction)
} label: {
Label("Favorite", systemImage: "star")
.symbolVariant(backyard.isFavorite ? .fill : .none)
}
.popoverMiniTip(tip: favoriteBackyardTip) Configure max display count on a tip
struct FavoriteBackyardTip: Tip {
// ...
var options: [Option] {
[.maxDisplayCount(5)]
}
} Programmatically call testing API
// Show all defined tips in the app
TipsCenter.showAllTips()
// Show some tips, but not all
TipsCenter.showTips([tip1, tip2, tip3])
// Hide some tips, but not all
TipsCenter.hideTips([tip1, tip2, tip3])
// Hide all tips defined in the app
TipsCenter.hideAllTips()
// Purge all TipKit related data
TipsCenter.resetDatastore() Configure launch arguments in your scheme
// Show all defined tips in the app
com.apple.TipKit.ShowAllTips 1
// Show some tips, but not all
com.apple.TipKit.ShowTips tipID,otherTipID
// Hide some tips, but not all
com.apple.TipKit.HideAllTips 1
// Hide all tips defined in the app
com.apple.TipKit.HideTips tipID,otherTipID
// Purge all TipKit related data
com.apple.TipKit.ResetDatastore 1 Related sessions
-
15 min