0

So I need to update a UILabel text value after X seconds have passed after the view appears on screen, the initial label text value comes from an API endpoint which is refreshed everytime the view appears.

Im currently doing the following:

 func updateLabelAfterAPICall(initialValue: String) {
    lastValue = initialValue //this is a local variable so I can use it to set the text once the view dissappears.
    label.text = lastValue
        Task {
            try? await Task.sleep(nanoseconds: 5_500_000_000)
            label.text = "New Value after 5.5 seconds passed"
        }
 }

Once the view dissappears, I need to set the label back to its initial value so I reset it again in a viewDidDissappear (if I dont do this, everytime the view is shown I see the "new text" for a brief second until the API call finishes, this is unwanted):

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    label.text = lastValue
}

This seems to be working OK for the most part but I feel like there's some edge case Im missing where I might need to cancel the Task or something similar? Maybe if I make the view appear and dissappear a bunch of times before 5.5 seconds have passed, would that create a bunch of different Tasks?

I ask this since I cant really replicate it exactly every time, but while testing, I've encountered some glitches such as the text not resetting to lastValue once I return to the view (the majority of times it seems to work fine though, which makes testing and debugging a pain).

Any tip for improvement is welcomed. Thanks!

stompy
  • 227
  • 1
  • 13
  • Oh no, the delay is not intended to replicate the API response time, that function updateLabelAfterAPICall is called exactly once the request ends. I just need to update the label 5 seconds after that function is called. – stompy May 08 '22 at 18:12
  • I just wish to know if this approach is good or if Im missing something or if it should be done differently. After testing it a bunch of times, it seems that randomly once the view appears again, the label text still has the new value (the one applied after 5.5 seconds have passed), it seems to happen like 1 time every 20 tries which makes testing and debuggin a pain. Maybe I need to cancel the Task at some point or something similar. – stompy May 08 '22 at 18:41
  • Well, what happens if you call `updateLabelAfterAPICall` and then the view immediately disappears? The task continues to run, and then the `lastValue` is overwritten with the new value. – matt May 08 '22 at 18:47
  • Yes! That are exactly my thoughts (if I understood you correctly, thats the case specified on my reply to the answer made by another user on this same question). How would I go about fixing that? I guess I would need to somehow cancel the action of updating the label if the view dissappears before 5.5s have passed. Any clue? – stompy May 08 '22 at 18:57
  • Sure, you'd need to store the task so you can cancel it. `Task.sleep` is cancellable so no worries. – matt May 08 '22 at 19:03
  • Would you mind providing an example with code? As far as I know, Tasks run the second you define them, theres nothing like aRandomTask.start(), so if I define it within the function, I wouldnt be able to cancel it from within a viewDidDissappear since It would be out of scope, and if I define it on a more global scope such as a local variable in the ViewController, it would run instantly once the view loads, not 5.5s after the API call finishes as intended. – stompy May 08 '22 at 19:20

2 Answers2

2

It seems you might be concerned about what happens if the view disappears before the label has a chance to change. In that case you'd presumably like to cancel the whole Task operation. To do so, retain the Task in an instance property so that you can cancel it if the view disappears prematurely, something like this:

var task = Task<Void, Never> {}
func updateLabelAfterAPICall(initialValue: String) {
    task.cancel() // just in case
    task = Task { [weak self] in
        do {
            try await Task.sleep(nanoseconds: 5_500_000_000)
            guard let self = self else { return }
            self.label.text = "New Value after 5.5 seconds passed"
        } catch {}
    }
}
override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    task.cancel()
}

The bulk of the Task is the call to Task.sleep, which is cancellable, so the whole operation will be cancelled in good order because the Task.sleep call will throw when cancelled and the whole Task operation will abort.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Works like magic! Thank you so much! Questions: 1) Are there any differences between this and what the user below suggested in the last comment? Specifically, using DispatchWorkItem & DispatchQueue.main.asyncAfter instead of Task. Explained here: https://stackoverflow.com/a/52360648/14214322 2) Do I need to update UI on the main thread manually or does Task take care of that? 3) Upon googling a bit, I stumbled upon this: https://www.hackingwithswift.com/quick-start/concurrency/how-to-cancel-a-task . Regarding point 5, would this be an issue (since im using Task.sleep() )? – stompy May 08 '22 at 23:28
  • All I was doing was expanding on my comment "store the task so you can cancel it", as you did not seem ready to grasp what I was describing. I'm not willing to have my answer spawn a dozen additional questions for me to deal with in the comment section. My answer is what it is; if it's not sufficiently helpful I'll be happy to delete it. – matt May 09 '22 at 02:42
  • 2
    @stompy - FWIW, I might advise a few simplifications on the above, e.g. https://gist.github.com/robertmryan/182d74de1f0bbafbd0a53bfc64f94785. But +1, matt. – Rob May 10 '22 at 14:02
  • Having your answer spawn a dozen additional questions for you to deal with was certainly not my intention, my apologies if it sounded sorta rude, I just thought your answer was great since it takes advantage of modern concurrency and thought I would ask questions comparing to the more "legacy" way to do this as the other question suggested. That's all. No need to delete it, it was super useful and I ended up using your alternative in my code. Once again, am super thankful and sorry If the extra questions were a bit too much! – stompy May 10 '22 at 19:29
  • @Rob Thank you so much for expanding on matts answer, I went ahead and made the proposed changes to my code! – stompy May 10 '22 at 19:30
0

It may not be safe to put an action you are actively performing to sleep. You can do this on the main thread. Try this:

func updateLabelAfterAPICall(initialValue: String) {
    lastValue = initialValue //this is a local variable so I can use it to set the text once the view dissappears.
    label.text = lastValue
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 5.5) {
        label.text = "New Value after 5.5 seconds passed"
    }
 }
Veysel Bozkurt
  • 53
  • 2
  • 11
  • I thought everything inside a Task was performed on the main thread, my bad. – stompy May 08 '22 at 18:50
  • what would happen if I leave the view before those 5.5 seconds have passed? It would still execute the block even if I am not in the view, right? How would I avoid that? – stompy May 08 '22 at 18:51
  • for that, I can check ARC. You can see it here: https://stackoverflow.com/a/49894330/14214322 – Veysel Bozkurt May 08 '22 at 19:41
  • So, Is my answer not useful – Veysel Bozkurt May 08 '22 at 19:41
  • Its useful yes! I just need to know how to avoid executing the code inside the DispatchQueue.main.asyncAfter if the user already left the view before the 5.5s have passed. As for ARC, I did read your link but fail to realize how it could help here? Mind sharing an example with code by any chance? – stompy May 08 '22 at 19:49
  • I understand what you mean, for this, you can block the process in the thread you are running in the viewWillDissappear method. like this https://stackoverflow.com/a/52360648/14214322 – Veysel Bozkurt May 08 '22 at 20:00
  • Just tested this and it seems to work as intended, thank you so much! Wondering if theres any difference between this and what matt suggested on his answer which uses the new concurrency syntax instead of DispatchWorkItem & DispatchQueue.main.asyncAfter. – stompy May 08 '22 at 23:31
  • Okay, Can you mark my answer as useful so that it will be useful for other users? – Veysel Bozkurt May 09 '22 at 06:34
  • Just to address a few of stompy’s questions in the above: Yes, this keeps a strong reference to `self`; and will fire off in 5.5 seconds, regardless. You could fix this by (a) using explicit `DispatchWorkItem` instead of block, (b) add `[weak self]` capture list; and (c) `cancel` this `DispatchWorkItem` in `viewDidDisappear`. But, with no offense intended, this simple block-based `asyncAfter` is not a good idea. – Rob May 10 '22 at 14:15
  • Thanks again, rob. I ended up using Task in favor of using modern concurrency. Wondering if theres any noticeable difference between using Task and DispatchWorkItem though. – stompy May 10 '22 at 19:34
  • And @VeyselBozkurt, I upvoted your answer since it definitely helped. Had to select the other answer as the best one since it used modern concurrency, but yours was great also, so super thankful for it ! – stompy May 10 '22 at 19:35