首页 > 解决方案 > 如何在 Swift 中按时间跟踪 CollectionView 单元格

问题描述

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

这是我的代码:我的 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, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 0, left: 20, bottom: 0, 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
    }
}

我的印象追踪者:

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
    }
}

标签: iosswiftanalyticsnstimercollectionview

解决方案


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

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

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 添加到您的单元格类中。

当单元格出现时:

  • 如果hasBeenSeen该单元格的数据是false
    • 启动 2 秒计时器
    • 如果计时器到时,单元格已经可见 2 秒,因此设置hasBeenSeentrue(使用闭包或协议/委托模式来告诉控制器更新数据源)并更改背景颜色
    • 如果在计时器结束之前单元格被滚动到屏幕外,请停止计时器并且不要告诉控制器任何事情
  • 如果hasBeenSeentrue开始,不要启动 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
    }

推荐阅读