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.
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:
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:
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:
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:
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:
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:
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:
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:
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.
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 withid
,title
, andbody
. - 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.
- Uses
- 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.