Introduction

Welcome to Chapter 7! Up until now, we’ve focused on building the visual and interactive components of our iOS applications. We’ve learned how to craft beautiful user interfaces, manage application state, and navigate between different screens. But what if your app needs to talk to the outside world? What if it needs to fetch the latest news, display current weather, or save user data to a remote server?

That’s where networking comes in! In this chapter, we’ll unlock the power of connecting your iOS apps to the vast world of the internet. We’ll learn how to fetch data from external services, known as Application Programming Interfaces (APIs), and seamlessly integrate that data into your app. This is a fundamental skill for almost any modern application, transforming static experiences into dynamic, real-time ones.

We’ll dive into Apple’s robust URLSession framework, explore Swift’s elegant Codable protocol for handling data, and embrace the modern async/await concurrency model to keep our apps responsive. By the end of this chapter, you’ll be able to make your app a true citizen of the internet, fetching and displaying dynamic content with confidence.

Before we begin, make sure you’re comfortable with basic Swift syntax, creating UI elements (either UIKit or SwiftUI), and understanding how data flows within your app. Let’s get connected!

Core Concepts: Talking to the Internet

Think of your app as a person who needs to ask questions and get information from an expert. The “expert” is an API (Application Programming Interface), and “talking” involves sending requests and receiving responses over the internet.

What is an API?

An API (Application Programming Interface) is essentially a set of rules and protocols that allows different software applications to communicate with each other. When your app needs data from a service (like weather data, stock prices, or social media posts), it doesn’t directly access the service’s database. Instead, it sends a request to the service’s API, which then fetches the data, processes it, and sends it back to your app in a standardized format.

Most modern APIs use HTTP (Hypertext Transfer Protocol) as their communication backbone, similar to how web browsers communicate with websites.

Common HTTP Methods

When your app talks to an API, it uses specific “verbs” to indicate what kind of operation it wants to perform. These are called HTTP Methods:

  • GET: This is like “asking for information.” You use it to retrieve data from a server. For example, getting a list of products or the current weather.
  • POST: This is like “sending new information.” You use it to send data to the server to create a new resource, like submitting a new user registration or creating a new post.
  • PUT/PATCH: These are for “updating information.” You use them to modify existing data on the server.
  • DELETE: This is for “removing information.” You use it to remove a resource from the server.

In this chapter, we’ll primarily focus on GET requests, as they are the most common for fetching data.

JSON: The Universal Language for Data Exchange

When an API sends data back to your app, it needs a common format that both sides can understand. The most popular format for this is JSON (JavaScript Object Notation).

JSON is a lightweight data-interchange format that is easy for humans to read and write, and easy for machines to parse and generate. It’s essentially a way to represent structured data using key-value pairs and arrays, similar to Swift dictionaries and arrays.

Here’s a quick example of what JSON looks like:

{
  "currency": "USD",
  "rate": 68752.45,
  "lastUpdated": "2026-02-26T10:30:00Z"
}

Our goal will be to take this JSON text and turn it into a Swift struct or class that we can easily work with in our app.

URLSession: Apple’s Networking Powerhouse

URLSession is the cornerstone of networking in Apple’s platforms. It’s a powerful and flexible API for downloading data, uploading data, and interacting with network services. It handles all the low-level details of network communication, letting you focus on what data you need and what to do with it.

At its core, URLSession allows you to create:

  • URL objects: These represent the address of the resource you want to access on the internet (e.g., https://api.example.com/data).
  • URLRequest objects: These encapsulate all the details of your request, including the URL, HTTP method (GET, POST, etc.), headers, and any data you want to send in the request body.
  • URLSessionDataTask (or other task types): These are the actual tasks that perform the network operation. For fetching data, URLSessionDataTask is what we’ll use.

Codable: Bridging JSON and Swift Types

Manually parsing JSON can be tedious and error-prone. Thankfully, Swift offers the incredibly powerful Codable protocol. Codable is a type alias for two protocols: Encodable and Decodable.

  • Decodable: Allows you to convert data (like JSON) into Swift custom types (structs or classes).
  • Encodable: Allows you to convert Swift custom types into data (like JSON) to send to an API.

By simply conforming your Swift struct or class to Codable, Swift can automatically generate the code to convert JSON data into instances of your type, and vice versa, as long as the property names match the JSON keys. This is a huge time-saver and makes working with APIs much cleaner.

Modern Concurrency with Async/Await

Networking operations are inherently asynchronous. This means they don’t happen instantly; they take time because your app is waiting for a response from a remote server. If your app waited silently on the main thread (where UI updates happen), it would freeze and become unresponsive – a terrible user experience!

Traditionally, Swift used completion handlers (closures) to deal with asynchronous code, which could sometimes lead to “callback hell” with nested closures. With Swift 5.5 and later (fully integrated with Swift 6 in Xcode 16+), Apple introduced structured concurrency using async/await.

  • async: Marks a function or method as capable of performing asynchronous work. When you call an async function, it might pause its execution at certain points to wait for results from other async operations (like a network request).
  • await: Marks a point where an async function might pause its execution to wait for an asynchronous result. The await keyword effectively “pauses” the current task until the awaited async operation completes, without blocking the entire thread.

This new model makes asynchronous code look and feel much more like synchronous code, improving readability and maintainability. We’ll use async/await extensively for our network requests.

The Networking Flow

Let’s visualize the journey of a network request from your app to an API and back:

flowchart TD A[Your App] --> B[Fetch Data] B --> C[Construct URL and Request] C --> D[URL Session Shared Data] D --->|Wait| E[Receive Raw Data] E --> F[Decode JSON] F --->|Success| G[Swift Model Object] G --> H[Update UI on Main Actor] D --->|Failure| I[Handle Network Error] F --->|Failure| J[Handle Decoding Error] I --> H J --> H

Figure 7.1: Simplified Network Request Data Flow

  1. Your app calls a function in a NetworkService to fetch data.
  2. The NetworkService constructs a URL and URLRequest.
  3. It then uses URLSession.shared.data(from:) (an async function) to send the request.
  4. Your code awaits the response. During this wait, your app’s main thread remains free to handle UI events.
  5. Once data is received, it’s raw Data (which is typically JSON).
  6. Codable (specifically JSONDecoder) attempts to convert this raw Data into your custom Swift struct.
  7. If successful, you get a beautiful Swift object!
  8. Finally, you update your app’s user interface with the new data, ensuring this happens on the @MainActor.
  9. If anything goes wrong (network error, decoding error), appropriate error handling paths are taken.

Step-by-Step Implementation: Fetching Bitcoin Price

Let’s put these concepts into practice by building a small utility that fetches the current Bitcoin price from the CoinDesk API. This API is public and doesn’t require an API key, making it perfect for a first networking example.

Our Goal:

  1. Define a Swift struct to represent the data we expect from the API.
  2. Create a service to make the network request.
  3. Display the fetched data in a simple SwiftUI view.

We’ll use Xcode 16.x and Swift 6.x, prioritizing modern async/await for concurrency.

Step 1: Create a New Project

Open Xcode and create a new project:

  • Choose iOS -> App.
  • Product Name: CryptoFetcher
  • Interface: SwiftUI (or UIKit, the networking logic is similar, but SwiftUI is concise for display). Let’s go with SwiftUI for modern practices.
  • Language: Swift
  • Make sure Use Core Data is unchecked.

Click Next and save your project.

Step 2: Understand the API Response and Define our Model

First, let’s look at the API endpoint we’ll be using: https://api.coindesk.com/v1/bpi/currentprice.json

If you open this URL in your web browser, you’ll see a JSON response similar to this (values will change):

{
  "time": {
    "updated": "Feb 26, 2026 10:30:00 UTC",
    "updatedISO": "2026-02-26T10:30:00+00:00",
    "updateduk": "Feb 26, 2026 at 10:30 GMT"
  },
  "disclaimer": "This data is provided...",
  "chartName": "Bitcoin",
  "bpi": {
    "USD": {
      "code": "USD",
      "symbol": "$",
      "rate": "68,752.4501",
      "description": "United States Dollar",
      "rate_float": 68752.4501
    },
    "GBP": {
      "code": "GBP",
      "symbol": "£",
      "rate": "54,233.7845",
      "description": "British Pound",
      "rate_float": 54233.7845
    },
    "EUR": {
      "code": "EUR",
      "symbol": "€",
      "rate": "63,201.2345",
      "description": "Euro",
      "rate_float": 63201.2345
    }
  }
}

That’s a lot of data! For our simple app, we just want the rate_float for USD. To get that, we need to navigate through bpi -> USD -> rate_float. We also might want the updatedISO time.

Let’s create Swift structs that mirror this structure, conforming to Codable.

Create a new Swift file named CryptoPrice.swift.

// CryptoPrice.swift

import Foundation

// 1. Top-level struct for the entire JSON response
struct CryptoPriceResponse: Codable {
    let time: TimeInfo
    let bpi: BPIInfo // bpi stands for Bitcoin Price Index
    // We can ignore 'disclaimer' and 'chartName' for this example
}

// 2. Struct for the 'time' object
struct TimeInfo: Codable {
    let updatedISO: String // We only need this specific field
}

// 3. Struct for the 'bpi' object
struct BPIInfo: Codable {
    let USD: CurrencyInfo // Only interested in USD for now
    // We can add GBP and EUR here if needed later
}

// 4. Struct for each currency's information (e.g., USD)
struct CurrencyInfo: Codable {
    let code: String
    let symbol: String
    let rate: String // The formatted rate as a string
    let description: String
    let rate_float: Double // The actual numeric rate we want
}

Explanation:

  • We’ve created a hierarchy of structs (CryptoPriceResponse, TimeInfo, BPIInfo, CurrencyInfo) that directly map to the nested structure of the JSON.
  • By adding : Codable to each struct, Swift automatically synthesizes the code needed to decode JSON into these types.
  • We only include the properties we care about. Codable is smart enough to ignore JSON keys that don’t have a corresponding property in your struct.
  • Notice rate is a String (e.g., “68,752.4501”) while rate_float is a Double (68752.4501). It’s crucial to match the JSON data types.

Step 3: Build the Networking Service

Now, let’s create a service that can fetch this data. We’ll use URLSession.shared and async/await.

Create a new Swift file named CryptoService.swift.

// CryptoService.swift

import Foundation

enum NetworkError: Error, LocalizedError {
    case invalidURL
    case requestFailed(Error)
    case invalidResponse
    case decodingFailed(Error)
    case unknown

    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "The URL provided was invalid."
        case .requestFailed(let error):
            return "Network request failed: \(error.localizedDescription)"
        case .invalidResponse:
            return "The server returned an invalid response."
        case .decodingFailed(let error):
            return "Failed to decode data: \(error.localizedDescription)"
        case .unknown:
            return "An unknown network error occurred."
        }
    }
}

class CryptoService {
    private let baseURL = "https://api.coindesk.com/v1/bpi/currentprice.json"

    // This async function fetches the latest Bitcoin price
    func fetchBitcoinPrice() async throws -> CryptoPriceResponse {
        guard let url = URL(string: baseURL) else {
            // If the URL is invalid, throw our custom error
            throw NetworkError.invalidURL
        }

        do {
            // 1. Perform the network request using async/await
            //    data(from: ) returns a tuple (Data, URLResponse)
            let (data, response) = try await URLSession.shared.data(from: url)

            // 2. Check for a valid HTTP response
            guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                throw NetworkError.invalidResponse
            }

            // 3. Decode the data into our Swift model using JSONDecoder
            let decoder = JSONDecoder()
            // Optional: If JSON keys are in snake_case (e.g., 'rate_float')
            // but Swift properties are camelCase (e.g., 'rateFloat'),
            // you can set a key decoding strategy:
            // decoder.keyDecodingStrategy = .convertFromSnakeCase

            return try decoder.decode(CryptoPriceResponse.self, from: data)

        } catch let urlError as URLError {
            // Catch specific URLSession errors
            throw NetworkError.requestFailed(urlError)
        } catch let decodingError as DecodingError {
            // Catch specific Codable decoding errors
            throw NetworkError.decodingFailed(decodingError)
        } catch {
            // Catch any other unexpected errors
            throw NetworkError.unknown
        }
    }
}

Explanation:

  • NetworkError Enum: We’ve defined a custom error enum that conforms to Error and LocalizedError. This makes error handling much clearer and provides user-friendly descriptions. It’s a best practice to define specific error types for different failure scenarios.
  • CryptoService Class:
    • baseURL: Stores the endpoint URL as a constant.
    • fetchBitcoinPrice(): This is our core networking method.
      • It’s marked async because it performs asynchronous work (network request).
      • It’s marked throws because network operations can fail, and we want to propagate those errors.
      • guard let url = URL(string: baseURL): Safely unwraps the URL. If the baseURL string isn’t a valid URL, we throw NetworkError.invalidURL.
      • try await URLSession.shared.data(from: url): This is the magic of async/await!
        • URLSession.shared is a singleton object that provides a default configuration for network requests.
        • data(from:) is an async method that makes the actual GET request.
        • await pauses the execution of fetchBitcoinPrice() until the network request completes or fails, without blocking the main thread.
        • try indicates that this method can throw an error.
      • guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200: After receiving a response, we cast it to HTTPURLResponse to check the statusCode. A 200 OK status indicates success. Otherwise, we throw NetworkError.invalidResponse.
      • let decoder = JSONDecoder(): Creates an instance of JSONDecoder, which is responsible for decoding JSON data.
      • return try decoder.decode(CryptoPriceResponse.self, from: data): This is where Codable shines! We tell the decoder to attempt to convert the received data into an instance of CryptoPriceResponse. CryptoPriceResponse.self refers to the type itself.
      • do-catch block: We use do-catch to handle various types of errors that can occur during the network request or data decoding. This allows us to throw our custom NetworkError for clearer error reporting.

Step 4: Integrate with SwiftUI View

Now let’s display the fetched price in our ContentView.swift.

// ContentView.swift

import SwiftUI

struct ContentView: View {
    @State private var bitcoinPrice: Double?
    @State private var lastUpdated: String?
    @State private var errorMessage: String?
    @State private var isLoading: Bool = false

    private let cryptoService = CryptoService() // Instantiate our service

    var body: some View {
        VStack(spacing: 20) {
            Text("Current Bitcoin Price")
                .font(.largeTitle)
                .fontWeight(.bold)

            if isLoading {
                ProgressView("Fetching price...")
                    .progressViewStyle(.circular)
            } else if let price = bitcoinPrice {
                Text(String(format: "$%.2f USD", price))
                    .font(.system(size: 40, weight: .semibold, design: .monospaced))
                    .foregroundColor(.green)

                if let updated = lastUpdated {
                    Text("Last updated: \(formattedDate(from: updated))")
                        .font(.caption)
                        .foregroundColor(.gray)
                }
            } else if let error = errorMessage {
                Text("Error: \(error)")
                    .font(.headline)
                    .foregroundColor(.red)
                    .multilineTextAlignment(.center)
            } else {
                Text("Tap 'Fetch Price' to get the latest Bitcoin value.")
                    .font(.title3)
                    .foregroundColor(.secondary)
            }

            Button("Fetch Price") {
                // Call our async function from a Task
                Task {
                    await fetchPrice()
                }
            }
            .buttonStyle(.borderedProminent)
            .controlSize(.large)
        }
        .padding()
        // We can also fetch on appear, but for this example, a button is clearer
        // .onAppear {
        //     Task {
        //         await fetchPrice()
        //     }
        // }
    }

    // This async function updates our @State properties
    private func fetchPrice() async {
        isLoading = true // Start loading
        errorMessage = nil // Clear previous errors
        do {
            let response = try await cryptoService.fetchBitcoinPrice()
            // IMPORTANT: UI updates must happen on the main actor.
            // Since this function is called from a Task, and we're in a View,
            // SwiftUI's @State already handles main actor isolation for us.
            // However, explicitly marking with @MainActor or Task { @MainActor in ... }
            // is good practice if you were in a non-View context.
            bitcoinPrice = response.bpi.USD.rate_float
            lastUpdated = response.time.updatedISO
        } catch {
            // Cast the generic error to our specific NetworkError if possible
            if let networkError = error as? NetworkError {
                errorMessage = networkError.localizedDescription
            } else {
                errorMessage = "An unexpected error occurred: \(error.localizedDescription)"
            }
            bitcoinPrice = nil // Clear previous price on error
            lastUpdated = nil
        }
        isLoading = false // End loading
    }
    
    // Helper to format the ISO date string
    private func formattedDate(from isoDateString: String) -> String {
        let formatter = ISO8601DateFormatter()
        if let date = formatter.date(from: isoDateString) {
            let displayFormatter = DateFormatter()
            displayFormatter.dateStyle = .medium
            displayFormatter.timeStyle = .short
            return displayFormatter.string(from: date)
        }
        return isoDateString // Fallback
    }
}

// Preview Provider for Xcode Canvas
#Preview {
    ContentView()
}

Explanation:

  • @State properties: bitcoinPrice, lastUpdated, errorMessage, and isLoading are declared with @State to make them observable by SwiftUI. When these values change, SwiftUI will re-render the view.
  • cryptoService: An instance of our CryptoService is created.
  • VStack and Conditional Views: The UI dynamically changes based on isLoading, bitcoinPrice, or errorMessage.
  • Button("Fetch Price"):
    • When the button is tapped, it creates a Task. A Task is how you start unstructured asynchronous operations in Swift. It provides an execution context for async functions.
    • Inside the Task, we await fetchPrice().
  • fetchPrice() async function:
    • This function is marked async because it calls cryptoService.fetchBitcoinPrice(), which is also async.
    • isLoading is set to true at the start and false at the end to show/hide the ProgressView.
    • The do-catch block handles potential errors from fetchBitcoinPrice().
    • Updating @State: When bitcoinPrice and lastUpdated are updated within this async function, SwiftUI automatically ensures these UI updates happen on the main actor, keeping your UI responsive.
    • Error messages are captured and displayed in errorMessage.
  • formattedDate helper: A small utility to make the updatedISO string more readable for the user. ISO8601DateFormatter is essential for parsing ISO 8601 date strings.

Now, run your CryptoFetcher app in the simulator or on a device. Tap the “Fetch Price” button, and you should see the current Bitcoin price and its update time appear!

Step 5: Handling Advanced JSON Decoding (Optional but useful)

Sometimes, the JSON keys from an API don’t perfectly match Swift’s camelCase naming convention, or you might want to map a JSON key to a different property name in your struct. Codable provides ways to handle this.

Using CodingKeys: If the JSON key was rate_float but you wanted your Swift property to be floatRate, you would use CodingKeys:

struct CurrencyInfo: Codable {
    let code: String
    // ... other properties ...
    let floatRate: Double // Our preferred Swift property name

    // Define custom mapping for JSON keys to Swift properties
    enum CodingKeys: String, CodingKey {
        case code
        // Map 'rate_float' JSON key to 'floatRate' Swift property
        case floatRate = "rate_float"
        // If a key is not listed here, it's assumed to have the same name
        // (e.g., 'symbol', 'rate', 'description' would still map automatically)
    }
}

This explicit mapping gives you fine-grained control over how JSON keys translate to your Swift model properties, which is incredibly useful for real-world APIs.

Mini-Challenge: Fetch a Different Currency

Your challenge is to modify the CryptoFetcher app to also display the Bitcoin price in Euros (EUR), in addition to USD.

Hints:

  1. Look back at the raw JSON response from https://api.coindesk.com/v1/bpi/currentprice.json. Notice the GBP and EUR objects inside bpi.
  2. You’ll need to modify your BPIInfo struct to include EUR.
  3. You’ll then need to update ContentView to display the EUR price alongside the USD price.
  4. Remember to handle optional values (?) if a currency might not always be present (though for CoinDesk, USD, GBP, EUR are usually there).

Take your time, try to implement it yourself, and don’t be afraid to experiment!

Common Pitfalls & Troubleshooting

  1. Networking on the Main Thread (Blocking UI):

    • Pitfall: Attempting to perform URLSession requests directly on the main thread without async/await or completion handlers. This will freeze your UI, making your app unresponsive.
    • Solution: Always perform network requests asynchronously. With async/await, ensure your network call is within a Task context (e.g., Task { await fetchData() }). Swift’s async/await model handles threading correctly by suspending the task without blocking the underlying thread.
  2. Incorrect Codable Struct Mapping:

    • Pitfall: Your Swift Codable struct properties don’t exactly match the JSON keys, or the data types are mismatched (e.g., expecting a Double but the JSON provides a String). This leads to DecodingError at runtime.
    • Solution:
      • Carefully inspect the JSON structure (use a browser or a JSON formatter tool).
      • Ensure Swift property names match JSON keys exactly (case-sensitive).
      • If names don’t match, use CodingKeys to provide explicit mapping.
      • Verify data types (e.g., String vs. Int vs. Double vs. Bool).
      • Use optional properties (?) in your struct for JSON keys that might sometimes be missing.
      • Print the DecodingError in your catch block (print(decodingError)) for detailed information about where the decoding failed.
  3. Network Connectivity Issues:

    • Pitfall: Your app tries to make a request when there’s no internet connection, or the connection is unstable. This results in URLError (e.g., .notConnectedToInternet).
    • Solution: Implement robust error handling (as shown with NetworkError). You might also consider using Apple’s Network framework (NWPathMonitor) to actively monitor network reachability and inform the user if they’re offline before even attempting a request.
  4. API Rate Limits or Authentication Errors:

    • Pitfall: The API might limit how many requests you can make in a certain period, or it might require an API key or authentication token that you haven’t provided or that has expired. This often results in HTTPURLResponse status codes like 429 Too Many Requests or 401 Unauthorized.
    • Solution:
      • Always check the API documentation for rate limits and authentication requirements.
      • Include necessary headers (e.g., Authorization header with a token, or x-api-key header).
      • Handle specific HTTP status codes in your URLSession response check.

Summary

Congratulations! You’ve taken a significant leap in your iOS development journey by mastering networking. Here’s a quick recap of what we covered:

  • APIs are how apps communicate with external services, typically using HTTP methods like GET to retrieve data.
  • JSON is the standard format for exchanging structured data between APIs and apps.
  • URLSession is Apple’s framework for making network requests, handling the low-level details for you.
  • Codable (Decodable + Encodable) provides an elegant and automatic way to convert JSON data into Swift custom types and vice-versa.
  • Swift’s async/await is the modern approach to handling asynchronous operations, making networking code clean, readable, and non-blocking.
  • Error handling is crucial for robust apps, using custom error types and do-catch blocks to manage network and decoding failures.
  • UI updates must always happen on the main actor, which SwiftUI handles automatically when updating @State properties from a Task.

You now have the fundamental tools to fetch dynamic content and make your apps truly interactive and connected. This skill is indispensable for building almost any real-world application.

In the next chapter, we’ll explore Data Persistence, learning how to store data locally on the device, allowing your apps to function offline or remember user preferences between sessions. This will complement your networking knowledge by providing ways to manage data both remotely and locally.

References

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.