Mastering Concurrency in iOS - Part 3 (Dispatch Group, Dispatch Work Item)
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
프로퍼티를 사용한다면 시간과 관계 없이 해당 태스크가 모두 종료되었음을 알림받을 수 있음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)
}
asyncAfter
를 통해 해당 워크 아이템을 실행하는 타이밍을 1초 후로 조정하는 게 핵심)cancel
을 통해 아직 실행되지 않았다면 취소 가능위와 같은 쿼리문의 타이밍 조절은 퍼블리셔를 다루는 여러 가지 기법 중
throttle
,debounce
를 통해 더욱 더 쉽게 조절할 수 있다! 하지만 디스패치 워크 아이템 또한 하나의 방법이 될 수 있다는 것 역시 체크해 놓자