Пара вещей:
Я бы не советовал использовать вызовы вашего обработчика таймера для
timeRemaining = timeRemaining - 1
Я бы вместо этого сохранил nextRefreshTime
ирассчитайте время, прошедшее между текущим временем и nextRefreshTime
, чтобы отобразить оставшееся время.
Обратите внимание, что когда вы вызываете scheduledTimer
, это добавляет его в цикл выполнения для вас, поэтомувам не нужно также добавлять его в цикл выполнения самостоятельно.
Или, если вы хотите использовать .commonModes
в цикле выполнения, чтобы позволить таймеру метки продолжать работу во время прокрутки, просто создайте Timer
с помощью init
, а затем добавьте его в цикл выполнения.
Но нет смысла снова использовать scheduleTimer
и add
в цикле выполнения.
Я бы вышел из дела подсчета минут и секунд самостоятельно.Вы можете использовать DateComponentsFormatter
для отображения оставшегося времени.
Вы говорили о запуске таймера в viewDidLoad
.Но UICollectionViewCell
не имеет такого метода (и если вы создадите одно из этих имен, он не будет его использовать).Вместо этого вызовите некоторый метод для настройки ячейки в cellForItemAt
.
Объекты модели, например, когда будет происходить следующее обновление данных, не принадлежат ячейке.Ячейки - это временные объекты UIKit, которые будут входить и выходить из памяти при прокрутке, поэтому не следует отслеживать это.Для этого у вас должна быть своя собственная модель, и контроллер представления отвечает за облегчение передачи этой информации в ячейку при представлении ячейки:
- первоначальная настройка ячейки с помощью
nextRefreshTime
;и - отправка некоторых уведомлений при изменении
nextRefreshTime
.
Затем ячейка:
- добавит себя в качестве наблюдателя к этому уведомлению, обновляя свои
nextRefreshTime
; - всякий раз, когда это уведомление отменяет любоепредыдущий таймер обновления метки, если он есть, и запуск нового при необходимости;
- Этот обработчик обновления метки должен обновить метку
Таким образом:
class ViewController: UIViewController {
@IBOutlet weak var collectionView: UICollectionView!
static let resetTimerNotification = Notification.Name("someuniqueidentifier")
// this handles the data refreshes (or whatever), say every two minutes, or whatever
private var nextRefreshTime: Date? {
didSet {
NotificationCenter.default.post(name: ViewController.resetTimerNotification, object: nextRefreshTime)
refreshDataTimer?.invalidate()
if let when = nextRefreshTime {
refreshDataTimer = Timer.scheduledTimer(withTimeInterval: when.timeIntervalSince(Date()), repeats: false) { [weak self] _ in
print("timer fired")
self?.resetTimer() // presumably, defer this until after the data refresh is done
}
}
}
}
private weak var refreshDataTimer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.register(UINib(nibName: "TimeRemainingCell", bundle: nil), forCellWithReuseIdentifier: "TimeRemaining")
resetTimer()
}
@IBAction func didTapResetButton(_ sender: Any) {
resetTimer()
}
private func resetTimer() {
nextRefreshTime = Date().addingTimeInterval(120) // or however you want to do this
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// lets imagine that cell 0 is the TimeRemainingCell
if indexPath.item == 0 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TimeRemaining", for: indexPath) as! TimeRemainingCell
cell.configure(for: nextRefreshTime)
return cell
}
... configure other types of cells
}
}
и
class TimeRemainingCell: UICollectionViewCell {
// MARK: - Properties
@IBOutlet weak var timeRemainingLabel: UILabel!
private var nextRefreshTime: Date? {
didSet {
labelUpdateTimer?.invalidate()
if nextRefreshTime != nil {
let timer = Timer(fire: nextRefreshTime!, interval: 0, repeats: false) { [weak self] timer in
// note, if cell is deallocated for any reason, let's stop the timer
guard let strongSelf = self else {
timer.invalidate()
return
}
strongSelf.updateLabel()
}
RunLoop.current.add(timer, forMode: .commonModes)
labelUpdateTimer = timer
}
}
}
private weak var labelUpdateTimer: Timer?
/// Formatter for time remaining
///
/// Note, this gets us out of manually calculating minutes and seconds remaining
private static let formatter: DateComponentsFormatter = {
let _formatter = DateComponentsFormatter()
_formatter.unitsStyle = .positional
_formatter.allowedUnits = [.minute, .second]
_formatter.zeroFormattingBehavior = .pad
return _formatter
}()
// MARK: - init/deinit methods
override init(frame: CGRect) {
super.init(frame: frame)
addNotificationObserver()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addNotificationObserver()
}
deinit {
labelUpdateTimer?.invalidate()
}
// MARK: - Configuration methods
/// Add notification observer
private func addNotificationObserver() {
NotificationCenter.default.addObserver(forName: ViewController.resetTimerNotification, object: nil, queue: .main) { [weak self] notification in
self?.nextRefreshTime = notification.object as? Date
self?.updateLabel()
}
}
/// Configure the cell for your model object.
///
/// Called by collectionView(_:cellForItemAt:).
///
/// Also starts the refresh timer.
///
/// - Parameter object: Your model object
func configure(for nextRefreshTime: Date?) {
self.nextRefreshTime = nextRefreshTime
}
// MARK: - Label updating
private func updateLabel() {
let now = Date()
if let when = nextRefreshTime {
timeRemainingLabel.text = TimeRemainingCell.formatter.string(from: now, to: when)
} else {
timeRemainingLabel.text = "No time remaining"
}
}
}