计时器和依赖注入

时间:2018-08-21 21:18:10

标签: ios swift

我刚开始使用Swift并将MVVM与依赖项注入结合使用。

在我的ViewModel中,我具有用于刷新数据的Timer。为了清楚起见,我对代码进行了一些简化。

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let viewModel = ViewModel()
    }
}

class ViewModel: NSObject {

    private var timer: Timer?

    override init() {
        super.init()
        setUpTimer()
    }

    func setUpTimer() {
        timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true){_ in
            self.refreshData()
        }
    }

    func refreshData() {
        //refresh data
        print("refresh data")
    }
}

我想使用依赖注入将Timer传递到ViewModel中,以便在进行单元测试时可以控制该计时器并使其立即调用。

因此,传递计时器非常简单。如何将Timer传递给ViewModel,它可以调用属于ViewModel的refreshData()。 Swift中有一个技巧可以允许这样做吗?

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true){_ in
            // call refreshData() from the class ViewModel
            }

        var viewModel = ViewModel(myTimer:timer)
    }
}

class ViewModel: NSObject {

    private var timer: Timer?

    init(myTimer:Timer) {
        super.init()
        //setUpTimer()
        timer = myTimer
    }

    func refreshData() {
        //refresh data
        print("refresh data")
    }
}

我认为使用使用选择器而不是块的scheduelTimer可能是可行的,但是这将需要在func refreshData()之前使用@objc,这似乎很笨拙,因为我在Swift中使用了Objective C功能。

有没有很好的方法来实现这一目标?

非常感谢, 代码

2 个答案:

答案 0 :(得分:1)

从概念上讲,您想解耦实现。因此,您不必传递Timer到视图模型,而传递了其他“控制”对象,该对象可以确保执行操作(在延迟后回调)

如果那不叫protocol,我不知道是什么...

typealias Ticker = () -> Void

protocol Refresher {
    var isRunning: Bool { get }
    func register(_ ticker: @escaping Ticker)
    func start();
    func stop();
}

所以,非常基本的概念。它可以开始,停止,观察者可以向其注册,并在发生“滴答”时得到通知。观察者不在乎它的“工作方式”,只要保证执行指定的操作即可。

基于Timer的实现可能看起来像...

class TimerRefresher: Refresher {

    private var timer: Timer? = nil
    private var ticker: Ticker? = nil

    var isRunning: Bool = false

    func register(_ ticker: @escaping Ticker) {
        self.ticker = ticker
        guard timer == nil else {
            return
        }
    }

    func start() {
        guard ticker != nil else {
            return
        }
        stop()
        isRunning = true
        timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true, block: { (timer) in
            self.tick()
        })
    }

    func stop() {
        guard let timer = timer else {
            return
        }
        isRunning = false
        timer.invalidate()
        self.timer = nil
    }

    private func tick() {
        guard let ticker = ticker else {
            stop()
            return
        }
        ticker()
    }
}

通过为Refresher的实现替换为您可以手动控制的实现(或根据需要使用其他“延迟”操作),这为您提供了模拟依赖项注入的入口点

这只是一个概念性示例,您的实现/需求可能会有所不同,并导致您进行稍微不同的设计,但是想法仍然相同,以某种方式将物理实现解耦。

一个替代方案将需要您重新考虑设计,并且视图/控制器将代替您承担责任,而不是由视图模型自己执行刷新。由于这是一个重大的设计决策,因此您实际上只是可以做出该决策的人,但这是另一个想法

答案 1 :(得分:1)

如果我对您的理解正确,则希望模型在应用程序中运行时每30秒刷新一次,但测试速度更快。如果是这样,请勿注入计时器。注入刷新频率。

class ViewModel: NSObject {
    // We need something to observe and confirm that the data is fresh
    @objc dynamic var lastRefreshed: Date?

    private var timer: Timer!

    // The default frequency is 30 seconds but users can adjust that
    // The unit test uses it to inject dependency
    init(refreshFrequency: TimeInterval = 30) {
        super.init()
        timer = Timer.scheduledTimer(timeInterval: refreshFrequency, target: self, selector: #selector(refreshData), userInfo: nil, repeats: true)
    }

    @objc func refreshData() {
        lastRefreshed = Date()
        print("refreshed on: \(lastRefreshed!)")
    }
}

以及您的单元测试:

func testModel() {
    let startTime = Date()
    let model = ViewModel(refreshFrequency: 5)

    // Test first refresh: must be within 5 - 6 seconds from startTime
    keyValueObservingExpectation(for: model, keyPath: #keyPath(ViewModel.lastRefreshed)) { (_, _) -> Bool in
        if let duration = model.lastRefreshed?.timeIntervalSince(startTime), 5...6 ~= duration {
            return true
        } else {
            return false
        }
    }

    // Test second refresh: must be within 10 - 12 seconds from startTime
    keyValueObservingExpectation(for: model, keyPath: #keyPath(ViewModel.lastRefreshed)) { (_, _) -> Bool in
        if let duration = model.lastRefreshed?.timeIntervalSince(startTime), 10...12 ~= duration {
            return true
        } else {
            return false
        }
    }

    // Wait 12 seconds for both expectations to be fulfilled
    waitForExpectations(timeout: 12, handler: nil)
}

Timer是不正确的:它不会像您要求的那样每5秒精确触发一次。苹果公司说Timer的精确度约为50-100ms。因此,我们不能指望从现在起5秒钟会进行第一次刷新。我们必须允许一些容忍度。您走得越远,承受力就越大。