Combine framework tutorial: transforming Operators part 1 - map, compactMap and tryMap
map
, tryMap
, compactMap
등을 통해 핸들링private func addSubscriber() {
guard let url = URL(string: urlString) else { return }
URLSession
.shared
.dataTaskPublisher(for: url)
.receive(on: DispatchQueue.global(qos: .background), options: nil)
.tryMap({ output in
return output.data
})
.decode(type: [CommentModel].self, decoder: JSONDecoder())
.sink { completion in
switch completion {
case .failure(let error):
print(error.localizedDescription)
case .finished:
print("Successfully Get data")
}
} receiveValue: { [weak self] returnedValue in
guard let self = self else { return }
self.commentSubject.send(returnedValue)
}
.store(in: &cancellables)
}
receive
를 통해 비동기 이벤트를 백그라운드 스레드로 받기tryMap
을 통해 데이터 퍼블리셔의 특정 값을 핸들링, 에러가 있을 경우 throw
가능sink
를 통해 뷰 모델 내의 데이터에 반영private func addSubscriber() {
viewModel
.commentSubject
.receive(on: DispatchQueue.main)
.sink { [weak self] comments in
guard let self = self else { return }
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
.store(in: &cancellables)
}
commentSubject
의 데이터 플로우를 받아 데이터를 리턴하는 데 사용import Foundation
struct CommentModel: Codable {
let postId: Int
let id: Int
let name: String
let email: String
let body: String
}
Codable
프로토콜을 준수하는 구조체final class CommentViewModel {
let commentSubject = CurrentValueSubject<[CommentModel], Never>([CommentModel(postId: 1, id: 1, name: "Name", email: "Mock@email.com", body: "doloribus at sed quis culpa deserunt consectetur qui praesentium\naccusamus fugiat dicta\nvoluptatem rerum ut voluptate autem\nvoluptatem repellendus aspernatur dolorem i")])
let urlString: String
private var cancellables = Set<AnyCancellable>()
init(urlString: String = "https://jsonplaceholder.typicode.com/comments") {
self.urlString = urlString
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self.addSubscriber()
}
}
private func addSubscriber() {
guard let url = URL(string: urlString) else { return }
URLSession
.shared
.dataTaskPublisher(for: url)
.receive(on: DispatchQueue.global(qos: .background), options: nil)
.tryMap({ output in
return output.data
})
.decode(type: [CommentModel].self, decoder: JSONDecoder())
.sink { completion in
switch completion {
case .failure(let error):
print(error.localizedDescription)
case .finished:
print("Successfully Get data")
}
} receiveValue: { [weak self] returnedValue in
guard let self = self else { return }
self.commentSubject.send(returnedValue)
}
.store(in: &cancellables)
}
}
CommentViewController
의 데이터를 관리하고 있는 뷰 모델commentSubject
가 초깃값을 가지고 있는 데이터 퍼블리셔sink
import UIKit
import Combine
class CommentViewController: UIViewController {
private var viewModel = CommentViewModel()
private let tableView: UITableView = {
let tableView = UITableView()
tableView.register(CommentCell.self, forCellReuseIdentifier: CommentCell.identifier)
tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 200
return tableView
}()
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
setCommentViewUI()
addSubscriber()
}
private func setCommentViewUI() {
title = "Comments"
navigationController?.navigationBar.prefersLargeTitles = true
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.frame = view.bounds
tableView.dataSource = self
}
private func addSubscriber() {
viewModel
.commentSubject
.receive(on: DispatchQueue.main)
.sink { [weak self] comments in
guard let self = self else { return }
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
.store(in: &cancellables)
}
}
extension CommentViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.commentSubject.value.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: CommentCell.identifier, for: indexPath) as? CommentCell else {
return UITableViewCell()
}
let model = viewModel.commentSubject.value[indexPath.row]
cell.configure(with: model)
return cell
}
}
addSubscriber
를 통해 데이터 퍼블리셔의 sink
이벤트가 발생할 때마다 self.tableView.reloadData()
함수를 통해 뷰를 다시 그림import UIKit
final class CommentCell: UITableViewCell {
static let identifier = "CommentCell"
private let postIdLabel: UILabel = {
let label = UILabel()
label.text = "Post Id"
label.textColor = .darkGray
label.numberOfLines = 0
return label
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.text = "Name"
label.textColor = .black
label.numberOfLines = 0
return label
}()
private let emailLabel: UILabel = {
let label = UILabel()
label.text = "abc@def.com"
label.font = .preferredFont(forTextStyle: .subheadline)
label.textColor = .black
label.numberOfLines = 0
return label
}()
private let bodyLabel: UILabel = {
let label = UILabel()
label.text = "comment body"
label.font = .preferredFont(forTextStyle: .body)
label.textColor = .black
label.numberOfLines = 0
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setCommentCellLayout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setCommentCellLayout() {
nameLabel.translatesAutoresizingMaskIntoConstraints = false
emailLabel.translatesAutoresizingMaskIntoConstraints = false
postIdLabel.translatesAutoresizingMaskIntoConstraints = false
bodyLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(nameLabel)
contentView.addSubview(emailLabel)
contentView.addSubview(postIdLabel)
contentView.addSubview(bodyLabel)
separatorInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5).isActive = true
nameLabel.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor).isActive = true
postIdLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 5).isActive = true
postIdLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
postIdLabel.topAnchor.constraint(equalTo: nameLabel.topAnchor).isActive = true
emailLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 5).isActive = true
emailLabel.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor).isActive = true
emailLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor).isActive = true
emailLabel.bottomAnchor.constraint(equalTo: bodyLabel.topAnchor, constant: -5).isActive = true
bodyLabel.topAnchor.constraint(equalTo: emailLabel.bottomAnchor, constant: 5).isActive = true
bodyLabel.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor).isActive = true
bodyLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor).isActive = true
bodyLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5).isActive = true
}
func configure(with model: CommentModel) {
postIdLabel.text = model.postId.description
emailLabel.text = model.email
nameLabel.text = model.name
bodyLabel.text = model.body
}
}
강의 내용은 일반적인 JSON을 사용하고 있었는데, 실제 비동기 이벤트를 다뤄보는 연습을 하기 위해 URLSession을 사용했다.
AnyPublisher
를 리턴하는 함수가 곧 API를 호출해서 새롭게 데이터를 리턴하도록 하는 곳이다. 즉 뷰 모델에서의 최초 구독은 해당 데이터 서비스 클래스의 함수의 출력값과 연결하기만 하면 보다 간단해진다. 이 경우 물론 뷰의 버튼 이벤트를 별도로 연결해야 하는 추가 일이 생긴다.