简体   繁体   中英

CADisplayLink Timer Not working proper | Swift

I've been stuck with my timer, my goal is to know for how long the user sees the post to count it as impression. what I mean is if you watch an event for more than 3 seconds it will count as Impression.

Now for some reason the timer works not as I expected and to be honest its close to work as I want to which freaks me out cause I'm close to the solution. My problem is that sometimes the func which takes care of StalkCells is also marking posts which are not displayed for long than 3 seconds as "Impression" or count.

Here's my Code: first my 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
    }
}

my Cell:

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

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

my ImpressionTracker:

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

If you want to create a display link, you would generally just call CADisplayLink(target:selector:) . See CADisplayLink documentation suggests that it would be something like:

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

(So, there's no need to navigate up from the view to the window to the screen. Just create your display link and add it to the main run loop. And if you're going to save a reference to it, I'd call it a displayLink , not timer , to avoid confusion. Also, I've give that handler a name and parameter that makes its purpose self-evident.)

But let's set that aside. The question is whether you need/want to use a display link at all. Display links are for timers that must be optimally tied to the screen refresh rate (eg it's for timers that update the UI, eg animations, stopwatch-like text fields, etc.).

That's inefficient, especially doing it every cell. You're firing off a separate display link for every cell, 60 times per second. If you had 20 cells visible, then your method would be called 1,200 times per second. Instead, you probably just one call per cell every three seconds. Eg, if you want to know if a cell has been shown for 3 seconds, you might just:

  • create a non-repeating, three-second Timer when the cell is displayed (eg willDisplay );
  • invalidate the Timer when the cell is no longer shown (eg in didEndDisplaying ), and
  • if the timer handler fires, that means the cell was shown for three seconds.

But it's a single timer event after 3 seconds, not calling it 60 times per second per cell.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM