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() 호출을 잊어버리면 작업이 영원히 대기하고 앱이 정지됩니다. 이를 주의해야합니다.
만약 스위프트의 동시성을 사용해보지 않았다면 조금 러닝커브가 있을 수 있습니다.