滚动

时间:2018-03-23 08:40:30

标签: ios swift uitableview timer

问题在标题中有描述,但更具体地说这是一个完整的图片。

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

我为那些可重复使用的单元尝试了不同的变体,但我无法发现问题。需要帮助!!!

这是逻辑实现的代码。

自定义单元格子类:

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()
    }
}

一些注意事项:

  1. 尝试在prepareForReuse()中将deadlineDate重置为nil - 没有帮助;
  2. 使用SwiftSoup Framework解析HTML,如上所述,如上所述,可以看到。

2 个答案:

答案 0 :(得分:0)

你的问题在这里:

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

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

答案 1 :(得分:0)

这是相当多的代码,但是根据您所描述的问题,重复使用单元格。

您最好将定时器从单元格中分离出来并将它们放在对象中。它是它们所属的地方(或者像某个管理器一样的视图控制器)。想象一下如下所示:

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()
}

我相信其余的应该是非常直接的......