Combine framework tutorial - Part 2 - nested publisher streams with switchToLatest and flatMap
private func addSubscriber() {
guard let url = URL(string: urlString) else { return }
URLSession
.shared
.dataTaskPublisher(for: url)
.receive(on: DispatchQueue.global(qos: .background))
.tryMap { output in
return output.data
}
.decode(type: [AlbumModel].self, decoder: JSONDecoder())
.sink { completion in
switch completion {
case .finished: print("Successfully get album info")
case .failure(let error): print(error.localizedDescription)
}
} receiveValue: { [weak self] returnedValue in
guard let self = self else { return }
self.albumSubject.send(returnedValue)
}
.store(in: &cancellables)
imageUrlSubject
.removeDuplicates()
.compactMap { url in
return URL(string: url)
}
// .buffer(size: 10, prefetch: .byRequest, whenFull: .dropOldest)
.flatMap(maxPublishers: .max(4)) { url in
URLSession
.shared
.dataTaskPublisher(for: url)
.map(\.data)
.compactMap { data in
return UIImage(data: data)
}
.catch { _ in
return Empty()
}
}
.scan([UIImage]()) { all, next in
return all + [next]
}
.receive(on: DispatchQueue.main)
.sink { [weak self] images in
guard let self = self else { return }
self.imagesSubject.send(images)
}
.store(in: &cancellables)
}
albumSubject
데이터 퍼블리셔를 통해 테이블 뷰 데이터 그리기albumSubject
를 통해 그린 테이블 뷰 선택 → 해당 URL을 현재까지 가지고 있는 URL 정보에 더해서 이미지 호출하기imageURLSubject
에서 계속해서 다운로드한 이미지를 가지고 있는 UIImage 배열 데이터 퍼블리셔private func addSubscriber() {
viewModel
.albumSubject
.sink { [weak self] albums in
guard let self = self else { return }
DispatchQueue.main.async {
self.urlTableView.reloadData()
}
}
.store(in: &cancellabels)
viewModel
.imagesSubject
.compactMap{$0}
.sink { [weak self] images in
guard let self = self else { return }
DispatchQueue.main.async {
self.imageTableView.reloadData()
}
}
.store(in: &cancellabels)
}
albumSubject
imagesSubject
를 데이터소스로 활용import Foundation
import Combine
import UIKit
class AlbumCollectionViewModel {
let albumSubject = CurrentValueSubject<[AlbumModel], Never>([AlbumModel]())
let imageUrlSubject = CurrentValueSubject<String, Never>("")
let imagesSubject = CurrentValueSubject<[UIImage], Never>([UIImage]())
private let urlString: String
private var cancellables = Set<AnyCancellable>()
init(urlString: String = "https://jsonplaceholder.typicode.com/photos") {
self.urlString = urlString
addSubscriber()
}
private func addSubscriber() {
guard let url = URL(string: urlString) else { return }
URLSession
.shared
.dataTaskPublisher(for: url)
.receive(on: DispatchQueue.global(qos: .background))
.tryMap { output in
return output.data
}
.decode(type: [AlbumModel].self, decoder: JSONDecoder())
.sink { completion in
switch completion {
case .finished: print("Successfully get album info")
case .failure(let error): print(error.localizedDescription)
}
} receiveValue: { [weak self] returnedValue in
guard let self = self else { return }
self.albumSubject.send(returnedValue)
}
.store(in: &cancellables)
imageUrlSubject
.removeDuplicates()
.compactMap { url in
return URL(string: url)
}
// .buffer(size: 10, prefetch: .byRequest, whenFull: .dropOldest)
.flatMap(maxPublishers: .max(4)) { url in
URLSession
.shared
.dataTaskPublisher(for: url)
.map(\.data)
.compactMap { data in
return UIImage(data: data)
}
.catch { _ in
return Empty()
}
}
.scan([UIImage]()) { all, next in
return all + [next]
}
.receive(on: DispatchQueue.main)
.sink { [weak self] images in
guard let self = self else { return }
self.imagesSubject.send(images)
}
.store(in: &cancellables)
}
}
import UIKit
import Combine
class AlbumCollectionViewController: UIViewController {
private let urlTableView: UITableView = {
let tableView = UITableView()
tableView.register(AlbumCell.self, forCellReuseIdentifier: AlbumCell.identifier)
return tableView
}()
private let imageTableView: UITableView = {
let tableView = UITableView()
tableView.register(ImageCell.self, forCellReuseIdentifier: ImageCell.identifier)
return tableView
}()
private var cancellabels = Set<AnyCancellable>()
private let viewModel = AlbumCollectionViewModel()
override func viewDidLoad() {
super.viewDidLoad()
setAlbumViewUI()
addSubscriber()
}
private func setAlbumViewUI() {
view.backgroundColor = .systemBackground
urlTableView.dataSource = self
urlTableView.delegate = self
imageTableView.dataSource = self
urlTableView.translatesAutoresizingMaskIntoConstraints = false
imageTableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(urlTableView)
view.addSubview(imageTableView)
urlTableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
urlTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
urlTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
urlTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -400).isActive = true
imageTableView.topAnchor.constraint(equalTo: urlTableView.bottomAnchor, constant: 5).isActive = true
imageTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
imageTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
imageTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
private func addSubscriber() {
viewModel
.albumSubject
.sink { [weak self] albums in
guard let self = self else { return }
DispatchQueue.main.async {
self.urlTableView.reloadData()
}
}
.store(in: &cancellabels)
viewModel
.imagesSubject
.compactMap{$0}
.sink { [weak self] images in
guard let self = self else { return }
DispatchQueue.main.async {
self.imageTableView.reloadData()
}
}
.store(in: &cancellabels)
}
}
extension AlbumCollectionViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if tableView == urlTableView {
let imageUrlString = viewModel.albumSubject.value[indexPath.row].url
viewModel.imageUrlSubject.send(imageUrlString)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableView == urlTableView {
guard let cell = tableView.dequeueReusableCell(withIdentifier: AlbumCell.identifier, for: indexPath) as? AlbumCell else {
return UITableViewCell()
}
let model = viewModel.albumSubject.value[indexPath.row]
cell.configure(with: model)
return cell
} else {
guard let cell = tableView.dequeueReusableCell(withIdentifier: ImageCell.identifier, for: indexPath) as? ImageCell else {
return UITableViewCell()
}
let model = viewModel.imagesSubject.value[indexPath.row]
cell.configure(with: model)
return cell
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView == urlTableView {
return viewModel.albumSubject.value.count
} else {
return viewModel.imagesSubject.value.count
}
}
}
import UIKit
class ImageCell: UITableViewCell {
static let identifier = "ImageCell"
private let imageLabel: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
return imageView
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setImageCellLayout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setImageCellLayout() {
imageLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(imageLabel)
imageLabel.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
imageLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
imageLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
imageLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
}
func configure(with model: UIImage) {
imageLabel.image = model
}
}