https://medium.com/swiftblade/using-swift-concurrency-with-coordinator-pattern-de290b95f09b
위 글을 보고 번역/정리한 글! 자세한 내용은 위 링크 참JO !
코디네이터 패턴을 사용하는 프로토콜의 예제 코드.
얘를 스위프트의 동시성을 사용하여 리팩토링 한다고 하네여~
protocol ParentCoordinator: AnyObject {
var children: [AnyObject] { get set }
func start()
}
protocol ChildCoordinator: AnyObject {
var teardown: ((Self) -> Void)? { get set }
func start()
}
extension ChildCoordinator {
func stop() {
teardown?(self)
}
}
위 코드에서의 문제는 두 가지가 있습니다.
stop()
메소드 호출을 잊어버릴 가능성children
을 해제해줘야하는 필요성protocol Coordiantor: AnyObject {
associatedtype Output
func start() async throws -> Output
}
start()
함수에 async, throws
키워드를 포함하여, 예전 ChildCoordinator
의 teardown
클로저를 삭제할 수 있습니다.
teardown()
클로저는 비동기였기 때문에 스위프트의 동시성을 사용하여 좀 더 간결하고 읽기 쉬운 코드로 리팩토링 되었습니다.
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
static let shared = UIApplication.shared.delegate as! AppDelegate
var window: UIWindow?
private(set) var coordinator: AppCoordinator!
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds) // 1
self.window = window
self.coordinator = AppCoordinator(window: window)
self.coordinator.start() // 2
return true
}
}
UIWindow
를 생성하고, AppCoordinator
로 넘김start()
메소드를 호출하여 뷰컨트롤러를 설정하도록 합니다.이 친구는 3가지 메인 흐름이 있습니다.
@MainActor final class AppCoordinator {
private let window: UIWindow
init(window: UIWindow) {
self.window = window
// 아래 더미를 만들지 않으면 async로 실행하는 start()가 있어서
// 앱이 크래쉬가 난다네용
let dummyViewController = UIViewController()
window.rootViewController = dummyViewController
window.makeKeyAndVisible()
}
func start() {
// 1
if UserDefaults.standard.isLoggedIn {
showHome()
}
else {
showLogin()
}
}
private func showHome() {
Task {
let coordinator = HomeCoordinator(window: window)
try? await coordinator.start()
UserDefaults.standard.isLoggedIn = false
start()
}
}
private func showLogin() {
Task {
let coordinator = LoginCoordinator(window: window)
let result = try? await coordinator.start()
// 2
print("Login result: \(String(describing: result))")
// 3
UserDefaults.standard.isLoggedIn = true
// 4
start()
}
}
}
HomeCoordinator
, 아니면 LoginCoordianator
를 띄워준다await
를 통과하면, 로그인 코디네이터의 결과가 준비되었음을 의미합니다. 그리고 실제 앱이였다면 user, session
객체를 사용하겠지만 여기서는 그냥 문자열을 사용했다start
를 호출하여 showHome()
이 호출될 수 있게 만든다.이는 새로운 Coordinator
프로토콜을 채택합니다.
// 코드가 길어 필요한 부분만 남겼습니다. 자세한건 본문 참조
@MainActor final class LoginCoordinator: Coordinator {
private lazy var navigationController = UINavigationController()
private var continuation: CheckedContinuation<String, Error>?
func start() async throws -> String {
// 1.
let viewController = LoginLandingViewController()
viewController.delegate = self
navigationController.setViewControllers([viewController], animated: false)
window.rootViewController = navigationController
// 2.
return try await withCheckedThrowingContinuation { self.continuation = $0 }
}
}
extension LoginCoordinator: LoginLandingViewControllerDelegate {
func loginLandingViewControllerDidSelectLogin(_ viewController: LoginLandingViewController) {
let viewController = LoginViewController()
viewController.delegate = self
navigationController.pushViewController(viewController, animated: true)
}
}
extension LoginCoordinator: LoginViewControllerDelegate {
func loginViewControllerDidFinishLogin(_ viewController: LoginViewController, result: String) {
// 3.
continuation?.resume(returning: result)
}
func loginViewControllerDidCancel(_ viewController: LoginViewController) {
navigationController.popToRootViewController(animated: true)
}
}
continuation
인스턴스를 만들어 보유합니다이는 메인 콘텐츠가 있는 부분입니다. 여기는 데모 버전이므로, 구매 버튼, 로그아웃 버튼만 구현해있습니다.
위에 로그인 코디네이터랑 거의 동일한 모습 ~
// 코드의 필요한 부분만 냅뒀슴다
@MainActor final class HomeCoordinator: Coordinator {
private lazy var navigationController = UINavigationController()
private var continuation: CheckedContinuation<Void, Error>?
func start() async throws {
let viewController = HomeViewController()
viewController.delegate = self
navigationController.setViewControllers([viewController], animated: false)
window.rootViewController = navigationController
try await withCheckedThrowingContinuation { self.continuation = $0 }
}
}
extension HomeCoordinator: HomeViewControllerDelegate {
func homeViewControllerDidLogOut(_ viewController: HomeViewController) {
continuation?.resume(returning: ())
}
func homeViewControllerPurchase(_ viewController: HomeViewController) {
Task {
guard let viewController = window.rootViewController else { return }
let coordinator = PurchaseCoordinator(rootViewController: viewController)
let result = try? await coordinator.start()
// Use the result. Here we just print it.
print("Purchase result: \(String(describing: result))")
}
}
}
이 코디네이터는 UIAlerControllers
들을 관리하고, 앱의 구매 흐름을 모방합니다. 이 코디네이터는 앞에서 본 코디네이터들과의 차이가 있는데, window.rootViewController
를 사용하지 않는 부분입니다. Alert이라 당연한거라고도 볼 수 있을거 같습니다.
@MainActor final class PurchaseCoordinator: Coordinator {
enum PurchaseResult {
case cancelled
case success
}
let rootViewController: UIViewController
private var continuation: CheckedContinuation<PurchaseResult, Error>?
init(rootViewController: UIViewController) {
print("\(type(of: self)) \(#function)")
self.rootViewController = rootViewController
}
func start() async throws -> PurchaseResult {
rootViewController.present(purchaseAlertController(), animated: true)
return try await withCheckedThrowingContinuation { self.continuation = $0 }
}
// MARK: - Alert Controllers
func purchaseAlertController() -> UIAlertController {
let alert = UIAlertController(title: "Purchase flow",
message: "Do you want to purchase?",
preferredStyle: .alert)
// Cancel button
alert.addAction(UIAlertAction(title: "Cancel",
style: .cancel,
handler: { _ in
self.rootViewController.present(self.purchaseResultAlertController(.cancelled), animated: true)
}))
// OK button
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
Task {
await self.purchase()
self.rootViewController.present(self.purchaseResultAlertController(.success), animated: true)
}
}))
return alert
}
func purchase() async {
let alert = UIAlertController(title: "Purchasing ...",
message: "",
preferredStyle: .alert)
self.rootViewController.present(alert, animated: true)
try? await Task.sleep(nanoseconds:1_000_000_000) // wait 1 second
await alert.dismissAnimatedAsync() // 1
}
func purchaseResultAlertController(_ result: PurchaseResult) -> UIAlertController {
let title = result == .success ? "Purchase Success" : "Purchase Cancelled"
let alert = UIAlertController(title: title,
message: "",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
self.continuation?.resume(returning: result)
}))
return alert
}
}
extension UIViewController {
func dismissAnimatedAsync() async {
await withCheckedContinuation { continuation in
dismiss(animated: true) {
continuation.resume()
}
}
}
}
start()
자체가 값을 반환하므로 출력이 눈에 잘 띄고 가독성이 높아집니다. 더 이상 코디네이터 커스텀 변수 안에 출력을 숨기거나 새로운 델리게이트 프로토콜을 추가할 필요가 없습니다.코디네이터에서 수행하는 작업이 여전히 델리게이트 및 클로저 콜백을 사용하면, Continuation
을 사용하여 비동기 작업 완료를 코디네이터와 연결해야 하는데 이는 여전히 수동입니다.
이어서 resume()
호출을 잊어버리면 작업이 영원히 대기하고 앱이 정지됩니다. 이를 주의해야합니다.
만약 스위프트의 동시성을 사용해보지 않았다면 조금 러닝커브가 있을 수 있습니다.