Skip to content

Guide to create a Network Manager in Swift

June 10, 2025

The NetworkManager class is a reusable component for handling HTTP network requests in Swift, leveraging URLSession and async/await for modern, asynchronous networking. This document covers creating a NetworkManager to perform GET, DELETE, POST, and PATCH requests with JSON encoding/decoding, and demonstrates its use with a UITableView powered by a diffable data source for dynamic UI updates.

Overview

NetworkManager encapsulates networking logic, handling URL construction, request execution, and JSON serialization. It uses Swift’s concurrency model (async/await) for clean, readable code and includes robust error handling. The example integrates with a UITableView using NSDiffableDataSource to display and manage a list of resources efficiently.

Network Manager Implementation

Below is the NetworkManager class, which supports GET, DELETE, POST, and PATCH requests with JSON handling.

swift
import Foundation

enum NetworkError: Error, LocalizedError {
    case invalidURL
    case invalidResponse
    case serverError(statusCode: Int)
    case decodingError(Error)
    case encodingError(Error)
    
    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "Invalid URL provided."
        case .invalidResponse:
            return "Invalid response from server."
        case .serverError(let statusCode):
            return "Server error with status code: \(statusCode)."
        case .decodingError(let error):
            return "Decoding failed: \(error.localizedDescription)"
        case .encodingError(let error):
            return "Encoding failed: \(error.localizedDescription)"
        }
    }
}

class NetworkManager {
    private let baseURL: String
    private let session: URLSession
    
    init(baseURL: String, session: URLSession = .shared) {
        self.baseURL = baseURL
        self.session = session
    }
    
    private func createRequest(_ endpoint: String, method: String, body: Data? = nil) throws -> URLRequest {
        guard let url = URL(string: "\(baseURL)\(endpoint)") else {
            throw NetworkError.invalidURL
        }
        var request = URLRequest(url: url)
        request.httpMethod = method
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = body
        return request
    }
}

Sending GET Network Requests and JSON Decoding

GET requests fetch data from a server, and JSON decoding converts the response into Swift types.

Example:

swift
extension NetworkManager {
    func get<T: Decodable>(endpoint: String) async throws -> T {
        let request = try createRequest(endpoint, method: "GET")
        let (data, response) = try await session.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
            throw NetworkError.serverError(statusCode: (response as? HTTPURLResponse)?.statusCode ?? 0)
        }
        
        do {
            return try JSONDecoder().decode(T.self, from: data)
        } catch {
            throw NetworkError.decodingError(error)
        }
    }
}

Usage:

swift
struct Post: Codable, Identifiable {
    let id: Int
    let title: String
    let body: String
}

let networkManager = NetworkManager(baseURL: "https://jsonplaceholder.typicode.com")
Task {
    do {
        let posts: [Post] = try await networkManager.get(endpoint: "/posts")
        print(posts)
    } catch {
        print(error.localizedDescription)
    }
}

Sending DELETE Network Requests

DELETE requests remove resources from the server.

Example:

swift
extension NetworkManager {
    func delete(endpoint: String) async throws -> Bool {
        let request = try createRequest(endpoint, method: "DELETE")
        let (_, response) = try await session.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
            throw NetworkError.serverError(statusCode: (response as? HTTPURLResponse)?.statusCode ?? 0)
        }
        return true
    }
}

Usage:

swift
Task {
    do {
        let success = try await networkManager.delete(endpoint: "/posts/1")
        print("Delete successful: \(success)")
    } catch {
        print(error.localizedDescription)
    }
}

Sending POST Network Requests and JSON Encoding

POST requests create new resources, encoding Swift types into JSON.

Example:

swift
extension NetworkManager {
    func post<T: Encodable, U: Decodable>(endpoint: String, body: T) async throws -> U {
        do {
            let data = try JSONEncoder().encode(body)
            let request = try createRequest(endpoint, method: "POST", body: data)
            let (responseData, response) = try await session.data(for: request)
            
            guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
                throw NetworkError.serverError(statusCode: (response as? HTTPURLResponse)?.statusCode ?? 0)
            }
            
            do {
                return try JSONDecoder().decode(U.self, from: responseData)
            } catch {
                throw NetworkError.decodingError(error)
            }
        } catch {
            throw NetworkError.encodingError(error)
        }
    }
}

Usage:

swift
let newPost = Post(id: 101, title: "New Post", body: "This is a test.")
Task {
    do {
        let createdPost: Post = try await networkManager.post(endpoint: "/posts", body: newPost)
        print(createdPost)
    } catch {
        print(error.localizedDescription)
    }
}

Sending PATCH Network Requests

PATCH requests update existing resources partially.

Example:

swift
extension NetworkManager {
    func patch<T: Encodable, U: Decodable>(endpoint: String, body: T) async throws -> U {
        do {
            let data = try JSONEncoder().encode(body)
            let request = try createRequest(endpoint, method: "PATCH", body: data)
            let (responseData, response) = try await session.data(for: request)
            
            guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
                throw NetworkError.serverError(statusCode: (response as? HTTPURLResponse)?.statusCode ?? 0)
            }
            
            do {
                return try JSONDecoder().decode(U.self, from: responseData)
            } catch {
                throw NetworkError.decodingError(error)
            }
        } catch {
            throw NetworkError.encodingError(error)
        }
    }
}

Usage:

swift
let updatedPost = Post(id: 1, title: "Updated Title", body: "Updated body.")
Task {
    do {
        let patchedPost: Post = try await networkManager.patch(endpoint: "/posts/1", body: updatedPost)
        print(patchedPost)
    } catch {
        print(error.localizedDescription)
    }
}

Practical Example: UITableView with Diffable Data Source

Below is a complete example integrating NetworkManager with a UITableView using NSDiffableDataSource to display, add, update, and delete posts. The example uses the JSONPlaceholder API for mock data.

swift
import UIKit

class PostsViewController: UIViewController {
    enum Section { case main }
    
    private let tableView = UITableView(frame: .zero, style: .plain)
    private var dataSource: UITableViewDiffableDataSource<Section, Post>!
    private let networkManager = NetworkManager(baseURL: "https://jsonplaceholder.typicode.com")
    
    private var posts: [Post] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
        setupDataSource()
        setupNavigation()
        fetchPosts()
    }
    
    private func setupTableView() {
        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView)
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
    }
    
    private func setupDataSource() {
        dataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, post in
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
            var config = cell.defaultContentConfiguration()
            config.text = post.title
            config.secondaryText = post.body
            cell.contentConfiguration = config
            return cell
        }
    }
    
    private func setupNavigation() {
        navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addPost))
    }
    
    private func updateSnapshot() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Post>()
        snapshot.appendSections([.main])
        snapshot.appendItems(posts)
        dataSource.apply(snapshot, animatingDifferences: true)
        
        // Update empty state
        if posts.isEmpty {
            var config = UIContentUnavailableConfiguration.empty()
            config.text = "No Posts"
            config.secondaryText = "Add a post to get started."
            self.contentUnavailableConfiguration = config
        } else {
            self.contentUnavailableConfiguration = nil
        }
    }
    
    private func fetchPosts() {
        Task { @MainActor in
            do {
                self.posts = try await networkManager.get(endpoint: "/posts")
                updateSnapshot()
            } catch {
                showError(error)
            }
        }
    }
    
    @objc private func addPost() {
        let newPost = Post(id: (posts.max(by: { $0.id < $1.id })?.id ?? 0) + 1, title: "New Post", body: "This is a new post.")
        Task { @MainActor in
            do {
                let createdPost: Post = try await networkManager.post(endpoint: "/posts", body: newPost)
                posts.append(createdPost)
                updateSnapshot()
            } catch {
                showError(error)
            }
        }
    }
    
    private func deletePost(_ post: Post) {
        Task { @MainActor in
            do {
                let success = try await networkManager.delete(endpoint: "/posts/\(post.id)")
                if success {
                    posts.removeAll { $0.id == post.id }
                    updateSnapshot()
                }
            } catch {
                showError(error)
            }
        }
    }
    
    private func updatePost(_ post: Post) {
        let updatedPost = Post(id: post.id, title: "Updated: \(post.title)", body: post.body)
        Task { @MainActor in
            do {
                let patchedPost: Post = try await networkManager.patch(endpoint: "/posts/\(post.id)", body: updatedPost)
                if let index = posts.firstIndex(where: { $0.id == post.id }) {
                    posts[index] = patchedPost
                    updateSnapshot()
                }
            } catch {
                showError(error)
            }
        }
    }
    
    private func showError(_ error: Error) {
        var config = UIContentUnavailableConfiguration.empty()
        config.text = "Error"
        config.secondaryText = error.localizedDescription
        config.button = .primary(action: UIAction(title: "Retry") { [weak self] _ in
            self?.fetchPosts()
        })
        self.contentUnavailableConfiguration = config
    }
}

extension PostsViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        let post = posts[indexPath.row]
        let alert = UIAlertController(title: "Post Options", message: nil, preferredStyle: .actionSheet)
        alert.addAction(UIAlertAction(title: "Update", style: .default) { _ in
            self.updatePost(post)
        })
        alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { _ in
            self.deletePost(post)
        })
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        present(alert, animated: true)
    }
}

Explanation of the Example

  • NetworkManager: Handles HTTP requests (GET, DELETE, POST, PATCH) with JSON encoding/decoding, using async/await for modern concurrency.
  • Post Model: A Codable struct representing a post with id, title, and body.
  • UITableView with Diffable Data Source:
    • Uses NSDiffableDataSource for efficient, animated updates.
    • Displays posts with title and body in each cell.
    • Supports adding, updating, and deleting posts via network requests.
  • Empty State: Uses UIContentUnavailableConfiguration to show an empty state or errors, with a retry button for errors.
  • User Interaction:
    • A "+" button adds new posts.
    • Tapping a cell shows an action sheet to update or delete the post.
  • Error Handling: Displays errors using UIContentUnavailableConfiguration and allows retrying failed requests.

Best Practices

  • Use Async/Await: Prefer over callbacks for cleaner code.
  • Centralized Networking: Encapsulate logic in NetworkManager for reusability.
  • Type-Safe JSON: Use Codable for robust serialization.
  • Error Handling: Provide user-friendly error messages with retry options.
  • Diffable Data Source: Use for smooth table/collection view updates.
  • MainActor: Ensure UI updates are thread-safe with @MainActor.
  • Weak References: Use [weak self] in async closures to avoid retain cycles.

Troubleshooting

  • Network Failures: Log HTTP status codes and validate URLs.
  • Decoding Errors: Verify JSON matches the Codable model.
  • UI Not Updating: Ensure updateSnapshot is called on the main thread.
  • Retain Cycles: Check for strong references in async closures.
  • Empty State Issues: Verify contentUnavailableConfiguration is set/cleared correctly.
  • API Limitations: JSONPlaceholder returns mock data; real APIs may require authentication or additional headers.

Additional Notes

  • The example uses https://jsonplaceholder.typicode.com for mock API responses.
  • Extend NetworkManager with authentication headers or custom configurations for production use.
  • Test with real APIs and edge cases (e.g., network failures, invalid JSON).
  • Profile performance with Instruments for large datasets or frequent updates.

This example demonstrates a production-ready approach to networking and UI integration, suitable for iOS apps requiring dynamic data display.

Released under the MIT License.