Lesson 4 of 4

Networking and APIs in iOS

Learn to fetch data from REST APIs, handle asynchronous operations with async/await, and integrate web services into your iOS applications.

38 minutes

Networking and APIs in iOS

Modern iOS apps rely on web services and APIs to fetch and update data. Learn to build robust networking layers in your applications.

URLSession

Apple's native networking framework:

  • Built-in HTTP client
  • Support for all HTTP methods (GET, POST, PUT, DELETE)
  • Background downloads and uploads
  • Certificate pinning for security

Async/Await

Swift's modern concurrency model makes asynchronous code clean:

func fetchUser() async throws -> User {
    let url = URL(string: "https://api.example.com/user")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

RESTful API Integration

Common patterns for working with REST APIs:

  • GET: Retrieve data from server
  • POST: Create new resources
  • PUT/PATCH: Update existing resources
  • DELETE: Remove resources

Error Handling

Robust networking requires proper error handling:

  • Network connectivity issues
  • Server errors (4xx, 5xx status codes)
  • Invalid response data
  • Timeout scenarios

JSON Decoding

Swift's Codable makes API responses type-safe:

struct ApiResponse: Codable {
    let data: [Item]
    let success: Bool
    let message: String?
}

Best Practices

  • Use async/await for cleaner asynchronous code
  • Implement proper error handling
  • Cache responses when appropriate
  • Handle loading and error states in UI
  • Use environment-specific API endpoints
  • Implement request retry logic
  • Add authentication headers securely
  • Test with mock data during development

Code Example

import SwiftUI

// Data Models
struct Post: Identifiable, Codable {
    let id: Int
    let title: String
    let body: String
    let userId: Int
}

// Network Service
class NetworkService {
    static let shared = NetworkService()
    private let baseURL = "https://jsonplaceholder.typicode.com"
    
    func fetchPosts() async throws -> [Post] {
        guard let url = URL(string: "\(baseURL)/posts") else {
            throw NetworkError.invalidURL
        }
        
        let (data, response) = try await URLSession.shared.data(from: url)
        
        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw NetworkError.serverError
        }
        
        let posts = try JSONDecoder().decode([Post].self, from: data)
        return posts
    }
    
    func createPost(title: String, body: String) async throws -> Post {
        guard let url = URL(string: "\(baseURL)/posts") else {
            throw NetworkError.invalidURL
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let postData = ["title": title, "body": body, "userId": 1] as [String : Any]
        request.httpBody = try JSONSerialization.data(withJSONObject: postData)
        
        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(Post.self, from: data)
    }
}

enum NetworkError: Error {
    case invalidURL
    case serverError
    case decodingError
}

// ViewModel
class PostsViewModel: ObservableObject {
    @Published var posts: [Post] = []
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    func loadPosts() async {
        isLoading = true
        errorMessage = nil
        
        do {
            posts = try await NetworkService.shared.fetchPosts()
        } catch {
            errorMessage = "Failed to load posts: \(error.localizedDescription)"
        }
        
        isLoading = false
    }
}

// SwiftUI View
struct PostsListView: View {
    @StateObject private var viewModel = PostsViewModel()
    
    var body: some View {
        NavigationView {
            Group {
                if viewModel.isLoading {
                    ProgressView("Loading posts...")
                } else if let error = viewModel.errorMessage {
                    VStack {
                        Text(error)
                            .foregroundColor(.red)
                        Button("Retry") {
                            Task {
                                await viewModel.loadPosts()
                            }
                        }
                    }
                } else {
                    List(viewModel.posts) { post in
                        VStack(alignment: .leading, spacing: 8) {
                            Text(post.title)
                                .font(.headline)
                            Text(post.body)
                                .font(.subheadline)
                                .foregroundColor(.gray)
                                .lineLimit(2)
                        }
                    }
                }
            }
            .navigationTitle("Posts")
            .task {
                await viewModel.loadPosts()
            }
        }
    }
}