Learning SwiftUI with Combine Framework
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.
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
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 publisherOperators
- 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
Subscribe to my newsletter
Read articles from arpana rani directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by