Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Combine의 flatMap을 통한 ViewModel에서의 Coordinator 의존성 제거 방법 #32

Closed
SHIVVVPP opened this issue Nov 27, 2020 · 2 comments · Fixed by #34
Closed

Comments

@SHIVVVPP
Copy link
Collaborator

SHIVVVPP commented Nov 27, 2020

의논거리 🤔

Combine의 연산자중 flatMap(maxPublisher:_:) 를 사용하는 방법으로
기존에 ViewModel에서 coordinator를 주입받아 coordinator?.show~() 하는 방식에서
coordinator에 대한 의존성 없이 signal을 통해 화면 전환 및 전환 후 데이터를 받는 것이 가능하다는 것을 알게되었습니다.

먼저 flatMap을 살펴보면

flatMap(maxPublishers:_:)
Transforms all elements from an upstream publisher into a new publisher up to a maximum number of publishers you specify.
업스트림 Publisher의 모든 요소를 ​​지정한 최대 Publisher 수까지 새 Publisher로 변환합니다.

위와 같은 역할을 하는 연산자인데요
예제를 보면 더 이해하기 쉽습니다.

public struct WeatherStation {
    public let stationID: String
}

var weatherPublisher = PassthroughSubject<WeatherStation, URLError>()

cancellable = weatherPublisher.flatMap { station -> URLSession.DataTaskPublisher in
// wheatherPublisher를 옵저빙 하여 받은 station.stationID 를 사용해
// URL 요청을 보내고 이 요청을 다시 옵저빙 하는 것입니다
    let url = URL(string:"https://weatherapi.example.com/stations/\(station.stationID)/observations/latest")!
    return URLSession.shared.dataTaskPublisher(for: url)
}
.sink(
    receiveCompletion: { completion in
        // Handle publisher completion (normal or error).
    },
    receiveValue: {
        // Process the received data.
    }
 )

weatherPublisher.send(WeatherStation(stationID: "KSFO")) // San Francisco, CA
weatherPublisher.send(WeatherStation(stationID: "EGLC")) // London, UK
weatherPublisher.send(WeatherStation(stationID: "ZBBB")) // Beijing, CN

정리해 보자면
보통 Publisher에 sink 를 사용하여 값을 옵저빙 하게 되는데
flatMap 을 사용하면 옵저빙 하던 값을 사용해 만든 새로운 Publisher를 옵저빙 하게 될 수 있는 것이죠

이를 우리 프로젝트에 활용해 본다면

기존에 ViewModel에 Coordinator를 주입해

// PrepareRunCoordinator
func showGoalTypeActionSheet(goalType: GoalType) -> AnyPublisher<GoalType, Never>
// PrepareRunViewModel
coordinator?.showGoalTypeActionSheet(goalType: goalTypeObservable.value)
            .filter {
                $0 != self.goalTypeObservable.value
            }
            .sink(receiveValue: { goalType in
                self.goalTypeObservable.send(goalType)
                self.goalValueObservable.send(goalType.initialValue)
                if goalType != .none {
                    self.goalTypeSetupClosed.send()
                }
            })
            .store(in: &cancellables)

이렇게 showGoalTypeActionSheet(_:) 의 반환값을 구독해 ActionSheet 에서 goalType 변경에 대한 옵저빙을 하던 것에서

ViewModel에 var showGoalTypeActionSheet: PassthroughSubject<GoalType, Never> 라는 새로운 시그널을 만들어 준뒤에
Coordinator의 showPrepareRunViewController() 에서 ViewModel을 생성할 때
이 시그널을 옵저빙하여 showGoalTypeActionSheet(_:)을 하고
flatMap 을 통해 이 showGoalTypeActionSheet(_:)의 리턴값인 AnyPublisher<GoalType,Never>를 다시 옵저빙하는 것으로 변경해
ActionSheet에서의 goalType 변경을 처리할 수 있습니다.

예시를 보여드리면

func showPrepareRunViewController() {
        let prepareRunVM = PrepareRunViewModel()

        prepareRunVM.showGoalTypeActionSheet
            .compactMap { [weak self] in self?.showGoalTypeActionSheet(goalType: $0) }
            .flatMap { $0 }
            .compactMap { $0 }
            .sink { [weak prepareRunVM] goalType in prepareRunVM?.didChangeGoalType(goalType) }
            .store(in: &cancellables)
        ...
        let prepareRunVC = PrepareRunViewController(with: prepareRunVM)
        navigationController.pushViewController(prepareRunVC, animated: true)
    }

이렇게 coordinator 주입 없이 coordinator가 prepareRunVM을 바인딩하여
화면 전환과 화면간의 데이터 이동을 처리할 수 있게 되는 것이죠

결과적으로 ViewModel에서의 화면 전환 요청 처리는 이렇게 됩니다

    func didTapSetGoalButton() {
        showGoalTypeActionSheet.send(goalTypeObservable.value)
//        coordinator?.showGoalTypeActionSheet(goalType: goalTypeObservable.value)
//            .filter {
//                $0 != self.goalTypeObservable.value
//            }
//            .sink(receiveValue: { goalType in
//                self.goalTypeObservable.send(goalType)
//                self.goalValueObservable.send(goalType.initialValue)
//                if goalType != .none {
//                    self.goalTypeSetupClosed.send()
//                }
//            })
//            .store(in: &cancellables)
    }

    func didTapStartButton() {
        showRunningScene.send((goalTypeObservable.value, goalValueObservable.value))
//      coordinator?.showRunningScene(
//                  goalType: goalTypeObservable.value,
//                  goalValue: goalValueObservable.value)
    }

    func didTapGoalValueButton() {
        showGoalValueSetup.send((goalTypeObservable.value, goalValueObservable.value))
//        coordinator?.showGoalValueSetupViewController(
//            goalType: goalTypeObservable.value,
//            goalValue: goalValueObservable.value
//        ).compactMap {
//            self.goalValueSetupClosed.send()
//            return $0
//        }
//        .sink(receiveValue: { goalValue in
//            self.goalValueObservable.send(goalValue)
//        })
//        .store(in: &cancellables)
    }

이 방법을 우리 프로젝트에 적용해 보면 어떨까요??

@whrlgus
Copy link
Collaborator

whrlgus commented Nov 27, 2020

혹시 flatmap에 대한 애플 공식문서 이외에, 참고하신 자료가 있다면 공유해 주실 수 있을까요?

@SHIVVVPP
Copy link
Collaborator Author

https://medium.com/better-programming/reactive-mvvm-and-the-coordinator-pattern-done-right-88248baf8ca5
아까 주소를 까먹어서 올리지 못했네요!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants