纯SwiftUI集成

更新时间:
复制 MD 格式

1 前置说明

1.1 适用范围

  • 项目类型:纯 SwiftUI 工程(App + Scene + WindowGroup 生命周期)。

  • 目标:接入阿里云 Quick Tracking(QTCommon)SDK,完成页面 PV 埋点、页面属性事件上报。

  • 不适用:UIKit 工程的自动页面采集逻辑可直接参考:基础集成和埋点API,本文重点解决 SwiftUI 场景。

1.2 纯 SwiftUI 为什么要手动埋点?

官方setAutoPageEnabled:是通过 HookUIViewController的viewWillAppear:/viewWillDisappear:,并采集className实现的:

+ (void)setAutoPageEnabled:(BOOL)value;   // 默认 NO

但 SwiftUI 的 View 协议本身不是 UIViewController,其生命周期由 UIHostingController 托管,真正触发 viewWillAppear 的是 hosting controller 的 class name,而不是业务页面的语义名称。因此:

纯 SwiftUI 界面无法依赖 setAutoPageEnabled:YES 自动采集到业务页面,必须采用手动页面埋点方案。

1.3 封装的类文件

已经集成了QTCommon SDK ,将封装的类文件直接导入到项目:QTPageTracker.swift

2. SDK 初始化(SwiftUI 生命周期)

SwiftUI 默认没有 AppDelegate,需通过 @UIApplicationDelegateAdaptor 注入:

import SwiftUI
import QTCommon

class AppDelegate: NSObject, UIApplicationDelegate {

  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {
    qtInit()
    registerLifecycleNotifications()
    return true
  }

  private func qtInit() {
    // 1. 打开调试日志(Release 必须关闭)
    QTConfigure.setLogEnabled(true)
    // 2. 设置收数域名
    QTConfigure.setCustomDomain("https://your-collect-domain.aliyuncs.com", standbyDomain: nil)
    // 3. 初始化 SDK(务必在取得用户隐私授权后调用)
    QTConfigure.initWithAppkey("your_app_key", channel: "App Store")
    ......
  }

  // MARK: - 前后台通知监听
  private func registerLifecycleNotifications() {
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(didEnterBackground),
      name: UIApplication.didEnterBackgroundNotification,
      object: nil
    )
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(willEnterForeground),
      name: UIApplication.willEnterForegroundNotification,
      object: nil
    )
  }

  @objc private func didEnterBackground() {
    QTPageTracker.appDidEnterBackground()
  }

  @objc private func willEnterForeground() {
    QTPageTracker.appWillEnterForeground()
  }
  ......
}

3. 自动页面埋点封装

为了将 beginLogPageView: /endLogPageView: 与 SwiftUI 的onAppear/onDisappear声明式对接,建议统一封装一个ViewModifier和扩展方法。

3.1 核心封装(QTPageTracker.swift)

import SwiftUI
import QTCommon

/// QTCommon 页面追踪器
public enum QTPageTracker {

  // MARK: - 页面状态
  /// 逻辑上当前正在展示的页面(用于前后台恢复计时)
  private static var currentPageName: String?
  /// 是否处于后台状态
  private static var isInBackground = false

  // MARK: - 页面 PV

  /// 手动开始页面 PV 统计
  public static func beginPage(_ pageName: String) {
    currentPageName = pageName
    QTMobClick.beginLogPageView(pageName)
    #if DEBUG
    print("[QTPageTracker] begin page: \(pageName)")
    #endif
  }

  /// 手动结束页面 PV 统计
  public static func endPage(_ pageName: String) {
    if currentPageName == pageName {
      currentPageName = nil
    }
    QTMobClick.endLogPageView(pageName)
    #if DEBUG
    print("[QTPageTracker] end page: \(pageName)")
    #endif
  }

  /// 更新页面属性,可在任意时机调用
  public static func updateProperties(
    _ properties: [String: Any],
    forPage pageName: String
  ) {
    guard !properties.isEmpty else { return }
    UMSpm.updatePageProperties(pageName, properties: properties)
    #if DEBUG
    print("[QTPageTracker] update properties for page: \(pageName), properties: \(properties)")
    #endif
  }

  // MARK: - 前后台切换

  /// App 进入后台:结束当前页面计时
  public static func appDidEnterBackground() {
    guard !isInBackground else { return }
    isInBackground = true
    if let pageName = currentPageName {
      QTMobClick.endLogPageView(pageName)
      #if DEBUG
      print("[QTPageTracker] end page (background): \(pageName)")
      #endif
    }
  }

  /// App 返回前台:恢复当前页面计时
  public static func appWillEnterForeground() {
    guard isInBackground else { return }
    isInBackground = false
    if let pageName = currentPageName {
      QTMobClick.beginLogPageView(pageName)
      #if DEBUG
      print("[QTPageTracker] begin page (foreground): \\(pageName)")
      #endif
    }
  }
}

// MARK: - View Modifier
struct QTPageTrackModifier: ViewModifier {
  let pageName: String

  func body(content: Content) -> some View {
    content
    .onAppear {
      QTPageTracker.beginPage(pageName)
    }
    .onDisappear {
      QTPageTracker.endPage(pageName)
    }
  }
}

extension View {
  /// 为当前视图绑定一个 QTCommon 页面编码
  public func qtPage(_ name: String) -> some View {
    modifier(QTPageTrackModifier(pageName: name))
  }
}

3.2 普通页面使用方式

非根页面的普通 SwiftUI View,有两个方式

方式一:直接挂 .qtPage(...)

struct HomeView: View {
  var body: some View {  
    VStack {
      Text("首页")
    }
    .qtPage("home_page")
  }
}

方式二:在view的生命周期中 onAppear 和 onDisappear 调对应的界面开始和结束的埋点

struct HomeView: View {
  var body: some View {
    VStack {
      Text("首页")
    }
    .onAppear {
      QTPageTracker.beginPage("AutoTrackViewB")
    }
    .onDisappear {
      QTPageTracker.endPage("AutoTrackViewB")
    }
  }
}

3.3 NavigationStack根控制器

SwiftUI 中 NavigationStack 的根页面不能依赖 .qtPage 的 onAppear/onDisappear,必须通过 .onChange(of: path) 监听导航路径变化,手动调用 beginPage / endPage进行界面埋点。

struct ContentView: View {
  @State private var path = NavigationPath()

  var body: some View {
    NavigationStack(path: $path) {
      VStack(spacing: 20) {
        Image(systemName: "globe")
        Text("Hello, world!")

        NavigationLink(value: "AutoTrackViewA") {
          Text("跳转到 AutoTrackViewA")
        }
      }
      .padding()
      .navigationTitle("首页")
      .navigationDestination(for: String.self) { value in
                                                if value == "AutoTrackViewA" {
                                                  AutoTrackViewA()
                                                }
                                               }
    }
    .onChange(of: path, initial: true) { _, newPath in
                                        if newPath.isEmpty {
                                          QTPageTracker.beginPage("homePage")
                                        } else {
                                          QTPageTracker.endPage("homePage")
                                        }
                                       }
  }
}

4. 页面属性设置

4.1 接口说明

// 需要引入 <QTCommon/UMSpm.h>
+ (void)updatePageProperties:(NSString *)pageName
properties:(NSDictionary *)pProperties;

4.2 SwiftUI 中使用

进入页面时设置初始属性

struct DetailView: View {
  var body: some View {
    VStack {
      Text("详情页")
    }
    .qtPage("detail_page")
    .onAppear {
      QTPageTracker.updateProperties([
        "referrer": "home_page",
        "visit_type": "detail"
      ], forPage: "detail_page")
    }
  }
}

任意时机更新属性

struct DetailView: View {
  @State private var selectedTag: String = "none"

  var body: some View {
    VStack {
      Button("标记为 VIP") {
        selectedTag = "vip"
        QTPageTracker.updateProperties(
          ["user_tag": "vip"],
          forPage: "detail_page"
        )
      }
    }
    .qtPage("detail_page")
  }
}

5. 事件埋点

其他的相关的API使用可以参考:埋点API

5.1 接口说明

+ (void)event:(NSString *)eventCode;
+ (void)event:(NSString *)eventCode attributes:(NSDictionary *)attributes;
+ (void)event:(NSString *)eventId pageName:(NSString *)pageName attributes:(NSDictionary *)attributes;

5.2 SwiftUI 中使用

Button("购买") {
  QTMobClick.event("purchase", attributes: [
    "product_id": "12345",
    "category": "book",
    "quantity": 3
  ])
}

6. 完整示例

完整的示例:可以参考:swiftui-demo