多個cell中展示倒數計時,本地和伺服器時間差異解決方案

YasinZhou發表於2017-09-21

本文借鑑了IGListKit中多cell通知方案

Demo下載

公司需要做限時搶購的業務,這裡面有兩個需求點:
1.在多個cell中顯示倒數計時
在每個cell中新增定時器是不現實的,必定會增加許多效能開銷,所以肯定是使用一個定時器,關鍵在於如何通知到cell重新整理UI
2.本地時間可能和伺服器時間存在誤差
有的手機可能時間沒有和網路同步,或者使用者故意調整了時間,所以本地時間存在錯誤的可能,所以就定下使用伺服器時間

整體思路

1.在多個cell中顯示倒數計時

思路是這樣的:將需要接收定時器通知的物件註冊到定時器單例中,存放在陣列裡面,當定時器更新的時候遍歷陣列回撥通知

定時器的建立

注意:預設暫停定時器,定時器預設是載入到當前runloop中的,在進行UI介面操作比如滑動列表時,由於在main runloop中NSTimer是同步交付的被“阻塞”,就會導致NSTimer計時出現延誤
解決這種延誤的方法,一種是在子執行緒中進行NSTimer的操作,在主執行緒中修改UI介面顯示操作結果;另一種是仍然在主執行緒中進行NSTimer操作,但是將NSTimer例項加到main runloop的特定mode(模式)中。避免被複雜運算操作或者UI介面重新整理所干擾。

timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {[unowned self] (_) in
     self.onTimer()
})
//下面這種方法要求onTimer是@objc
//Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(onTimer), userInfo: nil, repeats: true) 
註冊通知

這裡使用NSHashTable存放註冊物件的陣列,可以防止迴圈引用註冊物件釋放不掉
swift的protocol是一個很好的東西,這樣可以更好的規範誰可以註冊通知

@objc
protocol TimerListener: class {
    func didOnTimer(announcer: YZTimerUtil, timeInterval: TimeInterval)
}
private let map: NSHashTable<TimerListener> = NSHashTable<TimerListener>.weakObjects()
何時註冊刪除通知

一開始是在willDisplay、didEndDisplaying方法中進行通知註冊的,後來發現沒有必要,因為cell建立後就是要接收通知的willDisplay、didEndDisplaying中還要進行cell型別的判斷,所以就改為cell- init/deinit中

    deinit {
        YZTimerUtil.sharedInstance.removeListener(listener: self)
    }
  //這裡cell使用的是SB
    override func awakeFromNib() {
        super.awakeFromNib()
        YZTimerUtil.sharedInstance.addListener(listener: self)
    }

2.本地時間可能和伺服器時間存在誤差

這裡看專案需求吧,如果專案對時間要求沒有那麼嚴格,不做伺服器時間對比也行,反正伺服器那邊會進行判斷的,有些對時間要求嚴格的肯定是要做對比的,比如手機手令的動態碼
我的思路是在定時器初始化的時候進行網路請求,拿到伺服器的當前時間,然後計算本地和伺服器時間的差值,後面就用這個差值進行計算。當然,受網路狀態的影響,這個時間可能也不是準確的時間,但是這個時間誤差會在一個可控範圍內,為了精確時間差,可以每隔一段時間就校準一次,如果要更精準的,可以通過請求的requestTime/responseTime進行演算法計算

    /// 從伺服器請求最新的時間,簡單示例
    func resetServerTime() {
        // 從伺服器請求最新的時間
        // 。。。
        var success = true
        
        if success {
            // 請求成功
            serverTimeInterval = 0
        } else {
            // 如果請求失敗,隔一段時間再請求一次
            perform(#selector(resetServerTime), with: nil, afterDelay: rloadTimeInterval)
        }
    }

demo裡面的程式碼很詳細,也很簡單,建議可以看看

相關文章