我刚开始使用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功能。
有没有很好的方法来实现这一目标?
非常感谢, 代码
答案 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秒钟会进行第一次刷新。我们必须允许一些容忍度。您走得越远,承受力就越大。