Today I'm researching how to provide data to UIKit component when using Combine pipeline.
I could see tons of tutorials about how to use SwiftUI + Combine from internet, but not found anyone helpful stuff about UIKit + Combine.
Well, I list one approach here, I put some data works in WebService
and mean time receive the data in ViewController
with Combine pipeline and provide them as the data source of tableView.
But I could see the fetchData()
is mixed in ViewController
now, it should be wonderful if I could put it into WebService
and isolate the data pipeline operation with ViewController
.
So what is the better way to provide the data to UIKit component when using Combine pipeline like dataTaskPublisher
? Is my current approach the common way to do it?
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()
view.backgroundColor = .systemBackground
title = "Petitions"
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Credits", style: .plain, target: self, action: #selector(rightBarButtonTapped))
tableView.delegate = self
tableView.dataSource = self
// tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
tableView.topAnchor.constraint(equalTo: g.topAnchor),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
])
}
override func viewWillAppear(_ animated: Bool) {
let url = WhiteHouseClient.petitions.url
print("url: \(url)")
fetchData(for: url)
print("viewWillAppear")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return petitions.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell(style: .subtitle, reuseIdentifier: "cell")
cell.accessoryType = .disclosureIndicator
let petition = petitions[indexPath.row]
cell.textLabel?.text = petition.name
cell.detailTextLabel?.text = petition.body
return cell
}
@objc func rightBarButtonTapped() {
let ac = UIAlertController(title: "Credits", message: "the data comes from the We The People API of the Whitehouse.", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default))
present(ac, animated: true)
}
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
}
}) { [unowned self] petitions in
self.petitions = petitions.results.map(PetitionViewModel.init)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}.store(in: &self.cancellableSet)
}
}