Reputation: 1473
I have a timer that i am running from within a closure to help perform some ui update (progress bar). It works for the first item, but i want it to work for multiple items. So each time the closure is run a new instance of the timer is run and the new userInfo.
// dispatch and add run loop to get it to fire
DispatchQueue.main.async(execute: {
timer = Timer.init(timeInterval: 0.25, target: self, selector: #selector(self.downloadTimer(_:)), userInfo: [innerCell, collectionView, indexPath], repeats: true)
RunLoop.main.add(timer, forMode: RunLoopMode.defaultRunLoopMode)
})
When this function below is finished i want the timer to be recreated and pass new userInfo to the selector..
func downloadTimer(_ timer: Timer) {
// need to pass arguments to timer as array inside userInfo
// then create dictionary and extract
let dict = timer.userInfo as! NSArray
let cell = dict[0] as! InnerCollectionCell
let collectionView = dict[1] as! UICollectionView
let indexPath = dict[2] as! IndexPath
// the index path isnt getting reset
// without casting the progress bar in each cell was displaying when coming back to DownloadCollectionController
if let downloadingCell = collectionView.cellForItem(at: indexPath) as? InnerCollectionCell {
DispatchQueue.main.async(execute: {
downloadingCell.progressBar.isHidden = false
downloadingCell.contentView.bringSubview(toFront: cell.progressBar)
downloadingCell.progressBar.setProgress(downloadProgress, animated: true)
downloadingCell.setNeedsDisplay()
if downloadProgress >= 1.0 {
print("TIMER STOPPED")
downloadingCell.progressBar.isHidden = true
//downloadQueue.operations[0].cancel()
timer.invalidate()
collectionView.reloadItems(at: [indexPath])
}
})
}
}
The closure containing the timer = Timer is re-run but the new user info is not passed to the timer, it just keeps running with the original userInfo. If i invalidate the timer it can no longer be fired?
Ive tried creating it locally to the initial firing:
var timer = Timer()
timer = Timer.init(timeInterval: 0.25, target: self, selector: #selector(self.downloadTimer(_:)), userInfo: [innerCell, collectionView, indexPath], repeats: true)
But it doesn't recreate the timer and i want to be able to stop it if the user leaves the viewcontroller.
Is there a better way of achieving this? I know your not supposed to be able to retire a timer. there must be a way of creating a unique instance of it each time.
---- EDIT ----
This is what the index paths print as:
[0, 5]
[0, 5]
[0, 5]
[0, 5]
[0, 2]
[0, 5]
[0, 2]
[0, 1]
[0, 1]
[0, 1]
[0, 1]
So its taking a while to invalidate the timer and in that time the timer has been re-fired and the function is using the old index path until progress = 1
---- EDIT ----
The only place invalidation takes place is when progress >= 1
if downloadProgress >= 1.0 {
timer.invalidate()
downloadingCell.progressBar.isHidden = true
downloadingCell.progressBar.progress = 0
collectionView.reloadData()
}
Then because the timer is fired from cellForItemAt if the cell meets certain conditions, when collectionView.reloadData() is called the timer should be re-validated and the updated userInfo sent.
So this is the block inside cellForItemAt:
for operation in downloadQueue.operations {
if operation.name == self.multiPartArray[collectionView.tag][indexPath.item].name {
// edits for all queued operations
innerCell.spinner.isHidden = false
innerCell.spinner.startAnimating()
innerCell.contentView.bringSubview(toFront: innerCell.spinner)
innerCell.spinner.activityIndicatorViewStyle = .gray
innerCell.spinner.color = UIColor.darkGray
// hide the progress on all in queue it will be pushed to front when needed
innerCell.progressBar.isHidden = true
if operation.name == downloadQueue.operations[0].name {
// edits for the active downlaod in queue
// hide the spinner for currently downloading cell
// innerCell.spinner.isHidden = true
innerCell.spinner.isHidden = false
innerCell.spinner.startAnimating()
innerCell.contentView.bringSubview(toFront: innerCell.spinner)
innerCell.spinner.activityIndicatorViewStyle = .gray
innerCell.spinner.color = UIColor.blue
DispatchQueue.main.async(execute: {
let timer = Timer.init(timeInterval: 0.5, target: self, selector: #selector(self.downloadTimer(_:)), userInfo: [innerCell, collectionView, indexPath, collectionView.tag], repeats: true)
RunLoop.main.add(timer, forMode: RunLoopMode.defaultRunLoopMode)
})
break
} else {
// edits spinner that isnt currently downloading
innerCell.progressBar.isHidden = true
innerCell.spinner.color = UIColor.darkGray
}
} else {
// edits the spinner that isnt in queue
innerCell.progressBar.isHidden = true
innerCell.spinner.color = UIColor.darkGray
}
}
I realise its not the most elegant solution but I've got nested collection views that have probably not been set up entirely properly so ill settle for a work around in this case.
And the downloadTimer:
func downloadTimer(_ timer: Timer) {
// need to pass arguments to timer as array inside userInfo
// then create dictionary and extract
let dict = timer.userInfo as! NSArray
let cell = dict[0] as! InnerCollectionCell
let collectionView = dict[1] as! UICollectionView
let indexPath = dict[2] as! IndexPath
let tag = dict[3] as! Int
print(indexPath)
// without casting the progress bar in each cell was displaying when coming back to DownloadCollectionController
if let downloadingCell = collectionView.cellForItem(at: indexPath) as? InnerCollectionCell {
// self.outerCollectionView.reloadItems(at: [IndexPath(item: tag, section: 0)])
DispatchQueue.main.async(execute: {
downloadingCell.spinner.isHidden = true
downloadingCell.progressBar.isHidden = false
downloadingCell.contentView.bringSubview(toFront: cell.progressBar)
downloadingCell.progressBar.setProgress(downloadProgress, animated: true)
downloadingCell.setNeedsDisplay()
if downloadProgress >= 1.0 {
timer.invalidate()
downloadingCell.progressBar.isHidden = true
downloadingCell.progressBar.progress = 0
collectionView.reloadData()
}
})
}
}
Upvotes: 0
Views: 362
Reputation: 1735
Best way to schedule and use a time is like this:
//schedules timer at an 4 seconds interval + pass desired userInfo
var myTimer = Timer.scheduledTimer(timeInterval: 4.0
, target: self, selector: #selector(timerSelector), userInfo: nil, repeats: true)
//timer selector peform your actions there and
func timerSelector(dt:Timer){
}
//invalidate timer when needed like this
myTimer?.invalidate()
Also to reschedule the timer just call again Timer.scheduledTimer
Upvotes: 1