TL;DR: By default, the display order of TipKit tips is uncontrolled. In iOS 18, Apple introduced the TipGroup API specifically to build ordered Onboarding flows. For older versions, we can achieve the same effect through custom rules.
TipKit is a powerful tool for helping users discover new features. However, when building step-by-step tutorials, we need strict control over the sequence in which tips appear. This article covers the native solution for iOS 18 and a compatibility workaround for iOS 17.
Solution 1: Using TipGroup (Recommended for iOS 18+)
In iOS 18 (along with macOS 15 and the iOS 26 environment), TipGroup is the optimal way to manage the priority of multiple tips. By setting the .ordered property, the system automatically ensures that only one tip is displayed at a time, and the next tip appears only after the previous one becomes invalid or is dismissed.
Basic Usage
struct Tip1: Tip {
@Parameter static var show: Bool = false
var title: Text { Text("Step 1: Tap Here") }
var rules: [Rule] {
#Rule(Self.$show) { $0 }
}
}
struct Tip2: Tip {
var title: Text { Text("Step 2: View Details") }
// Tip2 doesn't need extra Rules; TipGroup automatically controls its timing
}
struct TipGroupDemo: View {
// Create an ordered TipGroup
@State var tips = TipGroup(.ordered) {
Tip1()
Tip2()
}
var body: some View {
VStack {
Text("Welcome")
.popoverTip(tips.currentTip) // Automatically shows the tip that should be active
Button("Start Tutorial") {
Tip1.show = true // Activate Tip1; Tip2 waits in the queue
}
.buttonStyle(.bordered)
}
}
}
Demo
Distributed Display (Across Components)
TipGroup isn’t limited to a single view. You can pass it to different components to create an onboarding flow that spans multiple views:
var body: some View {
VStack(spacing: 80) {
Text("Feature Module A")
// Only displays if currentTip is Tip1
.popoverTip(tips.currentTip as? Tip1)
Text("Feature Module B")
// Only displays if currentTip is Tip2
.popoverTip(tips.currentTip as? Tip2)
Button("Start Tutorial") {
Tip1.show = true
}
}
}
Solution 2: Custom Sequential Logic (iOS 17 Compatibility)
In iOS 17, we need to manually orchestrate the sequence using @Parameter and the invalidate callback. The core logic is: When the current Tip is closed, automatically modify the activation parameter for the next Tip.
Implementation Steps
- Define a Protocol: Unify the interface for toggling Tips.
- Custom Style: Intercept the close action to trigger the next Tip.
Complete Code
import SwiftUI
import TipKit
// 1. Define a unified protocol
protocol ShowTip: Tip {
static var show: Bool { get set }
}
// 2. Define Tips
struct Step1Tip: ShowTip {
var title: Text = Text("Step 1")
@Parameter static var show: Bool = false
var rules: [Rule] { [ #Rule(Self.$show) { $0 == true } ] }
}
struct Step2Tip: ShowTip {
var title: Text = Text("Step 2")
@Parameter static var show: Bool = false
var rules: [Rule] { [ #Rule(Self.$show) { $0 == true } ] }
}
// 3. Custom Style responsible for the "relay"
struct ChainedTipStyle<NextTip: ShowTip>: TipViewStyle {
let nextTip: NextTip.Type
func makeBody(configuration: Configuration) -> some View {
VStack(alignment: .leading) {
configuration.title?.font(.headline)
configuration.message?.font(.subheadline)
}
.padding()
.background(.thinMaterial)
.cornerRadius(10)
.overlay(alignment: .topTrailing) {
Button(action: {
// Close the current Tip
configuration.tip.invalidate(reason: .tipClosed)
// Core: Activate the next Tip
nextTip.show = true
}) {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
}
.padding(8)
}
}
}
// 4. Usage
struct LegacySequenceDemo: View {
let tip1 = Step1Tip()
let tip2 = Step2Tip()
var body: some View {
VStack {
Button("Start Tutorial") { Step1Tip.show = true }
Text("Target A")
.popoverTip(tip1)
// Bind Tip1 to trigger Tip2 upon closing
.tipViewStyle(ChainedTipStyle(nextTip: Step2Tip.self))
Text("Target B")
.popoverTip(tip2)
}
}
}