如何在 Swift 中按时间跟踪 CollectionView 单元格

如何解决如何在 Swift 中按时间跟踪 CollectionView 单元格

我一直在开发一项功能,用于检测用户何时看到帖子以及何时未看到。当用户确实看到帖子时,我将单元格的背景变成绿色,如果没有,则保持红色。现在这样做之后,我注意到即使用户只向下滚动页面,我也将所有单元格都打开为绿色,所以我添加了一个计时器,但我无法理解如何正确使用它,所以我想自己也许你们有给我的一个建议,因为我有点坚持了两天:(

  • 编辑:忘记提及如果单元格通过了 2 秒的最小长度,则标记为已看到。

这是我的代码: 我的 VC(CollectionView):

import UIKit

class ViewController: UIViewController,UIScrollViewDelegate {
    
    var impressionEventStalker: ImpressionStalker?
    var impressionTracker: ImpressionTracker?
    
    var indexPathsOfCellsTurnedGreen = [IndexPath]() // All the read "posts"
    
    
    @IBOutlet weak var collectionView: UICollectionView!{
        didSet{
            collectionView.contentInset = UIEdgeInsets(top: 20,left: 0,bottom: 0,right: 0)
            impressionEventStalker = ImpressionStalker(minimumPercentageOfCell: 0.70,collectionView: collectionView,delegate: self)

        }
    }
    
    
    
    func registerCollectionViewCells(){
        let cellNib = UINib(nibName: CustomCollectionViewCell.nibName,bundle: nil)
        collectionView.register(cellNib,forCellWithReuseIdentifier: CustomCollectionViewCell.reuseIdentifier)
    }
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        collectionView.delegate = self
        collectionView.dataSource = self
        
        registerCollectionViewCells()
        
    }
    
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        impressionEventStalker?.stalkCells()
        
    }
    
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        impressionEventStalker?.stalkCells()
    }
    
}

// MARK: CollectionView Delegate + DataSource Methods
extension ViewController: UICollectionViewDelegateFlowLayout,UICollectionViewDataSource{
    func collectionView(_ collectionView: UICollectionView,numberOfItemsInSection section: Int) -> Int {
        return 100
    }
    
    
    func collectionView(_ collectionView: UICollectionView,cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        guard let customCell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.reuseIdentifier,for: indexPath) as? CustomCollectionViewCell else {
            fatalError()
        }
        
        customCell.textLabel.text = "\(indexPath.row)"
        
        if indexPathsOfCellsTurnedGreen.contains(indexPath){
            customCell.cellBackground.backgroundColor = .green
        }else{
            customCell.cellBackground.backgroundColor = .red
        }
        
        return customCell
    }
    
    
    
    func collectionView(_ collectionView: UICollectionView,layout collectionViewLayout: UICollectionViewLayout,sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: 150,height: 225)
    }
    
    
    func collectionView(_ collectionView: UICollectionView,insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 0,left: 20,right: 20) // Setting up the padding
    }
    
    
    func collectionView(_ collectionView: UICollectionView,willDisplay cell: UICollectionViewCell,forItemAt indexPath: IndexPath) {
        //Start The Clock:
        if let trackableCell = cell as? TrackableView {
            trackableCell.tracker = ImpressionTracker(delegate: trackableCell)
            trackableCell.tracker?.start()
            
        }
    }
    
    
    func collectionView(_ collectionView: UICollectionView,didEndDisplaying cell: UICollectionViewCell,forItemAt indexPath: IndexPath) {
        //Stop The Clock:
        (cell as? TrackableView)?.tracker?.stop()
    }
    
    
    
}


// MARK: - Delegate Method:
extension ViewController:ImpressionStalkerDelegate{
    func sendEventForCell(atIndexPath indexPath: IndexPath) {
        
        guard let customCell = collectionView.cellForItem(at: indexPath) as? CustomCollectionViewCell else {
            return
        }
        

        customCell.cellBackground.backgroundColor = .green
        indexPathsOfCellsTurnedGreen.append(indexPath) // We append all the visable Cells into an array
    }
}

我的 ImpressionStalker:

import Foundation
import UIKit

protocol ImpressionStalkerDelegate:NSObjectProtocol {
    func sendEventForCell(atIndexPath indexPath:IndexPath)
}

protocol ImpressionItem {
    func getUniqueId()->String
}


class ImpressionStalker: NSObject {
    
    //MARK: Variables & Constants
    let minimumPercentageOfCell: CGFloat
    weak var collectionView: UICollectionView?
    
    static var alreadySentIdentifiers = [String]()
    weak var delegate: ImpressionStalkerDelegate?
    
    
    //MARK: Initializer
    init(minimumPercentageOfCell: CGFloat,collectionView: UICollectionView,delegate:ImpressionStalkerDelegate ) {
            self.minimumPercentageOfCell = minimumPercentageOfCell
            self.collectionView = collectionView
            self.delegate = delegate
        }
    
    
    // Checks which cell is visible:
    func stalkCells() {
        for cell in collectionView!.visibleCells {
            if let visibleCell = cell as? UICollectionViewCell & ImpressionItem {
                let visiblePercentOfCell = percentOfVisiblePart(ofCell: visibleCell,inCollectionView: collectionView!)
                
                if visiblePercentOfCell >= minimumPercentageOfCell,!ImpressionStalker.alreadySentIdentifiers.contains(visibleCell.getUniqueId()){ // >0.70 and not seen yet then...
                    guard let indexPath = collectionView!.indexPath(for: visibleCell),let delegate = delegate else {
                        continue
                    }
                    
                   
                    delegate.sendEventForCell(atIndexPath: indexPath) // send the cell's index since its visible.
                    
                    ImpressionStalker.alreadySentIdentifiers.append(visibleCell.getUniqueId()) // to avoid double events to show up.
                }
            }
        }
    }
    
    
    // Func Which Calculate the % Of Visible of each Cell:
    private func percentOfVisiblePart(ofCell cell:UICollectionViewCell,inCollectionView collectionView:UICollectionView) -> CGFloat{
           
           guard let indexPathForCell = collectionView.indexPath(for: cell),let layoutAttributes = collectionView.layoutAttributesForItem(at: indexPathForCell) else {
                   return CGFloat.leastNonzeroMagnitude
           }
           
           let cellFrameInSuper = collectionView.convert(layoutAttributes.frame,to: collectionView.superview)
           
           let interSectionRect = cellFrameInSuper.intersection(collectionView.frame)
           let percentOfIntersection: CGFloat = interSectionRect.height/cellFrameInSuper.height
           
           return percentOfIntersection
       }
}

印象跟踪器:

import Foundation
import UIKit

protocol ViewTracker {

    init(delegate: TrackableView)
    func start()
    func pause()
    func stop()
    
}


final class ImpressionTracker: ViewTracker {
    private weak var viewToTrack: TrackableView?
        
    private var timer: CADisplayLink?
    private var startedTimeStamp: CFTimeInterval = 0
    private var endTimeStamp: CFTimeInterval = 0
    
     init(delegate: TrackableView) {
        viewToTrack = delegate
        setupTimer()
    }
    
    
    func setupTimer() {
        timer = (viewToTrack as? UIView)?.window?.screen.displayLink(withTarget: self,selector: #selector(update))
        timer?.add(to: RunLoop.main,forMode: .default)
        timer?.isPaused = true
    }
    
    
    func start() {
        guard viewToTrack != nil else { return }
        timer?.isPaused = false
        startedTimeStamp = CACurrentMediaTime() // Current Time in seconds.
    }
    
    func pause() {
        guard viewToTrack != nil else { return }
        timer?.isPaused = true
        endTimeStamp = CACurrentMediaTime()
        print("Im paused!")
    }
    
    func stop() {
        timer?.isPaused = true
        timer?.invalidate()
    }
    
    @objc func update() {
        guard let viewToTrack = viewToTrack else {
            stop()
            return
        }

        guard viewToTrack.precondition() else {
            startedTimeStamp = 0
            endTimeStamp = 0
            return
        }

        endTimeStamp = CACurrentMediaTime()
        trackIfThresholdCrossed()
    }
    
    
    private func trackIfThresholdCrossed() {
       
        guard let viewToTrack = viewToTrack else { return }
        let elapsedTime = endTimeStamp - startedTimeStamp
        if  elapsedTime >= viewToTrack.thresholdTimeInSeconds() {
            viewToTrack.viewDidStayOnViewPortForARound()
            

            startedTimeStamp = endTimeStamp
        }
    }
}

我的自定义单元:

import UIKit


protocol TrackableView: NSObject {
    var tracker: ViewTracker? { get set }
    func thresholdTimeInSeconds() -> Double //Takes care of the screen's time,how much "second" counts.
    func viewDidStayOnViewPortForARound() // Counter for how long the "Post" stays on screen.
    func precondition() -> Bool // Checks if the View is full displayed so the counter can go on fire.
}


class CustomCollectionViewCell: UICollectionViewCell {
    var tracker: ViewTracker?
    
    static let nibName = "CustomCollectionViewCell"
    static let reuseIdentifier = "customCell"
    
    @IBOutlet weak var cellBackground: UIView!
    @IBOutlet weak var textLabel: UILabel!
    
    var numberOfTimesTracked : Int = 0 {
        didSet {
            self.textLabel.text = "\(numberOfTimesTracked)"
        }
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        cellBackground.backgroundColor = .red
        layer.borderWidth = 0.5
        layer.borderColor = UIColor.lightGray.cgColor
    }
    
    
    
    override func prepareForReuse() {
        super.prepareForReuse()
        print("Hello")
        tracker?.stop()
        tracker = nil

    }
}

extension CustomCollectionViewCell: ImpressionItem{
    func getUniqueId() -> String {
        return self.textLabel.text!
    }
}


extension CustomCollectionViewCell: TrackableView {
    func thresholdTimeInSeconds() -> Double { // every 2 seconds counts as a view.
        return 2
    }
    
    
    func viewDidStayOnViewPortForARound() {
        numberOfTimesTracked += 1 // counts for how long the view stays on screen.
    }
    
    
    
    func precondition() -> Bool {
        let screenRect = UIScreen.main.bounds
        let viewRect = convert(bounds,to: nil)
        let intersection = screenRect.intersection(viewRect)
        return intersection.height == bounds.height && intersection.width == bounds.width
    }
}

解决方法

可能想要使用的方法...

在您发布的代码中,您创建了一个“阅读帖子”数组:

var indexPathsOfCellsTurnedGreen = [IndexPath]() // All the read "posts"

假设您的真实数据将具有多个属性,例如:

struct TrackPost {
    var message: String = ""
    var postAuthor: String = ""
    var postDate: Date = Date()
    // ... other stuff
}

添加另一个属性来跟踪它是否被“看过”:

struct TrackPost {
    var message: String = ""
    var postAuthor: String = ""
    var postDate: Date = Date()
    // ... other stuff

    var hasBeenSeen: Bool = false
}

将所有“跟踪”代码移出控制器,而是将 Timer 添加到单元格类中。

当单元格出现时:

  • 如果该单元格的数据的 hasBeenSeenfalse
    • 启动 2 秒计时器
    • 如果计时器超时,单元格已经可见 2 秒,因此将 hasBeenSeen 设置为 true(使用闭包或协议/委托模式告诉控制器更新数据源)和改变背景颜色
    • 如果在计时器结束之前单元格在屏幕外滚动,请停止计时器并且不要告诉控制器任何事情
  • 如果 hasBeenSeen 开始时为 true,则不要启动 2 秒计时器

现在,您的 cellForItemAt 代码将如下所示:

    let p: TrackPost = myData[indexPath.row]
    
    customCell.authorLabel.text = p.postAuthor
    customCell.dateLabel.text = myDateFormat(p.postDate) // formatted as a string
    customCell.textLabel.text = p.message
    
    // setting hasBeenSeen in your cell should also set the backgroundColor
    //  and will be used so the cell knows whether or not to start the timer
    customCell.hasBeenSeen = p.hasBeenSeen
    
    // this will be called by the cell if the timer elapsed
    customCell.wasSeenCallback = { [weak self] in
        guard let self = self else { return }
        self.myData[indexPath.item].hasBeenSeen = true
    }
,

更简单的方法怎么样:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    for subview in collectionView!.visibleCells {
        if /* check visible percentage */ {
             if !(subview as! TrackableCollectionViewCell).timerRunning {
                  (subview as! TrackableCollectionViewCell).startTimer()
             }
        } else {
             if (subview as! TrackableCollectionViewCell).timerRunning {
                  (subview as! TrackableCollectionViewCell).stopTimer()
             }
        }
    }
}

通过以下方式扩展 Cell-Class:

class TrackableCollectionViewCell {
    
    static let minimumVisibleTime: TimeInterval = 2.0

    var timerRunning: Bool = true
    private var timer: Timer = Timer()

    func startTimer() {

        if timerRunning {
            return
        }

        timerRunning = true
        timer = Timer.scheduledTimer(withTimeInterval: minimumVisibleTime,repeats: false) { (_) in
            // mark cell as seen
        }
    }

    func stopTimer() {
         timerRunning = false
         timer.invalidate()
    }
    
}

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 <select id="xxx"> SELECT di.id, di.name, di.work_type, di.updated... <where> <if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 <property name="dynamic.classpath" value="tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams['font.sans-serif'] = ['SimHei'] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -> systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping("/hires") public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate<String
使用vite构建项目报错 C:\Users\ychen\work>npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-