Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-Repeatable viewDidAppear Logic #8

Open
ahmedk92 opened this issue May 29, 2020 · 1 comment
Open

Non-Repeatable viewDidAppear Logic #8

ahmedk92 opened this issue May 29, 2020 · 1 comment

Comments

@ahmedk92
Copy link
Owner

(Originally published 2019-02-05)

Sometimes we need to show an alert, apply a gradient, or conditionally show another view controller on the startup of a view controller. We wish to do such thing in viewDidLoad, however we end up doing it in viewDidAppear(_:). This is because such purposes have requirements that are not fulfilled when viewDidLoad executes; e.g. frames are not yet correct, view hierarchy is not ready, ...etc.

One annoyance with viewDidAppear(_:) is that it can get called multiple times. For example, if you present a new view controller, then dismiss it sometime after, it gets called again. You have to handle such logic that shouldn't be repeated.

Solutions that work, but I don't quite like

1. Booleans

One can introduce a Bool like viewAppeared; checking and setting it for once:

var viewAppeared = false

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

    if !viewAppeared {
        // Do non-repeatable logic...
    }

    viewAppeared = true
}

It works. However, I tend to try to avoid "state variables" as much as I can (doesn't mean I succeed much 😅). Although they look simple, bugs find their way around them. And in this particular situation, you may need to repeat some check you did earlier in viewDidLoad, and it gets less nice:

var name: String?

override func viewDidLoad() {
    super.viewDidLoad()

    if let name = name {
        // conditional initial setup 
    }
}

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

    if !viewAppeared {

        // this check again
        if let name = name {

        }
    }

    viewAppeared = true
}

2. Deferring with GCD

Magic. GCD helps in executing code later. There are two ways to do it for our need: async and asyncAfter(deadline:execute:). We can use either in viewDidLoad to execute non-repeatable code "later enough".

asyncAfter(deadline:execute:) is straight forward:

override func viewDidLoad() {
    super.viewDidLoad()

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
    // Non-repeated logic
    }
}

Why 0.1 seconds? No idea. It's just late enough. It works, but it depends on a magic number, we don't know exactly when this code runs.

What about just async? It works too, no explicit delay needed. That's because code in a DispatchQueue.main.async block is executed in the next run loop.

Believe me, I've read a lot about run loops, I still don't understand them much. However, for the main thread, you can interpret them as time slices in which the main thread accepts input, updates UI, calculate layouts, and more importantly "polls" code enqueued via DispatchQueue.main.async.

So, When we dispatch code async on the main queue from the main thread, it doesn't run immediately; it waits to be polled in the next run loop, leaving enough time for the requirements mentioned above to be fulfilled. Remember our blog post? 😂 Now let's continue it. This GCD thing worth it's own blog post.

If you look at code I wrote (I hope you don't), you'll find me guilty of using this to hack my way through. It's not good, I don't recommend it. We should invest more time to really solve latency problems rather than working around them.

A Better Solution?

Here I suggest a solution that I think is better. GCD gave us a hint, we need to queue tasks on viewDidLoad; but execute it later. So, what about queuing it on viewDidLoad, then execute it on viewDidAppear(_:)?

private var viewDidAppearQueue: [() -> ()] = []

override func viewDidLoad() {
    super.viewDidLoad()
        
    viewDidAppearQueue.append {
        // Our non-repeatable logic
    }
}
    
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
        
    // Dequeue tasks and execute them in FIFO order
    while !viewDidAppearQueue.isEmpty {
        viewDidAppearQueue.removeFirst()()
    }
}

We introduce a simple array of closures. We add our tasks as closures in viewDidLoad. Then in viewDidAppear(_:) we execute each closure and remove it from the array. If there are no tasks; nothing happens, just what we need.

We also don't need to weakly capture self (i.e. [weak self] in) when appending closures. This is because strong references to the closures are lost when we remove them from the array. So, we should be safe.

Thanks for reading. Feedback is welcome.

@ahmedk92
Copy link
Owner Author

ahmedk92 commented Dec 12, 2022

Another - obvious - alternative that I wasn't aware of at the time of writing this is the isSuspended property of OperationQueue. However, looks like that doesn't work with the shared main OperationQueue. So, it should be used as the following:

class ViewController: UIViewController {
  private let operationQueue: OperationQueue = {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1
        return queue
    }()

  override func viewDidLoad() {
        super.viewDidLoad()
        
        operationQueue.isSuspended = true
        operationQueue.addOperation {
            DispatchQueue.main.async { // If we need to execute on the main thread
              // Some code that needs to be executed once in viewDidAppear
            }
        }
    }

  override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        operationQueue.isSuspended = false
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant