Learning SwiftUI with Combine Framework

arpana raniarpana rani
8 min read

Hello All,
Today, I am writing about the Combine framework that I learned along with basic SwiftUI design.

Combine framework: Combine is Apple's framework introduced in WWDC 2019 and required minimum iOS version is 13. It eliminates the need for external libraries like RxSwift and RxCocoa, providing the flexibility of reactive programming.
i.e it gives the same experience without having any third party library.

It provides a declarative api and allows to write a functional reactive code . A functional reactive programing is something that allows you to process value over time ,means task happening at some point of time. Like network calls, notifications which we don’t know at what time we will receive notifications.
It works on the principle of pub-sub model. So we have a publisher which produces/publishes the value and one or more subscriber which listens to publishers and consumes the value published by publisher and Combine introduce another third entity i.e. operators, they are different kind of publisher that receives value from another publisher and produce the value by performing operation on it.

  1. Publisher - It is a protocol
    - Declares that a type can transmit a sequence of values over a time.
    - It has two associated types:

    Output - the type of value it produces

    Failure - the type of error it could encounter

  2. Subscriber - It is a protocol
    - Declares a type that can receive input from a publisher
    - A given subscriber’s Input and Failure associated types must watch the output and failure of its corresponding publisher

  3. Operators

    - Methods that are called on publishers and returns same or different publishers

    - Used for manipulating the values received

    - Multiple operators can be chained together

    Lets see how to make a network call using combine framework.

//Here look the completion block (without combine framework , we use to pass this competition 
//handler which was returned once the response received 
//(by datatask AFnetworking or alamofire etc) 

func traditionawayOfcallingNetworkApi((endppoint: EndPoint<Any>, id:Int? = nil,  modelResponse : T.Type, completion:@escaping((T)->Void)){ 

        return completion......
}

//Now using combine
func getDataFromRestService<T:Decodable>(endppoint: EndPoint<Any>, id:Int? = nil,  modelResponse : T.Type ) -> Future<T, Error> { //for array [T]
        return Future<T ,Error> { [weak self] promise in

            guard let self = self , let url = endppoint.url else {
              //when url is not valid return Failure 
                return promise(.failure(NetworkError.invalidURL))
            }
            let request = getRequest(type: endppoint, url: url )

            guard let validURL = request.url else {
                return
            }
            URLSession.shared.dataTaskPublisher(for: validURL)
                .tryMap { (data, response) -> Data in
                    guard let httpResponse =  response as? HTTPURLResponse , 200...299 ~= httpResponse.statusCode else {
                        // ~= Returns a Boolean value indicating whether a value is included in the range mentioned
                        throw NetworkError.responseError
                    }
                    return data
                }
                .decode(type: T.self, decoder: JSONDecoder())
                .receive(on: RunLoop.main)
                .sink (receiveCompletion: { (completion) in

                    if case let .failure(error) = completion {

                        switch error{

                        case let decodingError as DecodingError:
                            promise(.failure(decodingError))

                        case let apiError as NetworkError:
                            promise(.failure(apiError))

                        default :         
                            promise(.failure(NetworkError.unknown(error)))
                        }
                    }
                }, receiveValue: { promise(.success($0))})
                .store(in: &self.cancellables)

        }
    }

Many of the existing api like URLSession, NotificationCenter and all those have been extended to use combine framework means they have been extended as publisher.

We are returning Future as return type. Future is a kind of publisher , it works on concept of promise. Promise is a block of code which gets executed , when something happens. Means when process A happens in Future asynchronously , I promise I will execute Y.

We are using Future here to make a call. And caller of this method will subscribe to publisher. So that it will get the value when they are published.

Promise is specific to Future

Future has two types output and failure. T parameter refers to response we get from api call . Here DataTask is also a Publisher. It will publish the data after a network call. Once this particular publisher is publishing the data, we are trying to operate on received data.

Operators used in this example

.tryMap is here a operator and we are mapping our response to check if our status code is lying between range and if we received the data. We have also chained the operator by another operator . .trymap() is similar to .map() operator except that trymap() can throws the error. So when we are interested in specific error information use .tryMap()

.decode. decoding the received data in our response model here which is generic type T).

.receive The third operator to receive data on main thread. It is completely optional. We can also write like out tradition approach on the place where we are going call this method by writing dispathQueue.main.async {....}

.sink whenever we use sink, we are creating a subscriber , see documentation for details , It has two parameters,

  • receiveComplete - whether a completion call is success or failure.

  • receiveValue: The closure will give the value we received.

If anything goes wrong pass failure to promise, else pass success to value

.store - It is also an important concept. Whenever we create subscriber, it remains in memory. It is used whenever required. But we need to think of deinitialization it. For purpose we need to keep reference somewhere. Set will have a element of type anyCancellable.

Now Lets check how we are calling the getDataFromRestService() The below code I have written in ViewModel swift file. The parameter here I have used is just for explanation purpose, You can take those param from your view input field like TextField.

    @Published var  employeeDetail: EmployeeLoginResponse?
     private var cancellables = Set<AnyCancellable>()

    func callLoginApi(){

      let param =  LoginRequestModel(email: "arpana@gmail.com", password: "123456")

        // take input from view
        NetworkManager.sharedNetworkInstance.getDataFromRestService(endppoint: EndPoint.login(model: param),  modelResponse: EmployeeLoginResponse.self)//, method: .post)
            .sink { completion in
                switch completion {
                case .failure(let err):
                    print("Error is \(err.localizedDescription)")
                case .finished:
                    print("Finished")
                }
            }
            receiveValue: { [weak self] employeeLoginData in
                self?.employeeDetail = employeeLoginData

                print( self?.employeeDetail)

            }
        .store(in: &cancellables)
        }

The below is complete networkManager file

//
//  NetworkManager.swift
//  EmployeeInfoEngine
//
//  Created by Arpana Rani on 02/05/24.
//

import Foundation
import Combine

enum NetworkError: Error {
    case invalidURL
    case responseError
    case invalidResponse
    case invalidData
    case network(Error?)
    case decoding(Error?)
    case unknown(Error?)
}

enum HTTPMethod: String {
    case get = "GET"
    case post = "POST"
    case put = "PUT"
    case delete = "DELETE"
}

protocol EndPointType {
    var path: String { get }
    var url: URL? { get }
    var method: HTTPMethod { get }
    var body: Encodable? { get }
    var headers: [String: String]? { get }
}

enum EndPoint<T>{
    case login(model: T)
    case register(model: T)
    case searchEmployee(employeeId: Int)
    case getEmployeeDetails(employeeId: Int)

}

extension EndPoint: EndPointType {
    var body: Encodable? {

        switch self{
        case .login(let requestModel):
            return requestModel as? Encodable

        case .register(let requestModel):
            return requestModel as? Encodable
        default: return nil
        }
    }

    var path: String {
        switch self {
        case .login:
            return "login"

        case .register:
            return "register"

       case .searchEmployee (let employeeId):
            return "employee/search/\(employeeId)"

        case .getEmployeeDetails(let employeeId):
            return "employee/\(employeeId)"


        }
    }
    var url: URL? {
        return URL(string: "your_api_base_url\(path)")
    }
    var method: HTTPMethod {
        switch self {
        case .login:
            return .post
        case .register:
            return .post
        case .searchEmployee:
            return .get
        default :
            return .put
        }
    }

    var headers: [String : String]? {
        return [
            "Content-Type": "application/json"
        ]
    }
}
class NetworkManager {

    static let sharedNetworkInstance = NetworkManager()
    private var cancellables = Set<AnyCancellable>()
    private init(){

    }

    func getDataFromRestService<T:Decodable>(endppoint: EndPoint<Any>, id:Int? = nil,  modelResponse : T.Type ) -> Future<T, Error> { //for array [T]
        return Future<T ,Error> { [weak self] promise in

           guard let self = self , let url = endppoint.url else {

                return promise(.failure(NetworkError.invalidURL))
            }
            let request = getRequest(type: endppoint, url: url )
            guard let validURL = request.url else {
                return
            }
            URLSession.shared.dataTaskPublisher(for: validURL)
                .tryMap { (data, response) -> Data in
                    guard let httpResponse =  response as? HTTPURLResponse , 200...299 ~= httpResponse.statusCode else {
                        // ~= Returns a Boolean value indicating whether a value is included in the range mentioned
                        throw NetworkError.responseError
                    }
                    return data
                }
                .decode(type: T.self, decoder: JSONDecoder())
                .receive(on: RunLoop.main)
                .sink (receiveCompletion: { (completion) in

                    if case let .failure(error) = completion {

                        switch error{

                        case let decodingError as DecodingError:
                            promise(.failure(decodingError))

                        case let apiError as NetworkError:
                            promise(.failure(apiError))

                        default :

                            promise(.failure(NetworkError.unknown(error)))
                        }
                    }
                }, receiveValue: { promise(.success($0))})
                .store(in: &self.cancellables)

        }
    }


func getRequest(type: EndPointType, url: URL ) -> URLRequest {
//The method is used for generating a request in different cases 
//like get request with query params or  
//post request with json body...
           var request = URLRequest(url: url)
           request.httpMethod = type.method.rawValue

           if let parameters = type.body, type.method == .get {
               var components = URLComponents(url: url, resolvingAgainstBaseURL: false)

//Created a different func to create a dictionary from encodable struct
               components?.queryItems = parameters.dictionary().map {
                   URLQueryItem(name: $0, value: $1 as? String)
              }
               request.url = components?.url
           } else if let parameters = type.body {
          //passing json as a request body
               request.httpBody = try? JSONEncoder().encode(parameters)
           }

           request.allHTTPHeaderFields = type.headers
           return request
       }

extension Encodable {
    func dictionary() -> [String:Any] {
        var dict = [String:Any]()
        let mirror = Mirror(reflecting: self)
        for child in mirror.children {
            guard let key = child.label else { continue }
            let childMirror = Mirror(reflecting: child.value)

            switch childMirror.displayStyle {
            case .struct, .class:
                let childDict = (child.value as! Encodable).dictionary()
                dict[key] = childDict
            case .collection:
                let childArray = (child.value as! [Encodable]).map({ $0.dictionary() })
                dict[key] = childArray
            case .set:
                let childArray = (child.value as! Set<AnyHashable>).map({ ($0 as! Encodable).dictionary() })
                dict[key] = childArray
            default:
                dict[key] = child.value
            }
        }

        return dict
    }
}

Summary: This article discusses the Combine framework and its key components like Publishers, Subscribers, and Operators, and how to make network calls using Combine. It also provides a detailed example of making a network call using Combine framework and explains the usage of Future, Promise, and various operators.
I explained how to make a web service call using the Combine framework. However, you can use it in various other ways by exploring different publishers, subscribers, and operators.

Thanks

0
Subscribe to my newsletter

Read articles from arpana rani directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

arpana rani
arpana rani