Reputation: 81
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
}
}
}
Upvotes: 1
Views: 1231
Reputation: 437407
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 (e.g. it’s for timers that update the UI, e.g. 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. E.g., if you want to know if a cell has been shown for 3 seconds, you might just:
Timer
when the cell is displayed (e.g. willDisplay
);invalidate
the Timer
when the cell is no longer shown (e.g. in didEndDisplaying
), andBut it’s a single timer event after 3 seconds, not calling it 60 times per second per cell.
Upvotes: 3