Towards Dev

A publication for sharing projects, ideas, codes, and new theories.

Follow publication

The Complete Guide to API Calling in SwiftUI: 4 Approaches Explained

--

Hey there, fellow iOS developers! If you’ve been building apps for any length of time, you know that talking to APIs is pretty much unavoidable. Whether you’re fetching data from a server, submitting user information, or integrating with third-party services, knowing how to make network calls is an essential skill.

Today, I want to walk you through the four main approaches to API calling in SwiftUI and iOS. I’ve built a demo app that showcases all these methods side by side, and I’m going to break down each approach in simple terms. Let’s dive in!

Why Different Methods Matter

Before we jump into the code, you might be wondering: “Why do we need multiple ways to do the same thing?” Great question!

Each approach has its own strengths and weaknesses:

  • Some are more modern and concise
  • Others offer better compatibility with older codebases
  • Some give you powerful reactive programming capabilities
  • And certain methods might be preferred in specific team environments

Understanding all these approaches makes you a more versatile developer and helps you choose the right tool for the job.

Our Demo App

To illustrate these methods, I’ve created a simple app that fetches posts from a public API (JSONPlaceholder) using four different approaches. Each approach accomplishes the same thing but uses different Swift patterns.

First, here’s our model that we’ll use for all examples:

struct PostModel: Identifiable, Codable {
let id: Int
let userId: Int
let title: String
let body: String
}

And now, let’s dive into each method!

Method 1: Async/Await (Swift Concurrency)

Introduced in Swift 5.5, async/await is the most modern approach to handling asynchronous code. It makes your networking code read almost like synchronous code, which is a huge win for readability.

// Method 1: Using async/await
func fetchPostsAsync() async {
isLoading = true
errorMessage = nil

guard let url = URL(string: apiURL) else {
errorMessage = "Invalid URL"
isLoading = false
return
}

do {
let (data, _) = try await URLSession.shared.data(from: url)
posts = try JSONDecoder().decode([PostModel].self, from: data)
isLoading = false
} catch {
errorMessage = "Error: \(error.localizedDescription)"
isLoading = false
}
}

Calling it from SwiftUI:

Button("Fetch with Async/Await") {
Task {
await viewModel.fetchPostsAsync()
}
}

Pros:

  • Clean, readable code that resembles synchronous code
  • No callback hell or complex nesting
  • Built into Swift, no third-party dependencies
  • Excellent error handling with try/catch

Cons:

  • Requires iOS 15+ (or backporting with third-party libraries)
  • Requires learning some new concepts if you’re used to completion handlers

Method 2: Completion Handlers

This is the traditional way of handling asynchronous operations in Swift and has been around since the early days. Completion handlers (or callbacks) are simply functions that get called when an asynchronous operation completes.

// Method 2: Using completion handlers
func fetchPostsWithCompletionHandler(completion: @escaping (Bool) -> Void) {
isLoading = true
errorMessage = nil

guard let url = URL(string: apiURL) else {
errorMessage = "Invalid URL"
isLoading = false
completion(false)
return
}

URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let self = self else { return }

if let error = error {
DispatchQueue.main.async {
self.errorMessage = "Error: \(error.localizedDescription)"
self.isLoading = false
completion(false)
}
return
}

guard let data = data else {
DispatchQueue.main.async {
self.errorMessage = "No data received"
self.isLoading = false
completion(false)
}
return
}

do {
let decodedData = try JSONDecoder().decode([PostModel].self, from: data)
DispatchQueue.main.async {
self.posts = decodedData
self.isLoading = false
completion(true)
}
} catch {
DispatchQueue.main.async {
self.errorMessage = "Decoding error: \(error.localizedDescription)"
self.isLoading = false
completion(false)
}
}
}.resume()
}

Calling it from SwiftUI:

Button("Fetch with Completion Handler") {
viewModel.fetchPostsWithCompletionHandler { success in
if success {
print("Data fetched successfully")
}
}
}

Pros:

  • Works on all iOS versions
  • Widely understood by iOS developers
  • Explicit thread management with DispatchQueue

Cons:

  • More verbose and nested than other approaches
  • Can lead to “callback hell” in complex scenarios
  • Requires manual thread management
  • Memory management requires care (weak self)

Method 3: Combine Framework

Introduced in iOS 13, Combine provides a declarative Swift API for processing values over time. It’s Apple’s take on reactive programming and works beautifully with SwiftUI.

// Method 3: Using Combine
func fetchPostsWithCombine() {
isLoading = true
errorMessage = nil

guard let url = URL(string: apiURL) else {
errorMessage = "Invalid URL"
isLoading = false
return
}

URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [PostModel].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] completion in
guard let self = self else { return }
self.isLoading = false

switch completion {
case .finished:
break
case .failure(let error):
self.errorMessage = "Error: \(error.localizedDescription)"
}
}, receiveValue: { [weak self] value in
self?.posts = value
})
.store(in: &cancellables)
}

Calling it from SwiftUI:

Button("Fetch with Combine") {
viewModel.fetchPostsWithCombine()
}

Pros:

  • Declarative and chainable operations
  • Built-in thread management with receive(on:)
  • Excellent for complex data flows or transformations
  • Great for responding to multiple events over time

Cons:

  • Steeper learning curve for beginners
  • Requires iOS 13+
  • Need to manage subscription cancellation
  • Sometimes overkill for simple operations

Method 4: Alamofire

Alamofire is one of the most popular third-party networking libraries for Swift. It provides a higher-level interface over URLSession with many convenience features.

// Method 4: Using Alamofire
func fetchPostsWithAlamofire() {
isLoading = true
errorMessage = nil

AF.request(apiURL)
.validate()
.responseDecodable(of: [PostModel].self) { [weak self] response in
guard let self = self else { return }
self.isLoading = false

switch response.result {
case .success(let posts):
self.posts = posts
case .failure(let error):
self.errorMessage = "Alamofire Error: \(error.localizedDescription)"
}
}
}

Calling it from SwiftUI:

Button("Fetch with Alamofire") {
viewModel.fetchPostsWithAlamofire()
}

Pros:

  • Concise, readable syntax
  • Built-in validation, response handling, and error mapping
  • Extensive features for complex networking needs
  • Active community and good documentation

Cons:

  • External dependency (needs to be managed with SPM, CocoaPods, etc.)
  • Slightly bigger app size due to the dependency
  • One more library to keep updated
  • May be restricted in some enterprise environments that limit third-party code

Which Method Should You Choose?

Now for the big question: which approach should you use? Here’s my take:

  • For new projects targeting iOS 15+: Go with async/await. It’s the most modern, readable approach and is clearly the future of Swift concurrency.
  • For projects that need iOS 13+ support: Combine + SwiftUI is a great pairing and provides powerful reactive capabilities.
  • For projects with legacy support (pre-iOS 13): Stick with completion handlers, as they work everywhere.
  • For complex networking needs: Consider Alamofire, especially if you’re dealing with authentication, complex request chains, or need advanced features.

In my own projects, I’m increasingly using async/await for simple requests and Combine for more complex data flows, especially where user interface updates are involved.

Full ViewModel Implementation

For those who want to see how to put it all together, here’s a complete ViewModel implementation that includes all four methods:

class PostsViewModel: ObservableObject {
@Published var posts: [PostModel] = []
@Published var isLoading = false
@Published var errorMessage: String?

private var cancellables = Set<AnyCancellable>()
private let apiURL = "https://jsonplaceholder.typicode.com/posts"

// All four methods implemented here...
// (refer to the code samples above)

// Reset state for new request
func reset() {
posts = []
errorMessage = nil
}
}

SwiftUI View Implementation

And here’s a simple SwiftUI view that lets you try out all four methods:

struct APIDemoView: View {
@StateObject private var viewModel = PostsViewModel()
@State private var selectedMethod = 0

let methods = ["Async/Await", "Completion Handler", "Combine", "Alamofire"]

var body: some View {
NavigationView {
VStack {
// Method Selection
Picker("API Method", selection: $selectedMethod) {
ForEach(0..<methods.count, id: \.self) { index in
Text(methods[index]).tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()

// Fetch Button
Button(action: {
fetchData()
}) {
Text("Fetch Posts")
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
.padding()
.disabled(viewModel.isLoading)

// Loading & Error States
if viewModel.isLoading {
ProgressView()
}

if let errorMessage = viewModel.errorMessage {
Text(errorMessage)
.foregroundColor(.red)
}

// Results List
List(viewModel.posts) { post in
VStack(alignment: .leading) {
Text(post.title)
.font(.headline)
Text(post.body)
.font(.subheadline)
.lineLimit(2)
}
}
}
.navigationTitle("API Methods")
}
}

private func fetchData() {
// Call the appropriate method based on selection
switch selectedMethod {
case 0:
Task { await viewModel.fetchPostsAsync() }
case 1:
viewModel.fetchPostsWithCompletionHandler { _ in }
case 2:
viewModel.fetchPostsWithCombine()
case 3:
viewModel.fetchPostsWithAlamofire()
default:
break
}
}
}

Conclusion

Understanding different networking approaches gives you flexibility as a Swift developer. While the newest methods like async/await are exciting and offer significant improvements, there’s no single “best” approach for all situations.

My recommendation? Learn all four approaches, understand their trade-offs, and then choose the right one for your specific project needs.

Happy coding! Feel free to reach out with questions in the comments.

P.S. Want to see the full code for this demo app? Check out my GitHub repo [APICallingMethodsApp] where you can download the complete project.

If you found this guide helpful, follow me for more iOS development insights and tutorials:

🔗 LinkedIn
🔗 GitHub
🔗 Twitter

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

No responses yet

Write a response