[UIKit] Runloop.Main vs DispatchQueue.Main

Junyoung Park·2022년 12월 31일
0

UIKit

목록 보기
142/142
post-thumbnail

Common Mistake while using @Published | RunLoop.Main vs DispatchQueue.Main | Combine

Runloop.Main vs DispatchQueue.Main

@Published

  • 컴바인을 사용해 간단한 테이블 뷰를 그리기
  • 특정 버튼을 통해 테이블 뷰의 데이터 소스를 갱신
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
    }
}
  • 현 시점의 뷰 모델의 데이터 소스가 가지고 있는 값을 통해 테이블 뷰 UI 구성

  • 하지만 리프레시 버튼을 두 번 클릭해서야 이전의 값을 확인 가능
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를 그리기 때문에 올바른 타이밍에서 호출

Runloop.Main vs DispatchQueue.Main

  • 기본적으로 같은 개념
  • 런루프는 특정 태스크 스케줄링에 사용하는 이벤트 처리 루프
  • 각 스레드(커스텀 스레드 또는 시스템에 의한 디폴트 스레드 모두)는 각자의 런루프를 가지는 데, 메인 스레드와 연관된 모든 런루프가 곧 런루프 메인
  • 디스패치 큐는 메인 스레드와 연관 된 디스패치 큐로 메인 스레드와 연관된 시리얼 큐가 곧 디스패치 큐 메인 스레드로 UIKit의 모든 뷰 드로우 사이클이 해당 스레드와 연관
  • 런루프는 perform selector를 사용하는 반면 디스패치 큐는 일반적인 GCD 메소드를 사용하면서 실행
  • 런루프는 디폴트 모드에서 사용될 때에만 콜백 가능 → 유저 인터렉션과 함께 백그라운드 태스크를 작업해야 할 경우 사용 불가능(유저가가 테이블 뷰를 스크롤할 때 해당 셀에서 내부적으로 이미지를 다운로드받아야 하는 백그라운드 태스크를 실행할 때 런루프 메인을 사용할 때에는 해당 다운로드 작업이 유저 인터렉션이 끝날 때까지 실행되지 않음) → 런루프를 일종의 스케줄러로 사용하기 때문에 유저 인터렉션을 다루는 중이기 때문
profile
JUST DO IT

0개의 댓글