Suppose API is : "<https://jsonplaceholder.typicode.com/users>"
Step 1 : Model
Create Model Folder
Create User.swift
```
import Foundation
struct User: Codable {
let id: Int
let name: String
let username: String
let email: String
let phone: String
let website: String
}
```
Step 2: NetWork
Create NetWork Folder
Create NetworkError.swift
```
import Foundation
enum NetworkError: Error {
case invalidURL
case invalidResponse
case serverError(Int)
case decodingError
case unknown
}
```
Create NetworkService.swift
```
import Foundation
final class NetworkService: NetworkServiceProtocol {
private let urlString = "https://jsonplaceholder.typicode.com/users"
func fetchUsers() async throws -> [User] {
// Create URL
guard let url = URL(string: urlString) else {
throw NetworkError.invalidURL
}
// API call
let (data, response) = try await URLSession.shared.data(from: url)
// Check response
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
// Check status code
guard 200...299 ~= httpResponse.statusCode else {
throw NetworkError.serverError(httpResponse.statusCode)
}
// Parse JSON
do {
let users = try JSONDecoder().decode(
[User].self,
from: data
)
return users
} catch {
throw NetworkError.decodingError
}
}
}
```
Create NetworkServiceProtocol.swift
```
import Foundation
protocol NetworkServiceProtocol {
func fetchUsers() async throws -> [User]
}
```
Step 3: ViewModels
Create ViewModels Folder
Create UsersViewModel.swift
```
import Foundation
@MainActor
final class UsersViewModel {
// MARK: - Properties
private let networkService: NetworkServiceProtocol
private var fetchTask: Task<Void, Never>?
private(set) var users: [User] = []
private(set) var filteredUsers: [User] = []
// MARK: - Callbacks
var onUsersUpdated: (() -> Void)?
var onLoadingChanged: ((Bool) -> Void)?
var onError: ((String) -> Void)?
// MARK: - Init
init(networkService: NetworkServiceProtocol) {
self.networkService = networkService
}
// MARK: - Fetch Users
func fetchUsers() {
// Cancel previous request
fetchTask?.cancel()
onLoadingChanged?(true)
fetchTask = Task { [weak self] in
guard let self else {
return
}
do {
let users =
try await networkService.fetchUsers()
// Check cancellation
try Task.checkCancellation()
self.users = users
self.filteredUsers = users
self.onLoadingChanged?(false)
self.onUsersUpdated?()
} catch is CancellationError {
self.onLoadingChanged?(false)
} catch {
self.onLoadingChanged?(false)
self.onError?(
"Unable to load users. Please try again."
)
}
}
}
// MARK: - Cancel
func cancelRequest() {
fetchTask?.cancel()
fetchTask = nil
}
deinit {
fetchTask?.cancel()
}
}
```
Step : 4 Views
Create Views Folder
Create custom cell using XIB UserTableViewCell
```
import UIKit
class UserTableViewCell: UITableViewCell {
@IBOutlet weak var nameLBL: UILabel!
@IBOutlet weak var usernameLBL: UILabel!
@IBOutlet weak var emailLBL: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func configure(with user: User) {
nameLBL.text = user.name
usernameLBL.text = "@\(user.username)"
emailLBL.text = user.email
}
}
```
Step : 5 ViewController
Create Controller Folder
```
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var TBL: UITableView!
private let activityIndicator = UIActivityIndicatorView(
style: .large
)
private let viewModel: UsersViewModel
// MARK: - Init
init(viewModel: UsersViewModel) {
self.viewModel = viewModel
super.init(
nibName: nil,
bundle: nil
)
}
required init?(coder: NSCoder) {
let networkService = NetworkService()
self.viewModel = UsersViewModel(
networkService: networkService
)
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupActivityIndicator()
setupBindings()
viewModel.fetchUsers()
}
override func viewDidDisappear(
_ animated: Bool
) {
super.viewDidDisappear(animated)
viewModel.cancelRequest()
}
}
extension ViewController {
private func setupUI() {
view.backgroundColor = .systemBackground
title = "Users"
TBL.register( UINib( nibName: "UserTableViewCell", bundle: nil ), forCellReuseIdentifier: "UserTableViewCell")
TBL.rowHeight = UITableView.automaticDimension
TBL.estimatedRowHeight = 80
}
}
//TBL
extension ViewController: UITableViewDataSource {
func tableView( _ tableView: UITableView, numberOfRowsInSection section: Int ) -> Int {
return viewModel.users.count
}
func tableView( _ tableView: UITableView, cellForRowAt indexPath: IndexPath ) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell( withIdentifier: "UserTableViewCell", for: indexPath ) as? UserTableViewCell else {
return UITableViewCell()
}
let user = viewModel.users[indexPath.row]
cell.configure(with: user)
return cell
}
}
// activityIndicator
extension ViewController {
private func setupActivityIndicator() {
view.addSubview(activityIndicator)
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(
equalTo: view.centerXAnchor
),
activityIndicator.centerYAnchor.constraint(
equalTo: view.centerYAnchor
)
])
}
}
//setupBindings
extension ViewController {
private func setupBindings() {
viewModel.onUsersUpdated = { [weak self] in
self?.TBL.reloadData()
}
viewModel.onLoadingChanged = {
[weak self] isLoading in
if isLoading {
self?.activityIndicator.startAnimating()
} else {
self?.activityIndicator.stopAnimating()
}
}
viewModel.onError = {
[weak self] message in
self?.showError(message)
}
}
private func showError(_ message: String) {
let alert = UIAlertController(
title: "Error",
message: message,
preferredStyle: .alert
)
alert.addAction(
UIAlertAction(
title: "Retry",
style: .default
) { [weak self] _ in
self?.viewModel.fetchUsers()
}
)
alert.addAction(
UIAlertAction(
title: "Cancel",
style: .cancel
)
)
present(
alert,
animated: true
)
}
}
```
Suppose API is : "<https://jsonplaceholder.typicode.com/users>"
Step 1 : Model
Create Model Folder
Create User.swift
```
import Foundation
struct User: Codable {
let id: Int
let name: String
let username: String
let email: String
let phone: String
let website: String
}
```
Step 2: NetWork
Create NetWork Folder
Create NetworkError.swift
```
import Foundation
enum NetworkError: Error {
case invalidURL
case invalidResponse
case serverError(Int)
case decodingError
case unknown
}
```
Create NetworkService.swift
```
import Foundation
final class NetworkService: NetworkServiceProtocol {
private let urlString = "https://jsonplaceholder.typicode.com/users"
func fetchUsers() async throws -> [User] {
// Create URL
guard let url = URL(string: urlString) else {
throw NetworkError.invalidURL
}
// API call
let (data, response) = try await URLSession.shared.data(from: url)
// Check response
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
// Check status code
guard 200...299 ~= httpResponse.statusCode else {
throw NetworkError.serverError(httpResponse.statusCode)
}
// Parse JSON
do {
let users = try JSONDecoder().decode(
[User].self,
from: data
)
return users
} catch {
throw NetworkError.decodingError
}
}
}
```
Create NetworkServiceProtocol.swift
```
import Foundation
protocol NetworkServiceProtocol {
func fetchUsers() async throws -> [User]
}
```
Step 3: ViewModels
Create ViewModels Folder
Create UsersViewModel.swift
```
import Foundation
@MainActor
final class UsersViewModel {
// MARK: - Properties
private let networkService: NetworkServiceProtocol
private var fetchTask: Task<Void, Never>?
private(set) var users: [User] = []
private(set) var filteredUsers: [User] = []
// MARK: - Callbacks
var onUsersUpdated: (() -> Void)?
var onLoadingChanged: ((Bool) -> Void)?
var onError: ((String) -> Void)?
// MARK: - Init
init(networkService: NetworkServiceProtocol) {
self.networkService = networkService
}
// MARK: - Fetch Users
func fetchUsers() {
// Cancel previous request
fetchTask?.cancel()
onLoadingChanged?(true)
fetchTask = Task { [weak self] in
guard let self else {
return
}
do {
let users =
try await networkService.fetchUsers()
// Check cancellation
try Task.checkCancellation()
self.users = users
self.filteredUsers = users
self.onLoadingChanged?(false)
self.onUsersUpdated?()
} catch is CancellationError {
self.onLoadingChanged?(false)
} catch {
self.onLoadingChanged?(false)
self.onError?(
"Unable to load users. Please try again."
)
}
}
}
// MARK: - Cancel
func cancelRequest() {
fetchTask?.cancel()
fetchTask = nil
}
deinit {
fetchTask?.cancel()
}
}
```
Step : 4 Views
Create Views Folder
Create custom cell using XIB UserTableViewCell
```
import UIKit
class UserTableViewCell: UITableViewCell {
@IBOutlet weak var nameLBL: UILabel!
@IBOutlet weak var usernameLBL: UILabel!
@IBOutlet weak var emailLBL: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func configure(with user: User) {
nameLBL.text = user.name
usernameLBL.text = "@\(user.username)"
emailLBL.text = user.email
}
}
```
Step : 5 ViewController
Create Controller Folder
```
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var TBL: UITableView!
private let activityIndicator = UIActivityIndicatorView(
style: .large
)
private let viewModel: UsersViewModel
// MARK: - Init
init(viewModel: UsersViewModel) {
self.viewModel = viewModel
super.init(
nibName: nil,
bundle: nil
)
}
required init?(coder: NSCoder) {
let networkService = NetworkService()
self.viewModel = UsersViewModel(
networkService: networkService
)
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupActivityIndicator()
setupBindings()
viewModel.fetchUsers()
}
override func viewDidDisappear(
_ animated: Bool
) {
super.viewDidDisappear(animated)
viewModel.cancelRequest()
}
}
extension ViewController {
private func setupUI() {
view.backgroundColor = .systemBackground
title = "Users"
TBL.register( UINib( nibName: "UserTableViewCell", bundle: nil ), forCellReuseIdentifier: "UserTableViewCell")
TBL.rowHeight = UITableView.automaticDimension
TBL.estimatedRowHeight = 80
}
}
//TBL
extension ViewController: UITableViewDataSource {
func tableView( _ tableView: UITableView, numberOfRowsInSection section: Int ) -> Int {
return viewModel.users.count
}
func tableView( _ tableView: UITableView, cellForRowAt indexPath: IndexPath ) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell( withIdentifier: "UserTableViewCell", for: indexPath ) as? UserTableViewCell else {
return UITableViewCell()
}
let user = viewModel.users[indexPath.row]
cell.configure(with: user)
return cell
}
}
// activityIndicator
extension ViewController {
private func setupActivityIndicator() {
view.addSubview(activityIndicator)
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(
equalTo: view.centerXAnchor
),
activityIndicator.centerYAnchor.constraint(
equalTo: view.centerYAnchor
)
])
}
}
//setupBindings
extension ViewController {
private func setupBindings() {
viewModel.onUsersUpdated = { [weak self] in
self?.TBL.reloadData()
}
viewModel.onLoadingChanged = {
[weak self] isLoading in
if isLoading {
self?.activityIndicator.startAnimating()
} else {
self?.activityIndicator.stopAnimating()
}
}
viewModel.onError = {
[weak self] message in
self?.showError(message)
}
}
private func showError(_ message: String) {
let alert = UIAlertController(
title: "Error",
message: message,
preferredStyle: .alert
)
alert.addAction(
UIAlertAction(
title: "Retry",
style: .default
) { [weak self] _ in
self?.viewModel.fetchUsers()
}
)
alert.addAction(
UIAlertAction(
title: "Cancel",
style: .cancel
)
)
present(
alert,
animated: true
)
}
}
```