如何在Swift中创建延迟?

394
我想在特定时刻暂停我的应用程序。换句话说,我希望我的应用程序执行代码,但在某个点上暂停4秒,然后继续执行其余的代码。我该如何做?
我正在使用Swift。

1
https://dev59.com/gGAg5IYBdhLWcg3wDHQ3#24318861 - Mick MacCallum
19个回答

507

使用dispatch_after块通常比使用sleep(time)更好,因为执行sleep操作的线程会被阻塞,无法执行其他工作。使用dispatch_after时,被处理的线程不会被阻塞,因此可以在此期间执行其他工作。
如果您正在处理应用程序的主线程,则使用sleep(time)会影响用户体验,因为在此期间UI会变得不响应。

Dispatch after会安排执行一段代码块,而不会冻结线程:

Swift ≥ 3.0

let seconds = 4.0
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
    // Put your code which should be executed with a delay here
}

在异步上下文中使用Swift ≥ 5.5:

func foo() async {
    try await Task.sleep(nanoseconds: UInt64(seconds * Double(NSEC_PER_SEC)))
    // Put your code which should be executed with a delay here
}

Swift < 3.0

let time = dispatch_time(dispatch_time_t(DISPATCH_TIME_NOW), 4 * Int64(NSEC_PER_SEC))
dispatch_after(time, dispatch_get_main_queue()) {
    // Put your code which should be executed with a delay here
}

1
周期性的操作怎么办? - qed
2
有可能使用gcd来处理周期性任务,这在苹果开发者文档中的这篇文章中有所描述,但我建议使用NSTimer来完成。这个答案展示了如何在Swift中实现。 - Palle
2
代码已更新至Xcode 8.2.1。之前的.now() + .seconds(4)会出现错误:expression type is ambiguous without more context - richards
14
无需再添加.now() + .seconds(5),直接使用.now() + 5即可。 - cnzac
@cnzac 从来没有必要。 - Palle
显示剩余9条评论

370

如果你需要在UI线程中实现延迟操作,不要使用sleep方法,因为它会阻塞程序。而应该考虑使用NSTimer或者dispatch timer。

但是,如果你真的需要在当前线程中实现延迟操作:

do {
    sleep(4)
}

这里使用了UNIX中的sleep函数。


4
睡眠功能可在不导入达尔文模块的情况下正常运作,不确定这是否是一项更改。 - Dan Beaulieu
23
如果需要更精确的1秒分辨率,usleep()也可以工作。 - prewett
25
usleep() 函数以百万分之一秒为单位计时,因此 usleep(1000000) 会让程序休眠 1 秒。 - Elijah
66
感谢您将“不要这样做”的前言简短明了。 - Dan Rosenstark
4
我使用sleep()函数来模拟长时间运行的后台进程。 - William T. Mallard
显示剩余2条评论

69

在Swift 3.0中不同方法的比较

1. Sleep

这种方法没有回调函数。将代码直接放在此行后,在4秒钟后执行。它会阻止用户与UI元素(如测试按钮)交互,直到时间结束。虽然当睡眠开始时,该按钮有点冻结,但其他元素(如活动指示器)仍在旋转而不会冻结。在睡眠期间,您无法再次触发此操作。

sleep(4)
print("done")//Do stuff here

输入图像描述

2. Dispatch, Perform and Timer

这三种方法的工作方式类似,它们都在后台线程上运行,并带有回调函数,只是语法和功能略有不同。

Dispatch通常用于在后台线程上运行某些操作。它将回调作为函数调用的一部分。

DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(4), execute: {
    print("done")
})

Perform是一个简化的计时器。它设置了一个带有延迟的计时器,然后通过选择器触发功能。

perform(#selector(callback), with: nil, afterDelay: 4.0)

func callback() {
    print("done")
}}

最后,定时器还提供了重复调用回调函数的能力,但在这种情况下并不实用。

Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(callback), userInfo: nil, repeats: false)


func callback() {
    print("done")
}}

对于这三种方法,在点击按钮触发它们时,UI不会冻结,您可以再次单击它。如果您再次单击按钮,则会设置另一个计时器并触发回调两次。

输入图像描述

总结

这四种方法没有一种单独表现得足够好。 sleep将禁用用户交互,因此屏幕会"冻结"(实际上并非如此),导致用户体验不佳。其他三种方法不会冻结屏幕,但您可能会多次触发它们,并且大多数情况下,您希望在允许用户再次调用之前等待收到回调。

因此,更好的设计将使用其中一种具有屏幕阻塞功能的异步方法。当用户单击按钮时,用一些半透明视图覆盖整个屏幕,并在顶部放置旋转活动指示器,告诉用户正在处理按钮单击。然后在回调函数中删除视图和指示器,告诉用户操作已经正确处理等等。


4
请注意,在关闭了 Swift 4 中的 ObjC 推断的情况下,您需要在回调函数前面添加 "@objc" 以供 "perform(...)" 选项使用。就像这样:@objc func callback() { - leanne

48

在Swift 4.2和Xcode 10.1中,您总共有4种延迟的方式。其中,选项1是推荐的,在一段时间后调用或执行一个函数。使用sleep()是最不常见的情况。

选项1。

DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
    self.yourFuncHere()
}
//Your function here    
func yourFuncHere() {

}

选项 2。

perform(#selector(yourFuncHere2), with: nil, afterDelay: 5.0)

//Your function here  
@objc func yourFuncHere2() {
    print("this is...")
}

选项3。

Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(yourFuncHere3), userInfo: nil, repeats: false)

//Your function here  
@objc func yourFuncHere3() {

}

选项4。

sleep(5)

如果想在一段时间后调用函数以执行某些操作,请勿使用sleep


40

我同意 Palle 的观点,使用 dispatch_after 在这里是一个不错的选择。但你可能不喜欢 GCD 调用,因为它们写起来相当让人烦恼。相反,你可以添加这个方便的帮助程序:

public func delay(bySeconds seconds: Double, dispatchLevel: DispatchLevel = .main, closure: @escaping () -> Void) {
    let dispatchTime = DispatchTime.now() + seconds
    dispatchLevel.dispatchQueue.asyncAfter(deadline: dispatchTime, execute: closure)
}

public enum DispatchLevel {
    case main, userInteractive, userInitiated, utility, background
    var dispatchQueue: DispatchQueue {
        switch self {
        case .main:                 return DispatchQueue.main
        case .userInteractive:      return DispatchQueue.global(qos: .userInteractive)
        case .userInitiated:        return DispatchQueue.global(qos: .userInitiated)
        case .utility:              return DispatchQueue.global(qos: .utility)
        case .background:           return DispatchQueue.global(qos: .background)
        }
    }
}

现在,您只需像这样在后台线程上延迟执行您的代码:

现在,您只需要像这样在后台线程上延迟执行您的代码:

delay(bySeconds: 1.5, dispatchLevel: .background) { 
    // delayed code that will run on background thread
}

主线程上延迟代码甚至更加简单:

delay(bySeconds: 1.5) { 
    // delayed code, by default run in main thread
}

如果您更喜欢一个框架,它还有一些更方便的功能,请查看HandySwift。 您可以通过SwiftPM将其添加到项目中,然后像上面的示例一样使用:

import HandySwift    

delay(by: .seconds(1.5)) { 
    // delayed code
}

这似乎非常有帮助。您介意更新为Swift 3.0版本吗?谢谢。 - grahamcracker1234
我已经开始将HandySwift迁移到Swift 3,并计划在下周发布新版本。然后我也会更新上面的脚本。请继续关注。 - Jeehut
HandySwift的迁移已经完成,并在1.3.0版本中发布。我刚刚更新了上面的代码,使其与Swift 3兼容! :) - Jeehut
1
你为什么要先乘以NSEC_PER_SEC,然后再除以它呢?这不是多余的吗?(例如Double(Int64(seconds * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC))难道你不能只写成“DispatchTime.now() + Double(seconds)”吗? - Mark A. Donohoe
1
谢谢@MarqueIV,你当然是对的。我刚刚更新了代码。这只是从Xcode 8自动转换的结果,需要进行类型转换,因为在Swift 2中DispatchTime不可用。之前是let dispatchTime = dispatch_time(DISPATCH_TIME_NOW, Int64(seconds * Double(NSEC_PER_SEC))) - Jeehut
嗨,伙计,我可以在30秒后在后台(应用未运行)调用这个方法吗?先谢谢了。 - famfamfam

25

你也可以使用Swift 3来完成这个操作。

像这样延迟执行函数。

override func viewDidLoad() {
    super.viewDidLoad()

    self.perform(#selector(ClassName.performAction), with: nil, afterDelay: 2.0)
}


     @objc func performAction() {
//This function will perform after 2 seconds
            print("Delayed")
        }

22

NSTimer

@nneonneo的答案建议使用NSTimer,但没有展示如何实现。这是基本语法:

let delay = 0.5 // time in seconds
NSTimer.scheduledTimerWithTimeInterval(delay, target: self, selector: #selector(myFunctionName), userInfo: nil, repeats: false)

这是一个非常简单的项目,展示了如何使用它。当按下按钮时,它会启动一个计时器,在半秒钟延迟后调用一个函数。

import UIKit
class ViewController: UIViewController {

    var timer = NSTimer()
    let delay = 0.5
    
    // start timer when button is tapped
    @IBAction func startTimerButtonTapped(sender: UIButton) {

        // cancel the timer in case the button is tapped multiple times
        timer.invalidate()

        // start the timer
        timer = NSTimer.scheduledTimerWithTimeInterval(delay, target: self, selector: #selector(delayedAction), userInfo: nil, repeats: false)
    }

    // function to be called after the delay
    func delayedAction() {
        print("action has started")
    }
}

使用dispatch_time(例如Palle的答案中所述)是另一个有效的选项。但是,它很难以取消。使用NSTimer,在延迟事件发生之前取消它,你只需要调用

timer.invalidate()

使用 sleep 不被推荐,特别是在主线程上,因为它会停止该线程上所有正在执行的工作。

关于更详细的解答,请参考这里


15

我认为最简单和最新的4秒计时器方式是:

Task { 
    // Do something

    // Wait for 4 seconds
    try await Task.sleep(nanoseconds: 4_000_000_000) 

}

它使用了 Swift 5.5 的新并发特性。

苹果的并发


11

尝试使用Swift 3.0中的以下实现:

func delayWithSeconds(_ seconds: Double, completion: @escaping () -> ()) {
    DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { 
        completion()
    }
}

使用方法

delayWithSeconds(1) {
   //Do something
}

10

您可以轻松创建使用延迟函数的扩展(语法:Swift 4.2+)

extension UIViewController {
    func delay(_ delay:Double, closure:@escaping ()->()) {
        DispatchQueue.main.asyncAfter(
            deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
    }
}

如何在UIViewController中使用

self.delay(0.1, closure: {
   //execute code
})

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