每隔 x 分钟在 Swift 中执行某个操作

132

如何每分钟运行一次函数?在JavaScript中,我可以使用类似setInterval的东西,Swift中是否存在类似的功能呢?

期望的输出:

每分钟运行一次“Hello World”...


更新至Swift 2:Swift计时器 - JamesG
9个回答

192
var helloWorldTimer = NSTimer.scheduledTimerWithTimeInterval(60.0, target: self, selector: Selector("sayHello"), userInfo: nil, repeats: true)

func sayHello() 
{
    NSLog("hello World")
}

记得导入Foundation库。

Swift 4:

 var helloWorldTimer = Timer.scheduledTimer(timeInterval: 60.0, target: self, selector: #selector(ViewController.sayHello), userInfo: nil, repeats: true)

 @objc func sayHello() 
 {
     NSLog("hello World")
 }

1
但是如果你想在视图之间切换怎么办?代码会停止,对吧? - Cing
7
取决于“self”指的是什么。 - Antzi
1
不要忘记 NSTimer 会保留它的目标对象,因此,在这种设置下,如果 helloWorldTimerself 的属性,则会出现保留循环,其中 self 保留 helloWorldTimer,而 helloWorldTimer 保留 self - MANIAK_dobrii
@MANIAK_dobrii,你能否也评论一下如何打破保留循环?如果视图控制器被解散但计时器没有被取消,那么循环仍然存在?因此,您必须先取消计时器,然后解散视图以打破循环? - Dave G
@DaveG 当 timer 持有 VC,而 VC 又持有 timer 时,循环将一直保持,直到其中一个释放另一个,这就是引用计数。最好的方法是 -invalidate 定时器,这将释放它的目标并打破循环。当你完成使用定时器时,你需要 -invalidate 它,但不要在 VC-dealloc 中这样做,因为 dealloc 在对象最后一次释放后被调用,你知道,在那里你需要打破一个保留循环。 - MANIAK_dobrii
1
对于Swift 4,您想要获取选择器的方法必须向Objective-C公开,因此必须在方法声明中添加@objc属性。例如@objc func sayHello(){} - Shamim Hossain

152

如果针对 iOS 10 及以上版本进行目标定位,可以使用基于块的Timer演示,这种方法简化了潜在的强引用循环,例如:

weak var timer: Timer?

func startTimer() {
    timer?.invalidate()   // just in case you had existing `Timer`, `invalidate` it before we lose our reference to it
    timer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { [weak self] _ in
        // do something here
    }
}

func stopTimer() {
    timer?.invalidate()
}

// if appropriate, make sure to stop your timer in `deinit`

deinit {
    stopTimer()
}

虽然通常最好使用Timer,但为了完整起见,我应该注意到您也可以使用分派计时器,这对于在后台线程上安排计时器很有用。使用分派定时器,由于它们是基于块的,所以避免了旧的Timertarget/selector模式中一些强引用循环挑战,只要您使用weak 引用。

所以:

var timer: DispatchSourceTimer?

func startTimer() {
    let queue = DispatchQueue(label: "com.domain.app.timer")  // you can also use `DispatchQueue.main`, if you want
    timer = DispatchSource.makeTimerSource(queue: queue)
    timer!.schedule(deadline: .now(), repeating: .seconds(60))
    timer!.setEventHandler { [weak self] in
        // do whatever you want here
    }
    timer!.resume()
}

func stopTimer() {
    timer = nil
}

了解更多信息,请参见 Concurrency Programming Guide 中的 Dispatch Source ExamplesCreating a Timer 部分。在 Dispatch Sources 部分。


对于 Swift 2,请参见这个答案的之前的版本


在这种方法中,您如何实现仅执行一次的计时器? - Juan Boero
@JuanPabloBoero - 对于只需要触发一次的情况,我不会使用这种方法。你应该使用dispatch_after或者一个非重复的NSTimer - Rob
@Rob 我在屏幕上有一个计数器标签,它每秒从一个函数中更新一次,并且我正在使用上面的代码来增加我的计数器并更新标签文本。但是,我面临的问题是,我的计数器变量每秒都会更新,但屏幕上的UILabel不显示最新的计数器值。 - Nagendra Rao
Rao,上述内容对于在后台线程执行某些操作非常有用。但是您的UI更新必须在主队列上进行。因此,请将其安排在主线程上或者至少将UI更新调度回主线程。 - Rob
1
这个问题不应该在评论中出现,而是应该删除你上面的评论并发布自己的问题。但是,简而言之,通常情况下你实际上并不会让应用程序在后台运行,而只是让它看起来像是在运行。请参阅https://dev59.com/xo3da4cB1Zd3GeqPzmy9#31642036。 - Rob
显示剩余4条评论

22

如果您可以容许一些时间漂移,这里有一个简单的解决方案,每分钟执行一些代码:

private func executeRepeatedly() {
    // put your code here

    DispatchQueue.main.asyncAfter(deadline: .now() + 60.0) { [weak self] in
        self?.executeRepeatedly()
    }
}

只需运行executeRepeatedly()一次,它就会每分钟执行一次。当拥有对象(self)被释放时,执行将停止。您还可以使用标志来指示执行必须停止。


1
这个决定比拥有所有这些计时器,并将它们添加到运行循环中并使其无效更为方便...太棒了! - julia_v
这似乎是一个有效的解决方案,但当我将其与我的网络服务调用一起使用时,它会创建一个保留循环。 - jayant rawat

20

以下是使用闭包而不是命名函数来更新 NSTimer答案的Swift 3版本(其中NSTimer已重命名为Timer):

var timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) {
    (_) in
    print("Hello world")
}

7
仅适用于iOS 10。 - Glenn Posadas
请不要发布iOS 10+的解决方案。 - Numan Karaaslan

12
你可以使用Timer(Swift 3)
var timer = Timer.scheduledTimerWithTimeInterval(60, target: self, selector: Selector("function"), userInfo: nil, repeats: true)
在selector()函数中,你需要输入你的函数名。

你如何在Swift 3中实现它? - bibscy
1
使用Timer... NSTimer已更名。 - Joseph

7

在Swift 3.0中,GCD被重构了:

let timer : DispatchSourceTimer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.main)

timer.scheduleRepeating(deadline: .now(), interval: .seconds(60))
timer.setEventHandler
{
    NSLog("Hello World")
}
timer.resume()

这对于需要在特定队列上执行操作时非常有用。此外,如果您计划将其用于用户界面更新,则建议查看与GPU刷新率同步的CADisplayLink


1

Swift并发(无定时器,无Foundation)

你可以像这样简单地执行一个for循环:

for try await tick in Every(.seconds(3)) { 
    print(tick)
}

通过实施这个简单的异步序列:
struct Every: AsyncSequence, AsyncIteratorProtocol {
    typealias Element = Int

    var duration: Duration
    private(set) var tick = 0

    init(_ duration: Duration) { self.duration = duration }
    func makeAsyncIterator() -> Self { self }

    mutating func next() async throws -> Element? {
        try await Task.sleep(for: duration)
        guard !Task.isCancelled else { return nil }

        tick += 1
        return tick
    }
}

可取消

当然,你可以通过将整个操作包装在一个Task中,并随时取消任务来异步执行它,就像这样:

let clock = Task {
    for try await tick in Every(.seconds(3)) { 
        print(tick)
        if tick == 3 { clock.cancel() } //  Cancel the task after 3 ticks
    }
}

SwiftUI

.task修饰符中执行此操作将自动将迭代的生命周期绑定到View的生命周期。无需担心无效化的问题:

.task {
    do {
        for try await tick in Every(.seconds(1)) { print(tick) }
    } catch {
        print(error)
    }
}

0
timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true, block: myMethod)

func myMethod(_:Timer) {
...
}

或者

timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { _ in
...
}

请确保在某个时刻使计时器无效,例如您的时间不再可见,或者您的对象已被销毁。


0

这是另一个版本algrid's的答案,有一种简单的方法可以停止它

@objc func executeRepeatedly() {

    print("--Do something on repeat--")
        
    perform(#selector(executeRepeatedly), with: nil, afterDelay: 60.0)
}

这是一个如何启动和停止它的示例:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    executeRepeatedly() // start it
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)

    NSObject.cancelPreviousPerformRequests(withTarget: self) // stop it
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接