CADisplayLink 计时器无法正常工作 | Swift

CADisplayLink Timer Not working proper | Swift

我一直在用我的计时器,我的目标是知道用户看到 post 多长时间才能将其计为展示。我的意思是,如果您观看一个事件超过 3 秒,它将被计为印象。

现在由于某种原因,计时器没有按我预期的那样工作,老实说,它接近于我想要的工作了,这让我很害怕,因为我接近解决方案了。我的问题是,有时负责 StalkCells 的函数也会将显示时间不超过 3 秒的 posts 标记为“印象”或计数。

这是我的代码:首先是我的 VC:

import UIKit

class ViewController: UIViewController,UIScrollViewDelegate {
    
    var impressionEventStalker: ImpressionStalker?
    var impressionTracker: ImpressionTracker?
    
    var indexPathsOfCellsTurnedGreen = [IndexPath]() // All the read "posts"
    
    var timer = Timer()
    
    @IBOutlet weak var collectionView: UICollectionView!{
        didSet{
            collectionView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
            impressionEventStalker = ImpressionStalker(minimumPercentageOfCell: 0.75, 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.tracker = ImpressionTracker(delegate: customCell)
//        print("Index: \(indexPath.row)")
            customCell.tracker?.start()
        
        
        customCell.textLabel.text = "\(indexPath.row)"
        customCell.subLabel.text  = "\(customCell.getVisibleTime())"
        
        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: UIScreen.main.bounds.width - 40, height: 325)
    }
    
    
    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:
    }
    
    
    
    
    func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        //Stop The Clock:
        (cell as? TrackableView)?.tracker?.stop()

    }
    
    
    func delayWithSeconds(_ seconds: Double, completion: @escaping () -> ()) {
        DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
            completion()
        }
    }
}


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


// MARK: - Custome Cell Class:
class CustomCollectionViewCell: UICollectionViewCell {
    var tracker: ViewTracker?
    var indexPath : IndexPath?
    
    
    static let nibName = "CustomCollectionViewCell"
    static let reuseIdentifier = "customCell"
    
    @IBOutlet weak var cellBackground: UIView!
    @IBOutlet weak var textLabel: UILabel!
    @IBOutlet weak var subLabel : UILabel!
    
    
    func setup(_ index: IndexPath) {
        self.indexPath = index
        tracker?.start()
    }
    
    var numberOfTimesTracked : Double = 0 {
        didSet {
            self.subLabel.text = "\(numberOfTimesTracked)"
            
        }
    }
    

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

    }
}


// MARK: - ImpressionItem Delegate Methods:
extension CustomCollectionViewCell: ImpressionItem{
    func getVisibleTime() -> Double {
        return numberOfTimesTracked
    }
    
    func getUniqueId() -> String {
        return self.textLabel.text!
    }
}




// MARK: - TrackableView Delegate Methods:

extension CustomCollectionViewCell: TrackableView {
    func thresholdTimeInSeconds() -> Double { // every 2 seconds counts as a view.
        return 1
    }
    
    
    func viewDidStayOnViewPortForARound() {
        numberOfTimesTracked = tracker?.getCurrTime() ?? 0 // counter for how long the cell stays on screen.
    }
    
    func precondition() -> Bool { // Checks when the cell is fully displayed so the timer can start.
        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
    }
}

我的 ImpressionStalker:

import Foundation
import UIKit

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

protocol ImpressionItem {
    func getUniqueId()->String
    func getVisibleTime() -> Double
}


class ImpressionStalker: NSObject {
    
    //MARK: Variables & Constants
    let minimumPercentageOfCell: CGFloat
    weak var collectionView: UICollectionView?
    
    static var alreadySentIdentifiers = [String]() // All the cells IDs
    weak var delegate: ImpressionStalkerDelegate?
    
    
    //MARK: - Initializer
    init(minimumPercentageOfCell: CGFloat, collectionView: UICollectionView, delegate:ImpressionStalkerDelegate ) {
        self.minimumPercentageOfCell = minimumPercentageOfCell
        self.collectionView = collectionView
        self.delegate = delegate
    }
    
    
    
    //MARK: - Class Methods:
    func stalkCells() {
        for cell in collectionView!.visibleCells {
            if let visibleCell = cell as? UICollectionViewCell & ImpressionItem {
                if visibleCell.getVisibleTime() >= 3 {
                    
                    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
                        }
                        
                        
                        print("%OfEachCell: \(visiblePercentOfCell) | CellID: \(visibleCell.getUniqueId()) | VisibleTime: \(visibleCell.getVisibleTime())")
                        delegate.sendEventForCell(atIndexPath: indexPath) // send the cell's index since its visible.
                        
                        ImpressionStalker.alreadySentIdentifiers.append(visibleCell.getUniqueId())
                        
                        //                    print(ImpressionStalker.alreadySentIdentifiers.count)
                    }
                }
            }
        }
        collectionView?.reloadData()
    }
    
    
    // Func Which Calculate the % Of Visible of each Cell:
    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()
    func getCurrTime() -> Double
    
}


final class ImpressionTracker: ViewTracker {
    
    func getCurrTime() -> Double {
        return numberOfTimesTracked
    }
    
    private weak var viewToTrack: TrackableView?
        
    private var timer: CADisplayLink?
    private var startedTimeStamp: CFTimeInterval = 0
    private var endTimeStamp: CFTimeInterval = 0
    var numberOfTimesTracked : Double = 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: .common)
        timer?.isPaused = true
    }
    
    
    func start() {
        guard viewToTrack != nil else { return }
        timer?.isPaused = false
        startedTimeStamp = CACurrentMediaTime()  // Startup Time
    }
    
    func pause() {
        guard viewToTrack != nil else { return }
        timer?.isPaused = true
        endTimeStamp = CACurrentMediaTime()
        print("Im paused!")
    }
    
    func stop() {
        timer?.isPaused = true
        timer?.invalidate()
        numberOfTimesTracked = 0
    }
    
    @objc func update() {
        guard let viewToTrack = viewToTrack else {
            stop()
            return
        }

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

        numberOfTimesTracked = endTimeStamp - startedTimeStamp
        
        endTimeStamp = CACurrentMediaTime()
        trackIfThresholdCrossed()
    }
    
    
    private func trackIfThresholdCrossed() {
       guard let viewToTrack = viewToTrack else { return }
      

        
        let elapsedTime = endTimeStamp - startedTimeStamp // total amount of passedTime.
        
        if  elapsedTime >= viewToTrack.thresholdTimeInSeconds() { // if its equal or greater than 1
//            print("ElapsedTime: \(elapsedTime) | numberOfTimesTracked: \(numberOfTimesTracked)")
            numberOfTimesTracked = Double(Int(elapsedTime))
            
            viewToTrack.viewDidStayOnViewPortForARound()

//            startedTimeStamp = endTimeStamp
        }
        
    }
}

如果要创建显示 link,通常只需调用 CADisplayLink(target:selector:)。看到 CADisplayLink documentation 表明它会是这样的:

weak var displayLink: CADisplayLink?

func createDisplayLink() {
    self.displayLink?.invalidate() // cancel prior one, if any
    
    let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
    displayLink.add(to: .main, forMode: .common) 
    self.displayLink = displayLink
}

@objc func handleDisplayLink(_ displayLink: CADisplayLink) {
    print(displayLink.timestamp) 
}

(因此,无需从视图向上导航到 window 到屏幕。只需创建显示 link 并将其添加到主 运行 循环中。如果您要保存对它的引用,我会称它为 displayLink,而不是 timer,以避免混淆。另外,我给该处理程序一个名称和参数,使它的目的不言而喻。)

但是让我们把它放在一边。问题是您 need/want 是否完全使用显示 link。 Display links 适用于必须以最佳方式绑定到屏幕刷新率的计时器(例如,它适用于更新 UI 的计时器,例如动画、类似秒表的文本字段等)。

那是低效的,尤其是每个单元格都这样做。您正在为每个单元格单独显示 link,每秒 60 次。如果您有 20 个可见单元格,那么您的方法每秒将被调用 1,200 次。相反,您可能每三秒对每个单元格进行一次调用。例如,如果您想知道某个单元格是否显示了 3 秒,您可以:

  • 在显示单元格时创建一个非重复的三秒 Timer(例如 willDisplay);
  • invalidate 当单元格不再显示时 Timer(例如在 didEndDisplaying 中),以及
  • 如果计时器处理程序触发,则意味着单元格显示了三秒钟。

但它是 3 秒后的单个计时器事件,而不是每个单元每秒调用它 60 次。