swift – Better way to provide data to UIKit component from Combine pipeline

Today I’m researching how to provide data to UIKit component when using Combine pipeline.

I could see tons of turtoials about how to use SwiftUI + Combine from internet, but not found anyone helpful stuff about UIKit + Combine.

Well, I list two approaches below, the first one calls a fetchData in ViewController and assign data to dataSource of tableView in sink. The second one uses call back to pass the data back from another class, which is opposite with Combine’s way as comment mentioned.

So what is the better way to provide the data to UIKit component when using Combine pipeline like dataTaskPublisher?

Approach 1

WebService.swift

import Combine
import UIKit

enum HTTPError: LocalizedError {
    case statusCode
    case post
}

enum FailureReason: Error {
    case sessionFailed(error: HTTPError)
    case decodingFailed
    case other(Error)
}

struct Response: Codable {
    let statusMessage: String?
    let success: Bool?
    let statusCode: Int?
}

class WebService {
    private var requests = Set<AnyCancellable>()
    
    private var decoder: JSONDecoder = {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return decoder
    }()
    
    private var session: URLSession = {
        let config = URLSessionConfiguration.default
        config.allowsExpensiveNetworkAccess = false
        config.allowsConstrainedNetworkAccess = false
        config.waitsForConnectivity = true
        config.requestCachePolicy = .reloadIgnoringLocalCacheData
        return URLSession(configuration: config)
    }()
    
    func createPublisher<T: Codable>(for url: URL) -> AnyPublisher<T, FailureReason> {
        return session.dataTaskPublisher(for: url)
            .tryMap { output in
                guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else {
                    throw HTTPError.statusCode
                }
                return output.data
            }
            .decode(type: T.self, decoder: decoder)
            .mapError { error in
                switch error {
                case is Swift.DecodingError:
                    return .decodingFailed
                case let httpError as HTTPError:
                    return .sessionFailed(error: httpError)
                default:
                    return .other(error)
                }
            }
            .eraseToAnyPublisher()
    }
    
    func getPetitionsPublisher(for url: URL) -> AnyPublisher<Petitions, FailureReason> {
        createPublisher(for: url)
    }   
}

ViewController

import Combine
import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    var tableView = UITableView()
    var petitions = (PetitionViewModel)()
    let webService = WebService()
    private var cancellableSet = Set<AnyCancellable>()
    

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // some view stuff
    }
    
    override func viewWillAppear(_ animated: Bool) {
        let url = URL(string: WhiteHouseClient.urlString)!
        fetchData(for: url)
        print("viewWillAppear")
    }
    
    // ...
    
    func fetchData(for url: URL) {
        webService.getPetitionsPublisher(for: url)
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { status in
                switch status {
                case .finished:
                    break
                case .failure(let error):
                    print(error)
                    break
                }
            }) { petitions in
                self.petitions = petitions.results.map(PetitionViewModel.init)
                DispatchQueue.main.async {
                    self.tableView.reloadData()
                }
            }.store(in: &self.cancellableSet)
        }
    }
}

Approach 2, using closure, but error handling is not made yet, I need to improve it

WebService

import Combine
import UIKit

class WebService {
    private var requests = Set<AnyCancellable>()
    
    private var decoder: JSONDecoder = {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return decoder
    }()
    
    private var session: URLSession = {
        let config = URLSessionConfiguration.default
        config.allowsExpensiveNetworkAccess = false
        config.allowsConstrainedNetworkAccess = false
        config.waitsForConnectivity = true
        config.requestCachePolicy = .reloadIgnoringLocalCacheData
        return URLSession(configuration: config)
    }()
    
    func fetch<T: Decodable>(_ url: URL, defaultValue: T, completion: @escaping (T) -> Void) {
        session.dataTaskPublisher(for: url)
            .retry(1)
            .map(.data)
            .decode(type: T.self, decoder: decoder)
            .replaceError(with: defaultValue)
            .receive(on: DispatchQueue.main)
            .sink(receiveValue: completion)
            .store(in: &requests)
    }
}

ViewController

import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    var tableView = UITableView()
    var petitions = (Petition)()
    let webService = WebService()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // some view stuff
    }
    
    override func viewWillAppear(_ animated: Bool) {
        fetchData()
    }
    
    // ...
    
    func fetchData() {
        DispatchQueue.global().async { (weak self) in
            let url = URL(string: WhiteHouseClient.urlString)!
            self?.webService.fetch(url, defaultValue: Petitions(results: (Petition.default))) { petitions in
                self?.petitions = petitions.results
                
                DispatchQueue.main.async {
                    self?.tableView.reloadData()
                }
            }
        }
    }
}
```