You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在iOS的SwiftUI中实现需停留3秒的Impression Tracking

SwiftUI 信息流曝光追踪(停留3秒统计浏览次数)

你的需求核心是只有当卡片在屏幕内停留满3秒且可见比例达标时,才统计浏览次数,原实现的主要问题是缺少定时器取消机制,快速滚动时旧任务仍会触发,导致错误统计。以下是正确的实现方案:

核心改进点

  • 为每个卡片独立管理定时器,进入可见区域启动,离开时立即取消
  • 精确计算卡片的可见比例(建议设置阈值,比如50%),避免边缘露出就触发
  • 使用ObservableObject管理卡片的可见状态和浏览次数,确保视图实时更新

完整实现代码

1. 数据模型与Presenter

import SwiftUI
import Combine

// 信息流数据模型
class Feed: ObservableObject, Identifiable {
    let id = UUID()
    @Published var viewedCount = 0
    @Published var isVisible = false // 标记是否处于可见状态
}

// Presenter 协议与实现
protocol CardViewToPresenterProtocol: AnyObject {
    func trackImpression(for feed: Feed)
}

class Presenter: ObservableObject, CardViewToPresenterProtocol {
    @Published var feeds: [Feed] = []
    
    init() {
        // 模拟测试数据
        (0..<20).forEach { _ in feeds.append(Feed()) }
    }
    
    func trackImpression(for feed: Feed) {
        guard !feed.isVisible else { return }
        feed.viewedCount += 1
        feed.isVisible = true // 标记为已曝光,避免重复统计
    }
}

2. 卡片视图(带曝光追踪逻辑)

struct CardView: View {
    weak var delegate: CardViewToPresenterProtocol?
    @ObservedObject var feed: Feed // 使用ObservedObject监听状态变化
    let index: Int
    
    // 定时器管理
    @State private var timer: DispatchSourceTimer?
    // 可见比例阈值(可根据需求调整,比如0.5代表50%可见)
    private let visibilityThreshold: CGFloat = 0.5
    
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 8)
                .fill(Color.green)
            
            VStack {
                Text("\(feed.viewedCount) 次浏览")
                    .font(.system(size: 12))
                Text("卡片索引:\(index)")
            }
        }
        .frame(height: 200)
        .onTapGesture { print("卡片被点击") }
        .background(
            GeometryReader { geo in
                Color.clear.onAppear {
                    checkVisibilityAndStartTimer(geo: geo)
                }
                .onChange(of: geo.frame(in: .global)) { newFrame in
                    checkVisibilityAndUpdateTimer(newFrame: newFrame)
                }
                .onDisappear {
                    cancelTimer()
                    feed.isVisible = false // 离开屏幕重置状态
                }
            }
        )
    }
    
    // 检查可见性并启动/更新定时器
    private func checkVisibilityAndStartTimer(geo: GeometryProxy) {
        let frame = geo.frame(in: .global)
        guard isVisibleEnough(frame: frame) else { return }
        
        startTimer()
    }
    
    private func checkVisibilityAndUpdateTimer(newFrame: CGRect) {
        if isVisibleEnough(frame: newFrame) {
            if timer == nil {
                startTimer()
            }
        } else {
            cancelTimer()
            feed.isVisible = false
        }
    }
    
    // 判断卡片是否达到可见比例阈值
    private func isVisibleEnough(frame: CGRect) -> Bool {
        let screenBounds = UIScreen.main.bounds
        // 计算卡片与屏幕的交集区域
        let intersection = frame.intersection(screenBounds)
        // 可见比例 = 交集面积 / 卡片面积
        let visibleRatio = intersection.width * intersection.height / (frame.width * frame.height)
        return visibleRatio >= visibilityThreshold
    }
    
    // 启动3秒定时器
    private func startTimer() {
        guard timer == nil else { return }
        
        timer = DispatchSource.makeTimerSource(queue: .main)
        timer?.schedule(deadline: .now() + 3, repeating: .never)
        timer?.setEventHandler { [weak self] in
            guard let self = self else { return }
            self.delegate?.trackImpression(for: self.feed)
            self.cancelTimer() // 统计后取消定时器
        }
        timer?.resume()
    }
    
    // 取消定时器
    private func cancelTimer() {
        timer?.cancel()
        timer = nil
    }
}

3. 列表视图

struct TestUIList: View {
    @ObservedObject var presenter: Presenter
    
    var body: some View {
        List {
            LazyVStack {
                ForEach(presenter.feeds.enumerated().map { $0 }, id: \.element.id) { index, feed in
                    CardView(delegate: presenter, feed: feed, index: index)
                }
            }
        }
    }
}

关键说明

  1. 定时器管理:使用DispatchSourceTimer而非asyncAfter,可以在滚动时随时取消任务,避免延迟执行错误的统计。
  2. 可见性判断:通过计算卡片与屏幕的交集面积比例,确保只有卡片足够可见时才触发计时,避免边缘露出的误统计。
  3. 状态同步:Feed使用ObservableObject,确保曝光状态和浏览次数的变化能实时更新UI。
  4. 重复统计防护:曝光一次后标记isVisible为true,避免同一卡片在屏幕内停留过久重复统计(若需要重复统计,可移除该逻辑)。

内容的提问来源于stack exchange,提问作者Raju

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 13:47:10