如何解决CADisplayLink 计时器无法正常工作 |斯威夫特
我一直在用我的计时器,我的目标是知道用户看到帖子多长时间以将其计为展示。我的意思是,如果你观看一个事件超过 3 秒,它就会算作印象。
现在出于某种原因,计时器没有按我预期的那样工作,老实说,它接近于我想要的工作,这让我感到害怕,因为我已经接近解决方案了。我的问题是,有时处理 StalkCells 的 func 也会将显示时间不超过 3 秒的帖子标记为“印象”或计数。
这是我的代码:首先是我的 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,insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0,left: 20,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
}
}
}
解决方法
如果要创建显示链接,通常只需调用 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)
}
(因此,无需从视图向上导航到窗口再到屏幕。只需创建显示链接并将其添加到主运行循环中。如果您要保存对它的引用,我将其称为 displayLink
,而不是 timer
,以避免混淆。此外,我为该处理程序提供了一个名称和参数,使其目的不言而喻。)
但让我们把它放在一边。问题是您是否需要/想要使用显示链接。显示链接用于必须与屏幕刷新率最佳关联的计时器(例如,用于更新 UI 的计时器,例如动画、类似秒表的文本字段等)。
这是低效的,尤其是每个单元格都这样做。您正在为每个单元格触发一个单独的显示链接,每秒 60 次。如果您有 20 个可见的单元格,那么您的方法将每秒调用 1,200 次。相反,您可能每三秒钟每个单元只调用一次。例如,如果您想知道某个单元格是否已显示 3 秒钟,您可能只需:
- 在显示单元格时创建一个不重复的三秒
Timer
(例如willDisplay
); -
invalidate
单元格不再显示时的Timer
(例如在didEndDisplaying
中),以及 - 如果计时器处理程序触发,则表示该单元格显示了三秒钟。
但它是 3 秒后的单个计时器事件,而不是每个单元每秒调用 60 次。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。