表格视图单元格中的倒数计时器在滚动后显示不同的值

Posted

技术标签:

【中文标题】表格视图单元格中的倒数计时器在滚动后显示不同的值【英文标题】:Countdown timer in table view cell shows different values after scrolling 【发布时间】:2018-03-23 08:40:30 【问题描述】:

问题在标题中有所描述,但更具体地说,这里是全貌。

我有一个自定义表格视图单元子类,其中包含显示倒数计时器的标签。当有一小部分计时器时它工作正常,但是有很多数据我需要显示远远超出可见单元格的计时器,当我快速向下滚动然后快速向上滚动时,单元格中的计时器值开始显示不同的值,直到某个时间点,然后显示正确的值。

我为这些可重复使用的单元尝试了不同的变体,但我找不到问题。需要帮助!!!

这是逻辑的实现代码。

自定义单元子类:

let calendar = Calendar.current
var timer: Timer?

var deadlineDate: Date? 
    didSet 
        updateTimeLabel()
    


override func awakeFromNib() 

    purchaseCellCardView.layer.cornerRadius = 10

    let selectedView = UIView(frame: CGRect.zero)
    selectedView.backgroundColor = UIColor.clear
    selectedBackgroundView = selectedView


override func prepareForReuse() 
    super.prepareForReuse()
    if timer != nil 
        print("Invalidated!")
        timer?.invalidate()
        timer = nil
    


func configure(for purchase: Purchase) 
    purchaseSubjectLabel.text = purchase.subject
    startingPriceLabel.text = purchase.NMC
    stageLabel.text = purchase.stage
    fzImageView.image = purchase.fedLaw.contains("44") ? #imageLiteral(resourceName: "FZ44") : #imageLiteral(resourceName: "FZ223")
    timeLabel.isHidden = purchase.stage == "Работа комиссии"
    warningImageView.image = purchase.warningImage


func updateTimeLabel() 
    setTimeLeft()
    timer = Timer(timeInterval: 1, repeats: true)  [weak self] _ in
        guard let strongSelf = self else return
        strongSelf.setTimeLeft()
    
    RunLoop.current.add(timer!, forMode: .commonModes)


@objc private func setTimeLeft() 

    let currentDate = getCurrentLocalDate()

    if deadlineDate?.compare(currentDate) == .orderedDescending 

        var components = calendar.dateComponents([.day, .hour, .minute, .second], from: currentDate, to: deadlineDate!)

        let dayText = (components.day! == 0 || components.day! < 0) ? "" : String(format: "%i", components.day!)
        let hourText = (components.hour == 0 || components.hour! < 0) ? "" : String(format: "%i", components.hour!)

        switch (dayText, hourText) 
        case ("", ""):
            timeLabel.text = String(format: "%02i", components.minute!) + ":" + String(format: "%02i", components.second!)
        case ("", _):
            timeLabel.text = hourText + " ч."
        default:
            timeLabel.text = dayText + " дн."
        
     else 
        stageLabel.text = "Работа комиссии"
        timeLabel.text = ""
        timeLabel.isHidden = true
        timer?.invalidate()
    


private func getCurrentLocalDate() -> Date 
    var now = Date()
    var nowComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: now)
    nowComponents.timeZone = TimeZone(abbreviation: "UTC")
    now = calendar.date(from: nowComponents)!
    return now


deinit 
    print("DESTROYED")
    timer?.invalidate()
    timer = nil

tableView(_cellForRowAt:)最重要的部分

case .results:
        if filteredArrayOfPurchases.isEmpty 

            let cell = tableView.dequeueReusableCell(
                withIdentifier: TableViewCellIdentifiers.nothingFoundCell,
                for: indexPath)

            let label = cell.viewWithTag(110) as! UILabel

            switch segmentedControl.index 
            case 1:
                label.text = "Нет закупок способом\n«Запрос предложений»"
            case 2:
                label.text = "Нет закупок способом\n«Конкурс»"
            case 3:
                label.text = "Нет закупок способом\n«Аукцион»"
            default:
                label.text = "Нет закупок способом\n«Запрос котировок»"
            
            return cell
         else 
            let cell = tableView.dequeueReusableCell(
                withIdentifier: TableViewCellIdentifiers.purchaseCell,
                for: indexPath) as! PurchaseCell

            cell.containerViewTopConstraint.constant = indexPath.row == 0 ? 8.0 : 4.0
            cell.containerViewBottomConstraint.constant = indexPath.row == filteredArrayOfPurchases.count - 1 ? 8.0 : 4.0

            let purchase = filteredArrayOfPurchases[indexPath.row]

            cell.configure(for: purchase)

            if cell.timer != nil 
                cell.updateTimeLabel()
             else 
                search.getDeadlineDateAndTimeToApply(purchase.purchaseURL, purchase.fedLaw, purchase.stage, completion:  (date) in
                    cell.deadlineDate = date
                )
            
            return cell
        

最后一块拼图:

func getDeadlineDateAndTimeToApply(_ url: URL?, _ fedLaw: String, _ stage: String, completion: @escaping (Date) -> ()) 

    var deadlineDateAndTimeToApply = Date()

    guard stage != "Работа комиссии" else  return 

    if let url = url 
        dataTask = URLSession.shared.dataTask(with: url, completionHandler:  (data, response, error) in
            if let error = error as NSError?, error.code == -403 
                // TODO: Add alert here
                return
            
            guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data, let html = String(data: data, encoding: .utf8), let purchasePageBody = try? SwiftSoup.parse(html), let purchaseCard = try? purchasePageBody.select("td").array() else return

            let mappedArray = purchaseCard.map()String(describing: $0)

            if fedLaw.contains("44") 
                guard let deadlineDateToApplyString = try? purchaseCard[(mappedArray.index(of: "<td class=\"fontBoldTextTd\">Дата и время окончания подачи заявок</td>"))! + 1].text().components(separatedBy: " ") else return

                dateFormatter.dateFormat = "dd.MM.yyyy HH:mm"
                let deadlineDateToApply = deadlineDateToApplyString.first!
                let deadlineTimeToApply = deadlineDateToApplyString[1]

                guard let deadlineDateAndTimeToApplyCandidate = dateFormatter.date(from: "\(deadlineDateToApply) \(deadlineTimeToApply)") else return

                deadlineDateAndTimeToApply = deadlineDateAndTimeToApplyCandidate
             else 
                guard let deadlineDateToApplyString = try? purchaseCard[(mappedArray.index(of: "<td>Дата и время окончания подачи заявок<br> (по местному времени заказчика)</td>"))! + 1].text().components(separatedBy: " ") else return

                dateFormatter.dateFormat = "dd.MM.yyyy HH:mm"
                let deadlineDateToApply = deadlineDateToApplyString.first!
                let deadlineTimeToApply = deadlineDateToApplyString[2]

                guard let deadlineDateAndTimeToApplyCandidate = dateFormatter.date(from: "\(deadlineDateToApply) \(deadlineTimeToApply)") else return

                deadlineDateAndTimeToApply = deadlineDateAndTimeToApplyCandidate
            
            DispatchQueue.main.async 
                completion(deadlineDateAndTimeToApply)
            
        )
        dataTask?.resume()
    

几点说明:

    尝试在prepareForReuse() 中将deadlineDate 重置为nil - 没有帮助; 使用 SwiftSoup 框架解析 HTML,如果重要的话,您可以在最后一个代码示例中看到。

【问题讨论】:

不要在单元格中使用计时器。在您的视图控制器中使用单个计时器并使用它来更新您的模型。看到这个答案***.com/questions/49246036/… 我不明白这如何适用于我通过 Async dataTask 从网络获取截止日期的情况?我理解这个概念,但是我不知道如何在我的情况下实现它???? 您需要关注点分离。从 Internet 获取数据并将其放入数据模型中是一回事。在表格视图中显示它是另一回事。这两个答案都有一些代码显示如何更新表格视图单元格中的时间。这是一个简单的示例,但使用您从某处加载的截止日期数组而不是示例中的开始日期数组并不难。您不应在cellForRowAt 中启动异步数据传输。您应该已经获取了数据。 【参考方案1】:

这是相当多的代码,但从您所描述的问题来看,您的问题在于重用单元格。

您最好将计时器从单元中分离出来,并将它们放入您的对象中。这是他们所属的地方(或在某些管理器中,如视图控制器)。想象一下有这样的东西:

class MyObject 
    var timeLeft: TimeInterval = 0.0 
        didSet 
            if timeLeft > 0.0 && timer == nil 
                timer = Timer.scheduled...
             else if timeLeft <= 0.0, let timer = timer 
                timer.invalidate()
                self.timer = nil
            
            delegate?.myObject(self, updatedTimeLeft: timeLeft)
        
    

    weak var delegate: MyObjectDelegate?
    private var timer: Timer?


现在您只需要在索引路径中的行单元格来分配您的对象:cell.myObject = myObjects[indexPath.row]

你的细胞会做这样的事情:

var myObject: MyObject? 
    didSet 
        if oldValue.delegate == self 
            oldValue.delegate = nil // detach from previous item
        
        myObject.delegate = self
        refreshUI()
    



func myObject(_ sender: MyObject, updatedTimeLeft timeLeft: TimeInterval) 
    refreshUI()

我相信其余的应该非常简单......

【讨论】:

我会尝试使用您的解决方案并尽快报告。抱歉这么多代码,但我想给出一个完整的画面 如果我从完成处理程序中的 URLSession dataTask 异步获取截止日期,有什么特别需要实现的吗? @GregoryKovler 可能不会。我假设在完成处理程序中你调用类似self.myObjects = retrievedObjectsself.tableView.reloadData()【参考方案2】:

你的问题在这里:

search.getDeadlineDateAndTimeToApply(purchase.purchaseURL,
                                     purchase.fedLaw,
                                     purchase.stage,
                                     completion:  (date) in
                cell.deadlineDate = date
            )

getDeadlineDateAndTimeToApply 异步运行,计算一些东西,然后在主线程中更新cell.deadlineData(这很好)。但与此同时,在计算某些内容时,用户可能已经上下滚动,cell 可能已被重新用于另一行,现在更新错误地更新了cell。 您需要做的是:不要直接存储UITableViewCell。相反,跟踪要更新的IndexPath,一旦计算完成,检索属于该IndexPath 的单元格并更新它。

【讨论】:

我试图将逻辑移动到数据模型类,但似乎异步任务执行不允许我在单元格加载时显示计时器,只有在我重新加载表视图之后。不知道该怎么办,但正如@MaticOblak 指出的那样,它似乎更可靠。 并且可能会这么好,给我看代码示例。恐怕我不明白你的建议……你是建议只更新可见单元格吗? 换句话说,在我的情况下完成计算并将计时器直接移动到模型时,如何跟踪要更新的行? 关注@MaticOblak 并从单元格中删除计时器。单元格是 UI 元素,它几乎总是应该是哑的,从不创建计时器、访问 Web 服务或数据库,仅举几例。这应该由控制器完成:跟踪模型,设置计时器并将它们映射到模型中的IndexPath/ 行;计时器完成后,使用 IndexPath 抓取单元格(如果仍显示)并更新其中的值。 好的,我会尝试报告结果

以上是关于表格视图单元格中的倒数计时器在滚动后显示不同的值的主要内容,如果未能解决你的问题,请参考以下文章

更新表格视图单元格中的核心数据值后如何刷新

滚动时 UITableViewCell 内容会发生变化,因为在其中一个单元格中运行计时器

核心数据:如何显示 fetchrequest 的结果以计算表格视图单元格中的值

如何在表格视图单元格中快速迭代for循环

检查表格视图单元格中的部分索引标题

单元格中的 UITableView 动画导致平滑滚动中断