💭 Sequential Display Tips with TipKit

Get weekly handpicked updates on Swift and SwiftUI!

TL;DR: In iOS 18, use TipGroup to display tips in a specific order based on conditions. For iOS 17, achieve similar results with custom parameters and rules. Sample code demonstrates TipGroup usage and custom logic for sequential tip display.

Background

TipKit helps users discover new features or improve efficiency, but by default, tips are not displayed in a developer-defined sequence. This guide explains how to achieve sequential tip display in different iOS versions.

Using TipGroup (iOS 18)

In iOS 18, TipKit introduces TipGroup, allowing developers to group multiple tips and display them sequentially based on specified conditions.

Sample Code

Swift
struct Tip1: Tip {
  @Parameter static var show: Bool = false
  var title: Text { Text("Tip1") }

  var rules: [Rule] {
    #Rule(Self.$show) { $0 }
  }
}

struct Tip2: Tip {
  var title: Text { Text("Tip2") }
}

struct TipGroupDemo: View {
  @State var tips = TipGroup(.ordered) { // Create an ordered TipGroup
    Tip1()
    Tip2()
  }

  var body: some View {
    VStack {
      Text("Hello World")
        .popoverTip(tips.currentTip) // Show the current active tip

      Button("Start Show Tips") {
        Tip1.show = true // Activate Tip1
      }
      .buttonStyle(.bordered)
    }
  }
}

Demonstration

Using TipGroup Across Components

Developers can reuse a single TipGroup across different components to ensure only the appropriate tip is displayed:

Swift
var body: some View {
  VStack(spacing: 80) {
    Text("Hello World")
      .popoverTip(tips.currentTip as? Tip1) // Corresponding to Tip1

    Text("Fatbobman's Blog")
      .popoverTip(tips.currentTip as? Tip2) // Corresponding to Tip2

    Button("Start Show Tips") {
      Tip1.show = true
    }
    .buttonStyle(.bordered)
  }
}

Demonstration

Tip: In an ordered TipGroup, subsequent tips are only displayed after the previous tips are invalidated.

Custom Sequential Display (iOS 17)

For iOS 17, which lacks TipGroup, similar functionality can be implemented using custom parameters and rules.

Implementation Steps

  1. Define Custom Parameters and Rules Use a show parameter in tips to control display conditions.
  2. Declare Custom Styles Use a custom TipViewStyle to activate the next tip when the close button is tapped.

Sample Code

Swift
import SwiftUI
import TipKit

struct Tip1: ShowTip {
  var title: Text = Text("Step1 Tips")
  @Parameter static var show: Bool = false

  var rules: [Rule] {
    [ #Rule(Self.$show) { $0 == true } ]
  }
}

struct Tip2: ShowTip {
  var title: Text = Text("Step2 Tips")
  @Parameter static var show: Bool = false

  var rules: [Rule] {
    [ #Rule(Self.$show) { $0 == true } ]
  }
}

struct ContentView: View {
  private var tip1 = Tip1()
  private var tip2 = Tip2()
  var body: some View {
    List {
      Button("Show Tip1") {
        Tip1.show = true
      }

      TipView(tip1, arrowEdge: .bottom)
        .tipViewStyle(MyTipStyle(tip: Tip2.self))
      Text("Step1")

      TipView(tip2, arrowEdge: .bottom)
      Text("Step2")
    }
    .padding()
  }
}

protocol ShowTip: Tip {
  static var show: Bool { get set }
}

struct MyTipStyle<T: ShowTip>: TipViewStyle {
  let tip: T.Type
  func makeBody(configuration: Configuration) -> some View {
    VStack {
      configuration.image?.font(.title2).foregroundStyle(.green)
      configuration.title?.bold().font(.headline).textCase(.uppercase)
      configuration.message?.foregroundStyle(.secondary)
    }
    .frame(maxWidth: .infinity, alignment: .leading)
    .backgroundStyle(.thinMaterial)
    .overlay(alignment: .topTrailing) {
      Image(systemName: "multiply")
        .font(.title2)
        .foregroundStyle(.secondary)
        .onTapGesture {
          configuration.tip.invalidate(reason: .tipClosed)
          tip.show = true // Activate the next tip
        }
    }
    .padding()
  }
}

Demonstration

Further Reading