[UIKit] Concurrency: Dispatch Group & Dispatch Work Item

Junyoung Park·2022년 12월 26일
0

UIKit

목록 보기
135/142
post-thumbnail

Mastering Concurrency in iOS - Part 3 (Dispatch Group, Dispatch Work Item)

Concurrency: Dispatch Group & Dispatch Work Item

Dispatch Group

  • 여러 개의 태스크를 그룹화 가능
  • 여러 개의 태스크가 종료될 때까지 기다릴 수 있음
  • 다른 태스크를 계속 진행할 수 있고, 그룹 내 태스크가 종료될 때 알림을 받을 수 있음
  • enter(), leave(), wait(), notify()
final class SplashViewController: UIViewController {
    private let spinnerView: UIActivityIndicatorView = {
        let view = UIActivityIndicatorView()
        view.startAnimating()
        return view
    }()
    private var launchDataDispatchGroup = DispatchGroup()
    private var cancellables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
        DispatchQueue.global().async { [weak self] in
            self?.getAppLaunchData()
        }
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        spinnerView.style = .large
        spinnerView.center = view.center
    }
    
    private func setUI() {
        title = "SplashView"
        navigationItem.largeTitleDisplayMode = .always
        navigationController?.navigationBar.prefersLargeTitles = true
        view.backgroundColor = .systemBackground
        view.addSubview(spinnerView)
    }
    
    private func getAppLaunchData() {
        launchDataDispatchGroup.enter()
        NetworkManager
            .download(endPoint: .userPreferences)
            .sink { [weak self] completion in
                self?.launchDataDispatchGroup.leave()
                switch completion {
                case .failure(let error): print(error.localizedDescription)
                case .finished: break
                }
            } receiveValue: { _ in
                print("user preference data has received")
            }
            .store(in: &cancellables)
        
        launchDataDispatchGroup.enter()
        NetworkManager
            .download(endPoint: .appConfig)
            .sink { [weak self] completion in
                self?.launchDataDispatchGroup.leave()
                switch completion {
                case .failure(let error): print(error.localizedDescription)
                case .finished: break
                }
            } receiveValue: { _ in
                print("app Configuration data has received")
            }
            .store(in: &cancellables)
        
        let waitResult: DispatchTimeoutResult = launchDataDispatchGroup.wait(timeout: .now() + .seconds(5))
        DispatchQueue.main.async { [weak self] in
            switch waitResult {
            case .success:
                print("API call completed before timeout")
            case .timedOut:
                print("APIs timed out")
            }
            self?.spinnerView.stopAnimating()
            self?.navigateToSignVC()
        }
    }
    
    private func navigateToSignVC() {
        let vc = SignUpViewController()
        let navVC = UINavigationController(rootViewController: vc)
        let keyWindow = UIApplication.shared.keyWindow
        keyWindow?.rootViewController = navVC
    }
}
  • launchDataDispatchGroup이라는 디스패치 그룹을 통해 태스크에 들어가고 나오는 과정을 컨트롤 가능
  • DispatchTimeoutResult를 통해 체크하고자 하는 태스크에 걸리는 시간 내 종료 여부를 직접 확인 가능
  • notify 프로퍼티를 사용한다면 시간과 관계 없이 해당 태스크가 모두 종료되었음을 알림받을 수 있음

Dispatch Work Item

  • 코드 블록의 캡슐화
  • 디스패치 큐 및 디스패치 그룹 모두 디스패치될 수 있음
  • 실행이 시작되지 않은 시점에도 해당 태스크를 취소할 수 있음
  • 검색 쿼리와 같이 유저의 실시간 인터렉션이 태스크로 직결되는 상황에서 인터렉션이 종료될 때까지 불필요한 태스크가 여러 번 실행될 수 있음 → 최종 결과에 상응하는 태스크를 제외한, 이전에 발생했던 태스크들이 존재한다면 해당 태스크를 중도 취소/사전 취소 가능하다는 뜻
  • cancel: 실행 이전에 프로퍼티 값이 참이라면 실행되지 않음. 실행 도중 워크 아이템이 취소된다면 cancel은 참을 리턴하지만 실행은 중단되지 않을 것.
static func checkUserNameAvailable(userName: String) -> AnyPublisher<Bool, Error> {
        return Future { result in
            DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
                let answer = userName == "Pikachu" ? true : false
                result(.success(answer))
            }
        }
        .eraseToAnyPublisher()
    }
  • 간단한 실험을 위해 특정 쿼리 값을 넣었을 때에만 참을 리턴하고, 나머지는 거짓을 리턴하는 함수를 만들자
  • 비동기 데이터를 처리하기 위해, 그리고 네트워킹을 모킹하기 위해 Future 내에서 asyncAfter를 써서 1초 뒤에 해당 값을 리턴한다.
    @objc private func textFieldDidEdit() {
        nameAvailabilityWorkItem?.cancel()
        errorLabel.isHidden = true
        let userName = nameTextField.text ?? ""
        
        let workItem: DispatchWorkItem = DispatchWorkItem {
            NetworkManager.checkUserNameAvailable(userName: userName)
                .receive(on: DispatchQueue.main)
                .sink { _ in
                } receiveValue: { [weak self] isAvailable in
                    self?.errorLabel.isHidden = isAvailable
                }
                .store(in: &self.cancellables)

        }
        nameAvailabilityWorkItem = workItem
        DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1), execute: workItem)
    }
  • 특정 텍스트 필드(이름)에 텍스트를 입력할 때마다 API를 통해 텍스트 필드에 입력된 API 쿼리를 실행하는 게 아니라, 타이핑이 끝난 지 1초가 지나서야 비로소 API 쿼리를 실행(디스패치 글로벌 큐에서 asyncAfter를 통해 해당 워크 아이템을 실행하는 타이밍을 1초 후로 조정하는 게 핵심)
  • 지난 시점 생성된 워크 아이템은 cancel을 통해 아직 실행되지 않았다면 취소 가능

위와 같은 쿼리문의 타이밍 조절은 퍼블리셔를 다루는 여러 가지 기법 중 throttle, debounce를 통해 더욱 더 쉽게 조절할 수 있다! 하지만 디스패치 워크 아이템 또한 하나의 방법이 될 수 있다는 것 역시 체크해 놓자

profile
JUST DO IT

0개의 댓글