Swift
iOS Development
Asynchronous Programming
Functions
IBAction

How can I make a function complete before calling others in an IBAction?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In an IBAction, calling one function after another only guarantees order for synchronous code. If the first function starts asynchronous work such as a network call, animation, or database request, the next line runs immediately unless you explicitly wait through a completion handler or async and await. The correct fix is to model the first operation as asynchronous, not to block the main thread.

First Identify Whether the Function Is Synchronous

If the first function is truly synchronous, then Swift already waits for it to finish before the next line executes.

swift
1@IBAction func didTapButton(_ sender: UIButton) {
2    validateFields()
3    updateUI()
4}
5
6func validateFields() {
7    print("validation finished")
8}

Here updateUI() runs only after validateFields() returns. No extra control flow is needed.

The real problem appears when the first function starts asynchronous work and returns before that work finishes.

Use a Completion Handler for Callback-Based APIs

If your function finishes later, give it a completion closure and call the next step inside that closure.

swift
1@IBAction func didTapButton(_ sender: UIButton) {
2    loadUserProfile { success in
3        if success {
4            self.showProfile()
5        } else {
6            self.showError()
7        }
8    }
9}
10
11func loadUserProfile(completion: @escaping (Bool) -> Void) {
12    DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
13        let success = true
14
15        DispatchQueue.main.async {
16            completion(success)
17        }
18    }
19}

This is the classic pattern for UIKit-era APIs. The second action runs only after the async work completes.

Prefer async and await in Modern Swift

If you control the async function and your deployment target supports it, async and await usually make the code much clearer.

swift
1@IBAction func didTapButton(_ sender: UIButton) {
2    Task {
3        let success = await loadUserProfile()
4
5        if success {
6            showProfile()
7        } else {
8            showError()
9        }
10    }
11}
12
13func loadUserProfile() async -> Bool {
14    try? await Task.sleep(nanoseconds: 1_000_000_000)
15    return true
16}

This preserves readable top-to-bottom logic without nesting closures deeply.

Do Not Block the Main Thread

A common wrong idea is to "wait" inside the IBAction by using semaphores, sleep calls, or busy waiting. That freezes the UI and can deadlock if the async completion also needs the main thread.

Avoid patterns like:

swift
1@IBAction func didTapButton(_ sender: UIButton) {
2    Thread.sleep(forTimeInterval: 2)
3    updateUI()
4}

or:

swift
let semaphore = DispatchSemaphore(value: 0)

inside UI event handlers. UIKit expects the main thread to stay responsive.

Chain Multiple Steps Explicitly

If one action depends on another and then a third step depends on both, keep the dependency order explicit.

With completions:

swift
1@IBAction func didTapButton(_ sender: UIButton) {
2    validateInput { valid in
3        guard valid else {
4            self.showError()
5            return
6        }
7
8        self.saveRecord {
9            self.refreshScreen()
10        }
11    }
12}

With async and await:

swift
1@IBAction func didTapButton(_ sender: UIButton) {
2    Task {
3        let valid = await validateInput()
4        guard valid else {
5            showError()
6            return
7        }
8
9        await saveRecord()
10        refreshScreen()
11    }
12}

The second style is usually easier to maintain once the project uses Swift concurrency consistently.

Decide Where the State Change Belongs

Sometimes the real issue is not waiting for a function. It is deciding when UI should change. For example, if a button starts a network request, you may want to:

  1. disable the button immediately
  2. show a loading state
  3. re-enable the button only in completion or after await

That makes the sequencing visible to the user and prevents double taps from starting duplicate work.

Common Pitfalls

  • Assuming the next line in an IBAction waits for asynchronous work automatically.
  • Blocking the main thread with sleep calls or semaphores.
  • Calling UI updates from a background queue instead of the main thread.
  • Nesting too many completion handlers without extracting helper methods.
  • Mixing callback style and async plus await inconsistently in the same flow.

Summary

  • Synchronous functions already finish before the next line runs.
  • Asynchronous functions need an explicit completion handler or async plus await.
  • In modern Swift, async plus await is usually the clearest option.
  • Never block the main thread just to force order inside an IBAction.
  • Model the actual dependency chain directly so UI updates happen at the correct time.

Course illustration
Course illustration

All Rights Reserved.