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()
}
}
}
}
}
```