Using Async Calls in SwiftUI – Real Project Walkthrough

Introducing the App Idea + What We Want to Do

In this project, we want to call a remote API that gives us the current date, and display every date fetched in a list. Each time we hit refresh, we’ll add a new item to the list. Let’s start by creating the function that fetches this data.

Writing the 'populateDates()' Function

populateDates() is our main logic for fetching and storing new data.

fetch one item from the API and add it to the @State array that we’ll show as list item

func populateDates() async {
    do {
        guard let currentDate = try await getDate() else { return }
        self.currentDates.append(currentDate)
    } catch {
        print("Error: \(error)")
    }
}

This is just a function for now — we’ll soon see how to call it correctly inside a SwiftUI view.”

The API Call InsidegetDate()’

  • It talks to the network

  • Uses Swift’s built-in async ‘URLSession()’

  • Returns a decoded ‘CurrentDate‘

func getDate() async throws -> CurrentDate? {
    guard let url = URL(string: "https://ember-sparkly-rule.glitch.me/current-date") else {
        fatalError("Invalid URL")
    }
    let (data, _) = try await URLSession.shared.data(from: url)
    return try? JSONDecoder().decode(CurrentDate.self, from: data)
}

“We define an async function that makes the network request, waits for a response, and decodes the result. It uses Swift’s powerful async/await to keep things clean.

Calling the Async Function — But How?

Beginner problem:

This is a common beginner issue — you have an async function, but you can’t use await inside a regular closure like .onAppear {} and Button(action:) So how do we call it?”

We must use this inside a task{} closure

Use .task {} on the View

.task {
    await populateDates()
}

“.task is lifecycle-aware — it only runs once when the view appears, and it’s cancelable when the view disappears. Perfect for one-time loading tasks!”

Button(action: {
    async {
        await populateDates()
    }
}) {
    Image(systemName: "arrow.clockwise.circle")
}

“To call an async function from a regular SwiftUI closure, we use Task {} or async {} — this creates a small async environment and safely calls our function.”


Here’s our Complete view

var body: some View {
    NavigationView {
        List(currentDates) { currentDate in
            Text(currentDate.date)
        }
        .listStyle(.plain)
        .navigationTitle("Dates")
        .navigationBarItems(trailing:
            Button(action: {
                async {
                    await populateDates()
                }
            }, label: {
                Image(systemName: "arrow.clockwise.circle")
            })
        )
        .task {
            await populateDates()
        }
    }
}

This is ready for you to show as the final working UI that:

  • Automatically fetches data on view load (.task)

  • Lets user manually fetch more data via refresh button (with async call)

I hope you’ve successfully completed your first small app using concurrency — and now you understand how powerful and clean async code can be inside SwiftUI.

In the next part, we’ll go one step further — handling loading states, errors, and maybe even canceling tasks the smart way. Stay tuned!”

0
Subscribe to my newsletter

Read articles from Tabassum Akter Nusrat directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tabassum Akter Nusrat
Tabassum Akter Nusrat

Former Android dev at Samsung R&D, now diving deep into iOS development. Writing daily about Swift Concurrency, SwiftUI, and the journey of building real-world apps. Passionate about clean architecture, mental health tech, and learning in public.