💭 TipKit 实现按序显示提示

为您每周带来有关 Swift 和 SwiftUI 的精选资讯!

TL;DR: 在 iOS 18 中,通过 TipGroup 可以按顺序展示提示,确保用户按开发者预设的步骤接收提示信息。在 iOS 17 中,可通过自定义参数和规则实现类似功能。示例代码展示了 TipGroup 的基础用法和如何自定义提示逻辑。

背景

TipKit 可帮助用户发现新功能或提升效率,但默认情况下,提示不会按开发者预设的顺序展示。以下将探讨如何在不同 iOS 版本中实现按顺序显示提示。

使用 TipGroup(iOS 18)

在 iOS 18 中,TipKit 引入了 TipGroup,使开发者可以将多个 Tip 组合在一起,按顺序显示满足条件的提示。

示例代码

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) { // 创建按顺序显示的 TipGroup
    Tip1()
    Tip2()
  }

  var body: some View {
    VStack {
      Text("Hello World")
        .popoverTip(tips.currentTip) // 显示满足条件的当前提示

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

示例效果

在多个组件中使用 TipGroup

开发者可以在不同组件中复用同一个 TipGroup,确保仅展示符合条件的提示:

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

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

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

示例效果

提示: 在有序的 TipGroup 中,后续提示仅在前置提示被无效化后显示。

自定义顺序显示方案(iOS 17)

在 iOS 17 中,由于缺少 TipGroup,可以通过自定义参数和规则创建类似效果。

实现步骤

  1. 定义自定义参数和规则 在 Tip 中使用参数 show 控制显示条件。
  2. 声明自定义样式 在自定义 TipViewStyle 中,点击关闭按钮后激活下一个提示。

示例代码

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 // 激活下一个提示
        }
    }
    .padding()
  }
}

示例效果

延伸阅读