Common Mistake while using @Published | RunLoop.Main vs DispatchQueue.Main | Combine
class TableViewModel {
@Published var data = [String]() {
willSet {
print("Willset executed")
}
didSet {
print("Didset executed")
}
}
init() {
}
func fetchData() {
var data = [String]()
for _ in 1...100 {
let rand = Int.random(in: 1...100)
data.append(rand.description)
}
self.data = data
}
}
@Published
을 따르는 데이터 소스는 해당 값이 업데이트될 때를 감지class TableViewController: UIViewController {
private lazy var refreshButton: UIButton = {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.baseBackgroundColor = .systemGreen
config.baseForegroundColor = .white
config.title = "Refresh"
button.configuration = config
button.addTarget(self, action: #selector(didTapRefresh), for: .touchUpInside)
return button
}()
private lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "tableViewCell")
tableView.dataSource = self
tableView.delegate = self
return tableView
}()
private var cancelables = Set<AnyCancellable>()
private let viewModel = TableViewModel()
override func viewDidLoad() {
super.viewDidLoad()
setUI()
bind()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
refreshButton.frame = CGRect(x: (view.frame.size.width - 100) / 2, y: view.safeAreaInsets.top, width: 100, height: 50)
tableView.frame = CGRect(x: 0, y: refreshButton.frame.origin.y + 50 + 10, width: view.frame.size.width, height: view.frame.size.height)
}
private func setUI() {
view.backgroundColor = .systemBackground
view.addSubview(tableView)
view.addSubview(refreshButton)
}
@objc private func didTapRefresh() {
viewModel.fetchData()
}
private func bind() {
viewModel
.$data
.sink { [weak self] _ in
print("TableView reload!")
self?.tableView.reloadData()
}
.store(in: &cancelables)
}
}
extension TableViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
viewModel.data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableViewCell", for: indexPath)
var content = cell.defaultContentConfiguration()
let model = viewModel.data[indexPath.row]
content.text = model
content.textProperties.alignment = .center
cell.contentConfiguration = content
return cell
}
}
TableView reload!
Willset executed
TableView reload!
Didset executed
Willset executed
TableView reload!
Didset executed
Willset
이 호출된 뒤 퍼블리셔 변경을 감지하여 뷰 컨트롤러에서 구독한 tableView.reloadData()
가 호출되는 데, 이 시점에서는 실제 데이터가 들어가기 전이므로 이전 값이 계속 유지됨. 즉 업데이트될 것은 체크했으나 업데이트되기 전의 값을 통해 UI를 그리기 때문에 두 번 클릭해야 하는 불상사 발생private func bind() {
viewModel
.$data
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
print("TableView reload!")
self?.tableView.reloadData()
}
.store(in: &cancelables)
}
receive
메소드를 통해 해결 가능DispatchQueue.main
또는 Runloop.main
을 통해 해당 데이터 퍼블리셔의 변경 사항을 구독할 경우 올바른 행동을 기대 가능TableView reload!
Willset executed
Didset executed
TableView reload!
Willset executed
Didset executed
TableView reload!
didSet
이 호출된 이후에야 제대로 업데이트된 값을 통해 테이블 뷰 UI를 그리기 때문에 올바른 타이밍에서 호출perform selector
를 사용하는 반면 디스패치 큐는 일반적인 GCD 메소드를 사용하면서 실행