【问题标题】:How to manage multiple timers in one viewController?如何在一个 viewController 中管理多个计时器?
【发布时间】:2020-05-12 11:00:01
【问题描述】:

我正在开发计时器应用程序,但无法理解如何为每个单元格设置多个计时器。

我在 didSelectRowAt 处启动和暂停计时器:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)
    let cell = tableView.cellForRow(at: indexPath) as! TimerTableViewCell
    let item = timers.items[indexPath.row]
    item.toggle()
    print(item)
    startPauseTimer(for: cell, with: item)
}

这是我的 startPauseTimer 代码:

   var timer = Timer()

    func startPauseTimer(for cell: TimerTableViewCell, with item: Timers) {
       if !item.isStarted {
           timer.invalidate()
           } else {    
           timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {timer in
                item.seconds -= 1
                cell.timerTime.text = self.formattedTime(time: TimeInterval(item.seconds))

               if item.seconds < 1 {
                   self.timer.invalidate()
                   cell.timerTime.text = self.formattedTime(time: TimeInterval(item.seconds))
                   item.isStarted = false
               }
        }
       }
    }

还有我的数据模型:

class Timers: NSObject, Codable {
  var name = ""
  var id = ""
  var seconds = 0
  var editSeconds = 0
  var isStarted = false

  func toggle() {
        isStarted = !isStarted
    }

任何我的手机代码:

class TimerTableViewCell: UITableViewCell {

    @IBOutlet var timerLabel: UILabel!
    @IBOutlet var timerTime: UILabel
    @IBOutlet var startPauseButton: UIButton!
    @IBOutlet var resetButton: UIButton!    

    }

如何同时管理多个计时器?当我使用 didSelectRowAt 时,只有同一个 Timer() 实例正在触发,因此多个计时器正在混合。如何划分多个计时器并使其工作?

【问题讨论】:

  • 你能添加你的 TimerTableViewCell 定义吗?
  • 我添加了,但它只有网点。
  • @alexsmith - 请不要发布同一问题的多个版本。编辑这个,并尝试更好地解释您正在尝试做什么,而不是发布“不起作用*”的部分代码。“计时器应用程序”是什么意思 ?您要完成什么?您的基本方法可能是错误的,因此您在此问题的多个帖子中获得的多个 cmets / 答案可能与解决开始的基本方法无关。
  • @alexsmith - 看看这个教程:raywenderlich.com/113835-ios-timer-tutorial ... 做了非常小的改动,看起来它可以做你想做的事。

标签: ios swift uitableview timer


【解决方案1】:

这是一个完整的示例,基于 RayWenderlich.com 上的Fabrizio BrancatiiOS Timer Tutorial

一切都通过代码完成(不需要@IBOutlet@IBAction 连接),因此只需创建一个新的UITableViewController 并将其自定义类分配给ExampleTableViewController


ExampleTableViewController.swift

//
//  ExampleTableViewController.swift
//  MultipleTimers
//
//  Created by Don Mag on 5/12/20.
//

import UIKit

class ExampleTableViewController: UITableViewController {

    let cellID: String = "TaskCell"

    var taskList: [Task] = []
    var timer: Timer?

    override func viewDidLoad() {
        super.viewDidLoad()

        // -1 means use default 2-hours
        let sampleData: [(String, Double)] = [
            ("First (2 hours)", -1),
            ("Second (2 hours)", -1),
            ("Third (10 seconds)", 10),
            ("Fourth (30 seconds)", 30),
            ("Fifth (1 hour 10 minutes)", 60 * 70),
            ("Sixth (2 hours)", -1),
            ("Seventh (45 minutes)", 60 * 45),
            ("Eighth (2 hours)", -1),
            ("Ninth (1 hour 10 minutes)", 60 * 70),
            ("Tenth (2 hours)", -1),
            ("Eleventh (45 minutes)", 60 * 45),
            ("Thirteenth (2 hours)", -1),
            ("Fourteenth (2 minutes)", 60 * 2),
            ("Fifthteenth (11 minutes)", 60 * 11),
            ("Sixteenth (2 hours)", -1),
        ]

        sampleData.forEach { (s, t) in
            let task = Task(name: s, targetTime: t)
            self.taskList.append(task)
        }

        tableView.register(TaskCell.self, forCellReuseIdentifier: cellID)

        createTimer()
    }

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return taskList.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as! TaskCell

        cell.task = taskList[indexPath.row]

        return cell
    }

}

// MARK: - Timer
extension ExampleTableViewController {
    func createTimer() {
        if timer == nil {
            let timer = Timer(timeInterval: 1.0,
                              target: self,
                              selector: #selector(updateTimer),
                              userInfo: nil,
                              repeats: true)
            RunLoop.current.add(timer, forMode: .common)
            timer.tolerance = 0.1

            self.timer = timer
        }
    }

    func cancelTimer() {
        timer?.invalidate()
        timer = nil
    }

    @objc func updateTimer() {
        guard let visibleRowsIndexPaths = tableView.indexPathsForVisibleRows else {
            return
        }

        for indexPath in visibleRowsIndexPaths {
            if let cell = tableView.cellForRow(at: indexPath) as? TaskCell {
                cell.updateTime()
            }
        }
    }
}

TaskCell.swift

//
//  TaskCell.swift
//  MultipleTimers
//
//  Created by Don Mag on 5/12/20.
//

import UIKit

class TaskCell: UITableViewCell {

    let taskNameLabel: UILabel = {
        let v = UILabel()
        v.textAlignment = .center
        return v
    }()

    let timerLabel: UILabel = {
        let v = UILabel()
        v.textAlignment = .center
        v.font = UIFont.monospacedDigitSystemFont(ofSize: 17.0, weight: .medium)
        return v
    }()

    let actionButton: UIButton = {
        let v = UIButton()
        v.setTitle("Start", for: [])
        v.setTitleColor(.lightGray, for: .highlighted)
        v.setTitleColor(.darkGray, for: .disabled)
        v.backgroundColor = UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0)
        return v
    }()

    let resetButton: UIButton = {
        let v = UIButton()
        v.setTitle("Reset", for: [])
        v.setTitleColor(.lightGray, for: .highlighted)
        v.setTitleColor(.darkGray, for: .disabled)
        v.backgroundColor = .red
        return v
    }()

    let buttonStack: UIStackView = {
        let v = UIStackView()
        v.axis = .horizontal
        v.distribution = .fillEqually
        v.spacing = 16
        return v
    }()

    var task: Task? {
        didSet {
            taskNameLabel.text = task?.name
            timerLabel.text = "0"
            setState()
            updateTime()
        }
    }

    func setState() -> Void {

        switch task?.state {
        case .running:
            actionButton.setTitle("Pause", for: [])
            actionButton.isEnabled = true
        case .paused:
            if task?.elapsedTime == 0 {
                actionButton.setTitle("Start", for: [])
                actionButton.isEnabled = true
            } else {
                actionButton.setTitle("Resume", for: [])
                actionButton.isEnabled = true
            }
        default: // .completed
            actionButton.setTitle("", for: [])
            actionButton.isEnabled = false
        }

    }

    func updateTime() {
        guard let task = task else {
            return
        }

        var t: Double = 0
        if task.state == .paused {
            t = task.targetTime - task.elapsedTime
        } else {
            t = task.targetTime - (Date().timeIntervalSince(task.creationDate) + task.elapsedTime)
        }

        let tm = Int(max(t, 0))

        let hours = tm / 3600
        let minutes = tm / 60 % 60
        let seconds = tm % 60

        let s = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
        timerLabel.text = s
        timerLabel.textColor = tm > 0 ? .black : .red
        if tm == 0 {
            task.state = .completed
            setState()
        }
    }

    @objc
    func buttonTapped(_ sender: UIButton) -> Void {
        guard let s = sender.currentTitle, let task = task else { return }
        switch s {

        case "Start", "Resume":
            task.state = .running
            task.creationDate = Date()

        case "Pause":
            task.state = .paused
            task.elapsedTime += Date().timeIntervalSince(task.creationDate)

        case "Reset":
            task.state = .paused
            task.elapsedTime = 0

        default:
            break
        }
        setState()
    }

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    func commonInit() -> Void {

        [buttonStack, resetButton, actionButton, timerLabel, taskNameLabel].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
        }

        buttonStack.addArrangedSubview(actionButton)
        buttonStack.addArrangedSubview(resetButton)

        contentView.addSubview(buttonStack)
        contentView.addSubview(taskNameLabel)
        contentView.addSubview(timerLabel)

        let g = contentView.layoutMarginsGuide

        NSLayoutConstraint.activate([

            buttonStack.topAnchor.constraint(equalTo: g.topAnchor),
            buttonStack.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            buttonStack.widthAnchor.constraint(equalToConstant: 280.0),

            taskNameLabel.topAnchor.constraint(equalTo: buttonStack.bottomAnchor, constant: 8.0),
            taskNameLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            taskNameLabel.widthAnchor.constraint(equalTo: buttonStack.widthAnchor),

            timerLabel.topAnchor.constraint(equalTo: taskNameLabel.bottomAnchor, constant: 8.0),
            timerLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            timerLabel.widthAnchor.constraint(equalTo: buttonStack.widthAnchor),

            timerLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),

        ])

        actionButton.addTarget(self, action: #selector(self.buttonTapped(_:)), for: .touchUpInside)
        resetButton.addTarget(self, action: #selector(self.buttonTapped(_:)), for: .touchUpInside)

    }

}

Task.swift

//
//  Task.swift
//  MultipleTimers
//
//  Created by Don Mag on 5/12/20.
//

import Foundation

enum TimerState {
    case paused, running, completed
}

class Task {
    let name: String
    var creationDate = Date()
    var elapsedTime: Double = 0
    var state: TimerState = .paused
    var targetTime: Double = 60 * 60 * 2 // default 2 hours

    init(name: String, targetTime: Double) {
        self.name = name
        if targetTime != -1 {
            self.targetTime = targetTime
        }
    }
}

这是它在运行时的样子:

【讨论】:

  • 非常感谢!
【解决方案2】:

嗯,只保留了一个 Timer 实例,所以当你不想要它时,可以用其他计时器替换它。

最好将行而不是单元格对象传递给startPauseTimer,因为单元格通常会被重用。然后您可以通过func cellForRow(at indexPath: IndexPath) -&gt; UITableViewCell? 处理所需的单元格并更改其文本。

让我们创建TimerModel 类:

class TimerModel {
    let timer: Timers
    var actualTimer: Timer?

    init(_ timer: Timers) {
        self.timer = timer
        self.actualTimer = nil
    }
}

那么假设你有timers = [TimerModel(Timers()), TimerModel(Timers())]

选择一行:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)
    let cell = tableView.cellForRow(at: indexPath) as! TimerTableViewCell
    let item = timers[indexPath.row]
    item.timer.toggle()
    print(item)
    startPauseTimer(for: indexPath.row)
}

开始暂停:

func startPauseTimer(for row: Int) {
    let item = self.timers[row].timer
    if !item.isStarted {
        self.timers[row].actualTimer?.invalidate()
    } else {
        self.timers[row].actualTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {[weak self] timer in
            guard let self = self else { return }
            if let cell =  self.tableView.cellForRow(at: IndexPath(row: row, section: 0)) {
                item.seconds -= 1
                cell.textLabel?.text = "\(TimeInterval(item.seconds))"

                if item.seconds < 1 {
                    self.timers[row].actualTimer?.invalidate()
                    cell.textLabel?.text = "\(TimeInterval(item.seconds))"
                    item.isStarted = false
                }
            }
        }
    }
}

如果您想删除一行(在用户或以编程方式删除行时调用):

func onRemove(at row: Int) {
    timers[row].actualTimer?.invalidate()
    timers[row].actualTimer = nil
    timers.remove(at: row)
}

有关编辑 UITableView 的信息,请参阅苹果文档: https://developer.apple.com/documentation/uikit/uitableview

  • 将表格置于编辑模式如果您想通过与单元格交互进行编辑
  • 如果您想以编程方式编辑行,插入、删除和移动行和部分

【讨论】:

  • 我无法将 var timer 移动到 Timers 类,因为它不符合 Codable 协议。
  • 即使有一个计时器数组也无济于事,因为跟踪项目列表的修改会很乏味。即当添加/删除一个项目时,也应该添加/删除相应的计时器。
  • 它会起作用的。为了方便起见,它可以包装在其他类中。
  • 能否提供示例代码?如何在数组中添加和删除计时器?以及如何将其包装在其他类中?
  • @alexsmith,好的,请参阅最新的编辑。这应该说明方法。
猜你喜欢
  • 1970-01-01
  • 2023-04-07
  • 2015-03-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-17
相关资源
最近更新 更多