관리 메뉴

김종권의 iOS 앱 개발 알아가기

[Architecture] Coordinator pattern, XCoordinator 프레임워크 [응용], PostTaskManager 본문

Architecture (swift)

[Architecture] Coordinator pattern, XCoordinator 프레임워크 [응용], PostTaskManager

jake-kim 2020. 10. 17. 22:42

* XCoordinator를 이용하여 모든 화면에서 NavigationController하나를 공유하며 화면전환 하는 방법

 + 딥링크를 고려한 코드 스타일 적용

 

Xcoordinator개념편은 여기 참고

 

사전 지식

- XCoordinator에서는 strongRouter와 UnownedRouter가 있는데, 자식들을 계속 참조하며 잃지 않으려면 strongRouter로 사용

단, push와 같이 Transition을 반환하게 되면, strongRouter이후에 unownedRouter로 전달해도 참조를 잃지 않음

- Coordinator에서 다른 Coordinator로 이동 시키려면, addChild(_:) -> Transition함수를 정의하여 이 값을 리턴해야지만 deeplink가 가능한 구조

 

addChild(_:) -> Transition추가

import XCoordinator

extension Transition {
    static func addChild(_ child: Presentable) -> Transition {
        Transition(presentables: [child], animationInUse: nil) { _, _, completion in
            completion?()
        }
    }
}

Start화면

- coordinator를 통해서 이동: case .next부분

- NavigationController를 공유해서 쓰기 위해서 init의 인수에 rootViewController로 삽입 

(rootViewController를 init하지 않으면, 새로운 NavigationController가 생기기 때문에, 다음 push에서 중복 navigat9onController삽입으로 오류 발생)

//
//  StartCoordinator.swift
//  test
//
//  Created by 김종권 on 2020/10/17.
//  Copyright © 2020 jongkwon kim. All rights reserved.
//

import Foundation
import XCoordinator

indirect enum StartRoute: Route {
    case start
    case next

    case back
    case popAndBack(StartRoute)
}

class StartCoordinator: NavigationCoordinator<StartRoute> {
    init(initialRoute: StartRoute) {
        super.init(initialRoute: nil) // nil을 하지 않으면, animation이 디폴트로 적용됨 (switch문에 있는 push와 같은 애니메이션을 사용하려면 아래와 같이 trigger로 따로 불러야함)
        trigger(initialRoute)
    }

    override func prepareTransition(for route: StartRoute) -> NavigationTransition {
        rootViewController.setNavigationBarHidden(true, animated: false)

        switch route {
        case .start:
            // start View Controller 초기화하여 이동
            return .none()

        case .next:
            // Coordinator를 통해서 이동, 단 현재의 rootVC인 navigationController를 공유하기 위해서 인수로 rootVC도 넘김
	let nextCoordinator = NextCoordinator(rootViewController: rootViewController, initialRoute: .next)
            return .addChild(nextCoordinator)

        case .back:
            return .pop()

        case .popAndBack(let startRoute):
            trigger(.back)
            trigger(startRoute)
            return .none()
        }
    }
}

(NextCoordinator코드)

//
//  NextCoordinator.swift
//  test
//
//  Created by 김종권 on 2020/10/17.
//  Copyright © 2020 jongkwon kim. All rights reserved.
//

import Foundation
import XCoordinator

indirect enum NextRoute: Route {
    case next
}

class NextCoordinator: NavigationCoordinator<NextRoute> {
    init(rootViewController: rootViewController, initialRoute: NextRoute) {
	super.init(rootViewController: rootViewController, initialRoute: nil)
        trigger(initialRoute)
    }

    override func prepareTransition(for route: NextRoute) -> NavigationTransition {
        switch route {
        case .next:
            // Next View Controller 초기화하여 이동
            return .none()
        }
    }
}

BaseNavigationCoordinator 사용방법

  • NavigationCoordinator를 상속받아서, 공통처리 로직을 위해 생성 (back, popAndPush, popTwice등)
//
//  BaseNavigationCoordinator.swift
//  test
//
//  Created by 김종권 on 2020/11/03.
//

import Foundation
import UIKit
import XCoordinator

class BaseNavigationCoordinator<T: Route>: NavigationCoordinator<T> {
    func back() -> NavigationTransition {
        if rootViewController.presentedViewController != nil {
            rootViewController.presentedViewController?.dismiss(animated: true)
        } else {
            rootViewController.popViewController(animated: true)
        }
        return .none()
    }

    func popAndPush(route: T) -> NavigationTransition {
        trigger(route)
        let count = rootViewController.viewControllers.count
        if count > 2 {
            rootViewController.viewControllers.remove(at: count - 2)
        }
        return .none()
    }

    func popTwice() -> NavigationTransition {
        let countVC = rootViewController.viewControllers.count
        if countVC > 2 {
            rootViewController.viewControllers.remove(at: countVC - 1)
            rootViewController.popViewController(animated: true)
        } else if countVC == 1 {
            rootViewController.popViewController(animated: true)
        }
        return .none()
    }

    func dismissAndPush(route: T) -> NavigationTransition {
        if rootViewController.presentedViewController != nil {
            rootViewController.presentedViewController?.dismiss(animated: false)
            trigger(route)
        }
        return .none()
    }
}
  • 사용예제: BaseNaviagtionCoordinator를 상속 받고, 공통처리 로직들은 위에서 정의한 함수를 반환
//
//  HomeCoordinator.swift
//  test
//
//  Created by 김종권 on 2020/10/08.
//

import Foundation
import SideMenu
import Domain
import XCoordinator

enum HomeRoute: Route {
    case home
    case menu(HomeVC)
    case back
}

class HomeCoordinator: BaseNavigationCoordinator<HomeRoute> {
    
    let postTaskManager: PostTaskManager
    
    init(rootViewController: RootViewController, postTaskManager: PostTaskManager, initialRoute: HomeRoute) {
        self.postTaskManager = postTaskManager
        super.init(rootViewController: rootViewController, initialRoute: nil)
        trigger(initialRoute)
    }
    
    override func prepareTransition(for route: HomeRoute) -> NavigationTransition {
        
        switch route {
            
        case .home:

            let destinationPickerCoordinator = DestinationPickerCoodinator(
                rootViewController: rootViewController,
                postTaskManager: postTaskManager
            )

            let vc = HomeBuilder.build(
                router: strongRouter,
                postTaskManager: postTaskManager,
                destinationPickerCoordinator: destinationPickerCoordinator
            )

            return .set([vc])

        case let .menu(homeVC):
            let sideMenuCoordinator = SideMenuCoordinator(
                rootViewController: rootViewController,
                postTaskManager: postTaskManager,
                initialRoute: .sideMenu(homeVC)
            )
            return .addChild(sideMenuCoordinator)
            
        case .back:
            return back()
        }
    }
}

* PostTaskManager란 화면간의 이동전에 특정 화면에서 무슨일을 시키고자 할 때 task를 다른 화면에서 등록하고, 해당화면으로 돌아온 경우 처리할 수 있도록 하는 기능

import Foundation
import UIKit

enum PostTaskType {
    case showToast(message: String)
}

enum PostTaskTarget {
    case home
}

struct PostTask {
    let target: PostTaskTarget
    let task: PostTaskType
}

class PostTaskManager {

    private var postTasks = [PostTaskTarget: [PostTaskType]]()

    func register(postTask: PostTask) {
        guard postTasks[postTask.target] != nil else {
            postTasks[postTask.target] = [postTask.task]
            return
        }

        postTasks[postTask.target]?.append(postTask.task)
    }

    func postTasks(postTastTarget: PostTaskTarget) -> [PostTaskType]? {
        if isExist(taskTarget: postTastTarget) {
            return postTasks[postTastTarget]
        } else {
            return nil
        }
    }

    func removeAll() {
        postTasks.removeAll()
    }

    func remove(for input: PostTaskTarget) {
        postTasks[input]?.removeAll()
    }

    private func isExist(taskTarget: PostTaskTarget) -> Bool {
        return !(postTasks[taskTarget]?.isEmpty ?? true)
    }
}

NavigationController를 공유해서 사용할 때, Deeplink사용에 유의

Coordinator에서 넘어온 곳에서 처음에(NextCoordinator에서 .next경우) storngRouter로 초기화 해야함

 

Comments