I'm trying to figure out how to use URLSession with the Combine framework.
I have a class that is to fetch data as follows.
import UIKit
import Combine
class APIClient: NSObject {
var cancellables = [AnyCancellable]()
@Published var models = [MyModel]()
func fetchData(urlStr: String) -> AnyPublisher<[MyModel], Never> {
guard let url = URL(string: urlStr) else {
let subject = CurrentValueSubject<[MyModel], Never>([])
return subject.eraseToAnyPublisher()
}
let subject = CurrentValueSubject<[MyModel], Never>(models)
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [MyModel].self, decoder: JSONDecoder())
.replaceError(with: [])
.sink { posts in
print("api client: \(posts.count)")
self.models = posts
}
.store(in: &cancellables)
return subject.eraseToAnyPublisher()
}
}
I then have a view model class that is to deliver data for my view controller as follows.
import Foundation
import Combine
class ViewModel: NSObject {
@IBOutlet var apiClient: APIClient!
var cancellables = Set<AnyCancellable>()
@Published var dataModels = [MyModel]()
func getGitData() -> AnyPublisher<[MyModel], Never> {
let urlStr = "https://api.github.com/repos/ReactiveX/RxSwift/events"
let subject = CurrentValueSubject<[MyModel], Never>(dataModels)
apiClient.fetchData(urlStr: urlStr)
.sink { result in
print("view model: \(result.count)")
self.dataModels = result
}.store(in: &cancellables)
return subject.eraseToAnyPublisher()
}
}
My view controller has an IBOutlet of ViewModel.
import UIKit
import Combine
class ViewController: UIViewController {
// MARK: - Variables
var cancellables = [AnyCancellable]()
@IBOutlet var viewModel: ViewModel!
// MARK: - IBOutlet
@IBOutlet weak var tableView: UITableView!
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
viewModel.getGitData()
.sink { posts in
print("view controller: \(posts.count)")
}
.store(in: &cancellables)
}
}
If I run it, it seems that ViewModel returns 0 without waiting for APIClient to return data. And the view controller doesn't wait, either. What am I doing wrong? Can I do it without using the completion handler?
In case you need to know what MyModel is, it's a simple struct.
struct MyModel: Decodable {
let id: String
let type: String
}
Muchos thanks
Okay... The solution to my question is use of the Future guy.
class APIClient: NSObject {
var cancellables = [AnyCancellable]()
@Published var models = [MyModel]()
func fetchData(urlStr: String) -> Future<[MyModel], Error> {
return Future<[MyModel], Error> { [weak self] promise in
guard let url = URL(string: urlStr) else {
return promise(.failure("Failure" as! Error))
}
guard let strongSelf = self else { return }
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [MyModel].self, decoder: JSONDecoder())
.replaceError(with: [])
.sink { completion in
if case .failure(let error) = completion {
promise(.failure(error))
}
} receiveValue: { promise(.success($0)) }
.store(in: &strongSelf.cancellables)
}
}
}
class ViewModel: NSObject {
@IBOutlet var apiClient: APIClient!
var cancellables = Set<AnyCancellable>()
@Published var dataModels = [MyModel]()
func getGitData() -> Future<[MyModel], Error> {
let urlStr = "https://api.github.com/repos/ReactiveX/RxSwift/events"
return Future<[MyModel], Error> { [weak self] promise in
guard let strongSelf = self else { return }
strongSelf.apiClient.fetchData(urlStr: urlStr)
.sink { completion in
if case .failure(let error) = completion {
promise(.failure(error))
}
} receiveValue: {
promise(.success($0))
print("view model: \($0.count)")
}
.store(in: &strongSelf.cancellables)
}
}
}