Dispatchqueue.Main.Asyncafter Is Inaccurate

DispatchQueue.main.asyncAfter is inaccurate

asyncAfter is only guaranteed to wait at least as long as specified.

You probably should still be using Timer if you want exact time intervals on iOS9.

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

With executeClosure being a function that executes the last saved closure.

DispatchQueue.asyncAfter doesn't work the way I thought it would?

The other answers listed here are effective, but they aren't quite as robust as they could be. Calling task.resume() at fixed time intervals does not guarantee that requests are actually sent at those intervals. You are at the mercy of the internal queue maintained by URLSession.shared. Adding tasks at 4 second intervals does not mean they will necessarily be sent in that time. It also makes no guarantees about how long a request will take (think mobile networks with poor service). As for Data(contentsOf:), it provides absolutely no real features or customization, such as error handling.

A more robust solution would be to use a DispatchGroup and to only initiate a new request 4 seconds after the previous one has completed.

class ViewController

let serialQueue = DispatchQueue(label: "networkRequests")
let networkGroup = DispatchGroup()

func myFirstFunc {
for item in items {
self.mySecondFunc(item: item, completionHandler: {(completionItem) in
// you shouldn't need this
})
}
}

func mySecondFunc(item: someType, completionHandler: @escaping (String?) -> Void) {

let task = session.dataTask(with: request, completionHandler: {(data, response, error) in
// stuff
completionHandler(changedItem)

Thread.sleep(forTimeInterval: 4) // Wait for 4 seconds
networkGroup.leave() // Then leave this block so the next one can run
})
self.networkGroup.notify(queue: serialQueue) {
networkGroup.wait() // Wait for the previous block to finish
networkGroup.enter() // Enter a new block
task.resume()
print(Date())
}
}
}

This will guarantee that each subsequent request is sent no sooner than 4 seconds after the previous one finishes, and does not rely on outside factors (like URLSession's internal queue or network stability) to maintain proper timing, without sacrificing the modern features of URLSessions.

Bug possibly related to DispatchQueue.main.asyncAfter in UIButton action when button is spammed/rapidly and repeatedly pressed

Here is possible solution - just don't give possibility for a user to generate not desired taps

sender.isEnabled = false // don't allow action until question updated
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.questionLabel.text = self.quizArray[self.questionNumber].text
sender.isEnabled = true // << allow user interaction
}

in second do the same

why 'asyncAfter' is not act in exact time?

It's right there in the method name-- "asyncAfter". The method isn't guaranteed to run code at a specific time, it runs code after a specific time. That might be immediately after the time or it might be later on. You should think of the method as meaning "wait at least this long", not "run code at exactly this time".

If you need more precise timing, consider using Timer or else creating a DispatchSourceTimer.

Mutating conditional in DispatchQueue.main.asyncAfter behavior


Will the DispatchQueue always wait until all previous code is done executing, or is there any scenario in which the completion handler can run "in between" some other block of codes execution?

Depends if it's a Sequential queue or Concurrent queue. Sequential queue will finish task1 before starting task2; concurrent may run them in parallel. main thread is sequential, so you are good there, but...

You said yourself "sometime during these 3.5 seconds a function is called that sets viewModel.someBool to false". What if that "some time" is 1 nanosecond after a delayed task was picked up and started running?.. So now your function that changes viewModel.someBool to false needs to wait for your delayed task to complete.

So either this should be OK for your code (which is preferable, since such a strong dependency on order, especially in UI, usually means some design issues), or you need to guarantee the order in your code

DispatchQueue.main.asyncAfter with on/off switch

There are a few approaches:

  1. invalidate a Timer in deinit

    Its implementation might look like:

    class ResponseTimer {
    private weak var timer: Timer?

    func schedule() {
    timer = Timer.scheduledTimer(withTimeInterval: 8, repeats: false) { _ in // if you reference `self` in this block, make sure to include `[weak self]` capture list, too
    // do something
    }
    }

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

    // you might want to make sure you `invalidate` this when it’s deallocated just in
    // case you accidentally had path of execution that failed to call `invalidate`.

    deinit {
    invalidate()
    }
    }

    And then you can do:

    var responseTimer: ResponseTimer?

    func getSomethingFromFirebase() {
    responseTimer = ResponseTimer()
    responseTimer.schedule()

    ref.observeSingleEvent(of: .value) { snapshot in
    responseTimer?.invalidate()

    //do other stuff
    }
    }
  2. Use asyncAfter with DispatchWorkItem, which you can cancel:

    class ResponseTimer {
    private var item: DispatchWorkItem?

    func schedule() {
    item = DispatchWorkItem { // [weak self] in // if you reference `self` in this block, uncomment this `[weak self]` capture list, too
    // do something
    }
    DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: item!)
    }

    func invalidate() {
    item?.cancel()
    item = nil
    }

    deinit {
    invalidate()
    }
    }
  3. Use DispatchTimerSource, which cancels automatically when it falls out of scope:

    struct ResponseTimer {
    private var timer: DispatchSourceTimer?

    mutating func schedule() {
    timer = DispatchSource.makeTimerSource(queue: .main)
    timer?.setEventHandler { // [weak self] in // if you reference `self` in the closure, uncomment this
    NotificationCenter.default.post(name: notification, object: nil)
    }
    timer?.schedule(deadline: .now() + 8)
    timer?.activate()
    }

    mutating func invalidate() {
    timer = nil
    }
    }

In all three patterns, the timer will be canceled when ResponseTimer falls out of scope.

DispatchQueue.main.asyncAfter repeatedly called in iOS 14.0 Beta

In

.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.show.toggle() // << this !!
}
}

.. self.show.toggle() makes body rebuild and NavigationLink recreated (as it depends on show, and thus .onAppear called again (as it is new link), and you see - go by cycle.

So nothing wrong with DispatchQueue.main.asyncAfter.

Probably you meant to attach that .onAppear modifier to root view, ie. probably NavigationView.

Update: below tested as worked on Xcode 12 / iOS 14

struct ContentView: View {

@State var show = false

var body: some View {
NavigationView{

VStack{
NavigationLink(destination: Text("New View"), isActive: $show, label: {
Image("main_logo").renderingMode(.original).frame(width: 100, height: 100)
})
} .navigationBarHidden(true)
.navigationBarTitle(Text("Home"))
.edgesIgnoringSafeArea([.top, .bottom])
}.preferredColorScheme(.dark) // white tint on status bar
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.show.toggle()
}
}
}
}

Why putting a DispatchQueue delay function inside another DispatchQueue has no effect in Swift

There are a number of ways you could do this. Here are 3:

Use a timer to repeat a block every second

var i = 0
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
i += 1
print("hello \(i)")
if i == 5 {
timer.invalidate()
}
}

Dispatch multiple async tasks

for i in 1...5 {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(i)) {
print("hello \(i)")
}
}

Note: This queues all of the tasks up front and could overflow a queue if there were a large number of them.

Run loop in background with sleep and switch to foreground to print

DispatchQueue.global().async {
for i in 1...5 {
sleep(1)
DispatchQueue.main.async {
print("hello \(i)")
}
}
}

Note: This will drift a little (not be 1 second apart) if the work in the loop takes a significant amount of time.



Related Topics



Leave a reply



Submit