TIL

[Swift]ReactorKit, Coordinator 그리고 화면 이동과 데이터 전달

Kim Roks 2025. 1. 9. 19:20

개요

이번 포스팅에서는 테이블 뷰의 아이템을 선택했을 때, 코디네이터(Coordinator) 패턴을 사용하여 화면을 전환하고, 필요한 데이터를 전달하는 방법을 다룹니다.

이 글에서는 ReactorKit, RxSwift, 그리고 Coordinator 패턴을 사용하여 AView에서 BView로 데이터(User)를 전달하는 예시를 설명합니다.
각 디자인패턴이나 라이브러리의 상세한 설명은 생략됩니다.

 

액션 정의하기

먼저, A뷰와 B뷰가 있고, A뷰에서 B뷰로 이동한다고 가정합니다. 전달할 데이터는 User 형식으로 정의

데이터 모델 정의

struct User: Decodable {
    let name: String?
    let age: Int?
}

AView의 Reactor 정의

AView의 Reactor는 비동기 처리를 통해 데이터를 이미 받아왔다는 가정하에 진행

import ReactorKit
import RxSwift

// 해당 리액터에서 비동기 처리가 이미 되어있다는 가정하에 진행
class AViewReactor: Reactor {
    enum Action {
        // IndexPath 를 전달
        case itemSelected(IndexPath)
    }

    enum Mutation {
        // 기존 테이블 뷰에 네트워킹으로 받아놓은 데이터를 전달하기 위함
        case pushToItemView(User)
    }

    struct State {
        var selectedItem: User?
        var items: [User]
    }

    let initialState: State

    init(items: [User]) {
        self.initialState = State(items: items)
    }

    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        //  해당 아이템이 갖고 있는 데이터를 사용하기 위해 indexPath를 받음
        case .itemSelected(let indexPath):
            let user = currentState.items[indexPath.row]
            return .just(Mutation.pushToItemView(user))
        }
    }

    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state

        switch mutation {
        case .pushToItemView(let user):
            newState.selectedItem = user
        }

        return newState
    }
}

코디네이터 정의

AView와 BView의 화면 전환을 담당하는 코디네이터를 정의합니다.

AView의 코디네이터 정의

protocol ACoordinator: Coordinator {
    func navigateToBView(with data: User)
}

final class DefaultACoordinator: ACoordinator {
    weak var parentCoordinator: Coordinator?
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        // 초기 화면 설정
    }

    func didFinish() {
        // 코디네이터 종료 시 처리
    }

    func navigateToBView(with data: User) {
        let bViewCoordinator = DefaultBViewCoordinator(navigationController: navigationController)
        bViewCoordinator.parentCoordinator = self
        childCoordinators.append(bViewCoordinator)
        // BView의 Coordinator는 start 시 data를 받아 이동하도록 합니다.
        bViewCoordinator.start(with: data)
    }
}

BView의 코디네이터 정의

protocol BViewCoordinator: Coordinator {
    func start(with data: User)
}

final class DefaultBViewCoordinator: BViewCoordinator {
    weak var parentCoordinator: (any Coordinator)?
    var childCoordinators: [any Coordinator] = []
    var navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        // 초기 화면 설정
    }

    func start(with data: User) {
        // BView는 생성 시 data를 받도록 작성했습니다.
        let bView = BView(data: data)
        bView.coordinator = self
        navigationController.pushViewController(bView, animated: true)
    }

    func didFinish() {
        parentCoordinator?.remove(child: self)
    }
}

// BViewController 정의
final class BView: UIViewController {
    weak var coordinator: BViewCoordinator?
    var user: User?

    init(data: User) {
        self.user = data
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        self.title = user?.name
        // user 정보를 이용해 추가적인 UI 구성 가능
    }
}

AViewController에서 화면 이동 호출하기

이제 AViewController에서 선택된 아이템을 Reactor와 바인딩하고, 상태 변화에 따라 화면 이동을 처리합니다.

import UIKit
import RxSwift
import RxCocoa
import ReactorKit

// 기타 코드는 생략, 가독성을 위해 View 프로토콜을 extension에서 채택
extension AViewController: View {
    func bind(reactor: AViewReactor) {
        // 액션이 일어날 테이블 뷰
        myTableView.rx.itemSelected
            .map { Reactor.Action.itemSelected($0) }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

        // 상태 변화에 따른 화면 이동 처리
        reactor.state
            .map { $0.selectedItem }
            .compactMap { $0 }  // nil 값을 지우기 위함
            .subscribe(onNext: { [weak self] data in
                // 뷰가 가지고 있는 코디네이터에서 해당 메서드를 호출하며, data를 전달
                self?.coordinator?.navigateToBView(with: data)
            })
            .disposed(by: disposeBag)
    }
}

결론

이번 포스팅에서는 테이블 뷰의 아이템 선택 시 Reactor와 Coordinator 패턴을 사용하여 데이터를 전달하고 화면을 전환하는 방법을 살펴보았습니다. 이 방법을 통해 뷰와 비즈니스 로직을 분리하고, 네비게이션 로직을 좀 더 모듈화하여 관리할 수 있습니다.

 

참조:  https://benoitpasquier.com/data-between-views-using-coordinator-pattern-swift/