简体   繁体   English

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

[英]How to track a CollectionView cell by time in Swift

I've been working on a feature to detect when a user sees a post and when he doesn't.我一直在开发一项功能,以检测用户何时看到帖子以及何时没有。 When the user does see the post I turn the cell's background into green, when it doesn't then it stays red.当用户确实看到帖子时,我将单元格的背景变为绿色,如果没有,则保持红色。 Now after doing that I notice that I turn on all the cells into green even tho the user only scroll-down the page, so I added a timer but I couldn't understand how to use it right so I thought myself maybe you guys have a suggestion to me cause I'm kinda stuck with it for like two days:(现在,在这样做之后,我注意到即使用户只向下滚动页面,我也将所有单元格打开为绿色,所以我添加了一个计时器,但我不明白如何正确使用它,所以我想我自己也许你们有给我一个建议,因为我有点坚持了两天:(

  • Edit: Forgot to mention that a cell marks as seen if it passes the minimum length which is 2 seconds.编辑:忘了提到一个单元格标记为是否通过了最小长度(即 2 秒)。

Here's my Code: My VC(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
    }
}

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

ImpressionTracker:印象追踪器:

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

my customCell:我的自定义单元:

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

The approach you probably want to use...可能想要使用的方法...

In you posted code, you've created an array of "read posts":在您发布的代码中,您创建了一个“阅读帖子”数组:

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

Assuming your real data will have multiple properties, such as:假设您的真实数据将具有多个属性,例如:

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

add another property to track whether or not it has been "seen":添加另一个属性以跟踪它是否已“看到”:

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

    var hasBeenSeen: Bool = false
}

Move all of your "tracking" code out of the controller, and instead add a Timer to your cell class.将所有“跟踪”代码移出 controller,而是将计时器添加到您的单元 class。

When the cell appears:当单元格出现时:

  • if hasBeenSeen for that cell's Data is false如果该单元格的数据的hasBeenSeenfalse
    • start a 2-second timer启动 2 秒计时器
    • if the timer elapses, the cell has been visible for 2 seconds, so set hasBeenSeen to true (use a closure or protocol / delegate pattern to tell the controller to update the data source) and change the backgroundColor如果计时器到时,单元格已可见 2 秒,因此将hasBeenSeen设置为true (使用闭包或协议/委托模式告诉 controller 更新数据源)并更改背景颜色
    • if the cell is scrolled off-screen before the timer elapses, stop the timer and don't tell the controller anything如果在计时器结束之前单元格滚动到屏幕外,请停止计时器并且不要告诉 controller 任何事情
  • if hasBeenSeen is true to begin with, don't start the 2-second timer如果hasBeenSeen一开始就为true ,则不要启动 2 秒计时器

Now, your cellForItemAt code will look something like this:现在,您的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
    }

What about a much simpler approach like:一个更简单的方法怎么样:

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

With a Cell-Class extended by:通过以下方式扩展单元类:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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