본문 바로가기

iOS/Combine Framework

[Swift] MVVM에서 Combine을 활용한 ViewModel Input/Output binding 개념 완벽 부수기 +_+ | MVC와 MVVM 차이 진짜 뿌수기!!!!

 

안녕하세요! 이번 포스트는 MVVM 패턴을 사용할 때 Conbine framework로 ViewModel과 View를 Input/Output binding하는 방법에 대해 소개하려고 합니다. 뷰에서 발생 가능한 Input!, 그리고 View가 화면을 그려야 할 State!를 ViewModel은 Output!함으로 뷰의 render가 진행됩니다. 소프티어 부트캠프 활동을 하며 새로 배운 개념이 많아 기존의 글을 리빌딩 했습니다.


그 전에! MVVM과 MVC 아키텍처 패턴의 차이가 무엇인지 명확하게 알아야합니다. 둘의 차이가 무엇인지 아시는 분들만이 MVVM을 잘 활용한다고 할 수 있습니다. 저는 제 나름대로 MVVM과 MVC 아키텍처의 차이점을 정리해봤습니다(관련 포스트 링크). MVC, MVVM 차이를 이해한 후에 왜 ViewModel에서 input/output 바인딩을 사용하는지 소개하려고 합니다 +_+

ViewController과 ViewModel에서 Input/Output 이란?

작 전에 프로젝트를 보면서 하면 좋을 것 같아 이번 예시에 사용될 프로젝트를 공유합니다!! ( 깃헙 링크입니다. 참고해주세요!!!!!!! )

  1. View에서 이벤트 Input 발생한다.
  2. ViewModel에 View에서 발생되는 모든 Input(이벤트)에 대한 비즈니스 처리를 담당합니다. ( 필요에 따라 서버에 요청하던, 데이터를 이용해 새로운 결과를 만들어내던 등..)
  3. 위에서 Input에 대한 이벤트 처리의 결과로 View의 UI가 변경되야 할 View의 새로운 state가 생길 것입니다.(예를들어 버튼의 opaque를 100으로 바꾼다던지, 레이블의 값을 변경한다던지 등)
  4. 이런 View 의 State를 ViewModel의 Output으로 정의하고, 그대로 View에게 전달합니다.
  5. View는 ViewModel의 Output(viwe의 UI state)에 따른 UI를 render합니다.

 

어.. 말이 길어졌네요. 다시 요약을 하자면

 

  1. View에서 Inputs 발생
  2. ViewModel에서 각각의 Inupts에 대한 비즈니스 로직이나 모델을 활용해 처리 후 UI View에게 변경해야 할 state를 Output으로 View에게 전달
  3. View는 ViewModel로부터 Output를 받아 UI 새로 그림 또는 화면 이동

 

 

간단하게 이렇게 표시할 수 있습니다.

 

 

ViewController에서 모든 로직을 전부 처리해도 되지만, UI로직을 VC에서 담당하고 뷰의 모델(데이터)을 가공하는 일이나 비즈니스 로직을 VM에서 담당하도록 구현한다면 SOLID에서 SRP을 준수한다고 볼 수 있습니다.

 

MVC에선 massive한 VC의 역할을 뷰 모델에게 넘겨줌으로 뷰 컨트롤러가 담당하던 책임을 덜 수 있고, 뷰의 생명주기, UI render 및 델리게이트처리, 이벤트 감지 등의 역할만 담당할 수 있게됩니다.

 

한 화면에서 발생가능한 로직이 많아지게 된다면 이마저도 분리해서 특정 객체는 DataSource, Delegate담당 등을 처리하도록 역할을 분리해서 코드를 처리하면 훨씬 유지보수하기도 수월해집니다. 최소한의 변경에도 용이해집니다: )

 

이때 VC가 own하고 있는 객체들의 호출 함수들 각각의 객체에 맞는 protocol을 통해 VC가 protocol을 의존한다면 해당 객체를 stub객체로 변경한다던가, 테스트할때 원하는 로직만 테스트할 수 있게 됩니다. 무분별한 protocol화는 코드만 길어지지만, DIP를 준수해야 테스트에 용이하고 ui 개발할 때도 용이합니다.

화면전환 제어, 채팅, report, post관련 모든 기능들.. 서버에 호출하고 값을 가공해서 뷰한테 전달해주는 것을 뷰 컨트롤러가 다 한다면 massive해지지 않을까요?ㅎㅎ.. 


Input/Output with Combine using enum State and Output publisher!!

 

 

https://github.com/SHcommit/StudyCombine/tree/master/MVVM%20Inputs-Outputs%20binding%20with%20combine/Source/Feature/Main

 

이 예제는 ClockViewController에서 showDate 버튼을 클릭했을 때, 분홍색 화면에 showDate 버튼 클릭 당시의 Date()를 뷰 모델에게 데이터를 전달해서 눌렀을 당시 Date를 "yyyy-MM-dd HH:mm:ss" Format로 변경한 후에 화면에 다시 분홍색 Label에 보여줄려고 합니다. 동시에 0.4초간 버튼이 클릭 된 애니메이션 처리를 할 것입니다.

 

hideDate 버튼을 클릭했을 때 다시 "Outputs UI Rendered" 문자열로 레이블의 텍스트를 변경하는 것을 MVVM 패턴에서 Combine + Input/Output binding으로 구현하려고 합니다. 동시에 0.4초간 버튼이 클릭 된 애니메이션 처리를 할 것입니다.

 

각각의 버튼이 터치될 때 에니메이션이 반영되야 함으로, 한번 이벤트가 발생되면 0.4초동안은 ViewModel에게 특정 버튼의 event를 전달하지 못하도록 Input의 특정 퍼블리셔 신호를 제한(throttle)할 것입니다.

2. ViewModelable

MVVM에서 ViewModel을 정의할 때 ViewModel이 담당해야 할 가장 중요한 두 가지가 있습니다.

 

1. View에서 발생되는 각각의 inputs 이벤트에 대한 데이터 가공이나 서버 호출 등을 담당합니다. 처리한 이후

2. 변경되야 하는(UI를 새로 render 하거나 화면전환 등) view state를 다시 View에게 반환해 주는 것입니다.

 

protocol ViewModelable {
  associatedtype Input
  associatedtype State
  typealias Output = AnyPublisher<State, Never>
  
  func transform(_ input: Input) -> Output
}

 

여기서 이 프로토콜의 transform(_:) -> Output 함수는 View와 binding에 사용될 ViewModel의 함수입니다. 단 하나! ViewModel의 역할은 ViewController, View에서 발생하는 event들을 정의한 Inputs 신호가 오면, ViewModel은 신호에 맞게 적절한 로직을 처리합니다. 그리고 UI가 갱신되거나 아무것도 일어나지 않아도 된다는 StateOutput으로 상위 객체(뷰 컨트롤러)에게 전달하는 것입니다. 또는 model의 데이터나 서버에서 받아져 온 모델의 completionHandler를 바탕으로 업데이트 되야 할 View의 새로운 UI State를 ViewModel의 output으로 내보내면 됩니다.

 

output은 AnyPublisher를 사용했습니다. AnyPublisher를 왜 사용하는 것일까요? AnyPublisher에 대한 개념을 딮 하게 정리한번했는데 참고해주시면 감사합니다. (deep dive into AnyPublisher post 링크 바로가기~~)

 

cf. protocol을 사용할때의 장점은 구현체가 해당 프로토콜을 준수한다면, 프로토콜을 준수하는 서로 다른 구현체를 주입할 수 있다는 장점이 있습니다.

 

transform() 함수의 주 역할은 view에서 오는 여러 inputs 이벤트를 transform() 함수의 input(구조체) 매개변수로 받아서, 그 input 구조체 안에 있는 각각의 publisher에서 발생된 이벤트에 맞는 적절한 로직 수행 후 Output으로 반환하는 것입니다. 예를 들자면, ChatViewModelable은 사용자가 채팅을 입력하면 서버에 그 데이터를 저장하고, 서버에서 받아온 새로운 데이터가 화면에 보여져야 한다면 State(개선해야할 뷰의 상태)를 뷰한테 전달하는 역할입니다.

 

View와 Model간 중개자 역할을 하는 것이 viewModel의 역할입니다. 이때 View에서 발생되는 모든 user interaction(버튼 클릭이나 텍스트 필드 입력 등)에 대한 로직 처리를 viewModel에서 함으로 ViewUI render에 대해서만 신경쓰고, model은 데이터 관리, 가공, viewModel은 비즈니스 로직과 데이터 처리와 같은 일을 담당합니다. 1편 MVC와 MVVM에서도 말했지만 model이 비즈니스 처리 로직도 담당할 수 있습니다.

 

뷰 모델이 너무 많은 역할을 담당하면 잘게 조개면 됩니다: )

3. 뷰에서 발생되는 이벤트 Inputs로 정의하기

이제 ViewController의 view, subviews, ViewController에서 발생되는 lifecycle, user interaction등 여러 이벤트를 ViewModel의 Input으로 정의합니다.

 

현재 상황의 경우 view lifecycle 중 하나인 viewDidLoad, showDate 버튼 클릭, hideDate 버튼 클릭 총 3가지 이벤트가 있습니다. 물론 뷰가 새로 나타날 때마다 데이터를 갱신해야 할 경우 viewWillAppear 이벤트에서 fetch하기 위해 viewWillAppear publisher를 Input 구조체에 추가하면 되지만 제가 정의한 상황에서는 서버에서 받아오는 값은 없으므로 앞에서 말한 3가지를 View layer의 Input으로 하겠습니다.

 

struct ClockViewModelInput {
  let viewDidLoad: AnyPublisher<Void,Never>
  let showDate: AnyPublisher<Date,Never>
  let hideDate: AnyPublisher<Void,Never>
}

 

View에서 발생하는 각각의 Inputs는 언제 이벤트가 발생될 지 모르기에 각각의 publisher를 선정해야 합니다. 각각의 퍼블리셔는 각각의 뷰에서 특정 이벤트가 발생할 때마다 해당 이벤트를 뷰 모델에게 전달할 수 있는 특정 publihser, subject로 send()합니다.

 

ViewModel에서는 showDate 버튼 클릭했을 때, hideDate 버튼 클릭했을 때, viewDidLoad됬을때 총 3개의 publisher가 담긴 Input 구조체를 매개변수로 받습니다. 각각의 published value에 대해서 적절한 처리 후에 view state를 업데이트하기 위해서는 ViewModel에서 Output state를 보내야 합니다.

 

AnyPublisher는 Deep Dive into AnyPublisher 포스트에서도 소개했지만 모든 Publishers의 반환 퍼블리셔를 AnyPublisher 구조체로 wrapping해서 추상화 하는 것입니다. MyAnyPublisher라는 구조체를 Publisher protocol로부터 상속받아서 이걸 이제 추상화 타입으로 씁시다!! 이런 느낌과 비슷합니다. 장점은 Publishers의 구체적인 output을 AnyPublisher 타입으로 erase하는 역할을 합니다. 이또한 실제 Input 객체가 어느 퍼블리셔로 초기화 될지 Input을 호출하는 측에서 알지 않아도 된다는 장점이 있습니다.

3. 뷰모델에서 처리한 로직 결과에 따라 업데이트 될 view state 정하기

현재 상황에서 viewModel의 transform 함수 안에서 Input 구조체 안에 있는 각각의 publisher들 이벤트 신호에 대해 어느 로직을 처리하고 Output 신호를 어떻게 보내는지 좋을까요.. 그 전에 뷰모델의 transform(_:)에서 각각의 로직을 처리했을 때, 변화 될 뷰의 최종 state를 지정해야 합니다. 이 state는 enum으로 정의합니다 그리고 Output publihser의 value로 지정합니다. Output publihser는 단 하나입니다. 그러나 value 타입이 enum이라, 여러가지 타입 중 하나를 보낼 수 있습니다.

Input, Output을 정의하는 방법이 여러가지 있는데 뒤에서 각각의 장 단점을 소개하려고 합니다.

 

 

1. showDate 버튼이 눌렸을 때는 input.showDate 퍼블리셔를 통해 이벤트 신호를 viewModel's transform(_:)에서 받습니다. 버튼이 눌린 현재 시간을 계산해서 반환해야 합니다. + 버튼이 눌러졌을 때 애니메이션이 작동됨으로 최소 0.4초동안 중복되서 버튼이 눌리면 안됩니다. 

2. hideDate 버튼이 눌렸을 때 발생되는 Input.hideDate 퍼블리셔를 통해 이벤트 신호를 viewModel's transform(_:)에서 받습니다. 이땐 label을 원래 상태로 되돌려야 합니다. 이 원래 상태 값은 model에 있습니다 이 model 값을 반환해야 합니다. + 버튼이 눌러졌을 때 애니메이션이 작동됨으로 최소 0.4초동안 중복되서 버튼이 눌리면 안됩니다.

3. viewDidLoad 시점에 ViewController에서 setup, bind가 진행됨으로 이때 event가 발생되지만. update될 UI state 는 없습니다.

 

enum ClockViewModelState {
  case showTime(String)
  case hideTime(String)
  case none
}

 

1,2,3 각각의 publisher 이벤트에 대한 로직을 처리하게 된다면 그 결과로 현재 시간을 반환하거나(showTime), 현재 시간을 숨기면서 label의 원래 값으로 반환하거나(hideTime), 아무것도 발생하지 않는(none) 3가지 경우로 UI state가 갱신되어 새로 그려질(render) 것입니다.

4. 뷰모델에서 처리한 로직 결과에 따라 업데이트 될 view state를 Output publihser로 보내기

이제 남은 것은 viewModel의 output을 어떻게 view한테 Output publihser를 정하는 것입니다. publisher의 특징은!!!! 이벤트 신호를 sink나 subscriber로 받기 전가지 여러 operator를 chaining해서 결합해서 값을 쪼물딱 쪼물딱 변경할 수 있고, downstream에게 값을 emit할 수 있습니다. transform의 return 값이 publisher여야 합니다. 그래야만 뷰에서 subscribe해서 바인딩을 할 수 있습니다. transform()의 리턴 값인 output publisher를 view에서 bind하면 바인딩 성공입니다.

이때 Output publhiser는 단 하나로 지정했습니다. 

 

protocol ClockViewModelable: ViewModelable
where Input == ClockViewControllerInput,
      State == ClockViewControllerState { }

 

ClockViewModel이 준수할 ClockViewModelable을 선언합니다. 이때 ViewModelable 프로토콜을 상속받습니다. 그리고 위에서 지정한 Input, State를 기반으로 단 하나의 Output publihser를 선언합니다.

 

  • Input은 위에서 정의한 ClockViewControllerInput ( 3개 퍼블리셔 인스턴스 있음 )으로 제약합니다.
  • State는 위에서 정의한 ViewControllerState으로 제약합니다.
  • 이 특정 State에 대해 특정 .case 값을  publisher로 transform함수를 호출할 객체에게 방출할 퍼블리셔로 AnyPublihser<State, Never>타입의 Output을 지정합니다.
  • AnyPublisher로 타입 추상화는 ClockViewModelable을 준수하는 구조체가 실제로 어느 퍼블리셔를 사용해서 반환하는지 알지 않아도 된다는 엄청난 장점이 있습니다: )

protocol의 장점은 강하게 결합된 객체간 의존성을 느슨하게 만들어준다는 장점이 있습니다. ClockViewModelable을 dependency로 갖고 있는 상위 객체의 테스트를 할 때 VM이 초점이아니라면 Dummy 객체를 넣을 수 있습니다.

 

5. 뷰모델 transform()함수 구현

그리고 남은건 ViewModelCase에 정의된 transform()함수입니다. 

 

 

위 에서 각각의 Input에 따른 State를 위해 ViewModel의 transform(_:)에서 처리해주어야 합니다.

 

extension ClockViewModel: ClockViewModelable {
  func transform(_ input: Input) -> Output {
    let viewDidLoad = viewDidLoadChains(input)
    let showTime = showTimeChains(input)
    let hideTime = hideTimeChains(input)
    
    return Publishers
      .MergeMany([
        viewDidLoad,
        showTime,
        hideTime
      ]).eraseToAnyPublisher()
  }
}

 

그전에 여기서 초점은 View에서 발생된 event Input struct 내부 각각의 publisher에 대한 value emit 신호를 ViewModel에서 바인딩하지 않습니다. 그 신호 그대로 operator로 chains를 통해 새로운 operator publihser로 변환해서 Output publihser를 반환하고 그대로 신호를 다시 View에서 바인딩할 것입니다.

 

간단요약! 뷰에서 발생한 신호는 뷰에서 바인딩한다!! 뷰모델에서 바인딩하지 않는다!

 

그렇다면 Input 구조체의 upstream publihser는 3개 이고, 서로 다른 value(Void, Date)를 전달합니다. 그런데 Output publihser타입은 새로운 타입 (State == ClockViewControllerState)을 값으로 방출합니다.... 
그리고 Combine의 특징은, publihser의 Output generic type, subscriber의 Input generic type. 즉 퍼블리셔 보낼 값 타입과 구독자가 퍼블리셔로부터 받을 값이 동일히야 퍼블리셔는 값을 emit할 수 있습니다. 

 

이 방법은? 서로 다른 3개의 퍼블리셔의 upstream Input타입을 operator로 받아서 operator chain을 통해 로직을 처리 후, map operator로 서로 다른 타입의 upstream value에서 반환 타입을 State 중 하나의 값으로 downstream으로 지정!

 

서로 다른 3개의 퍼블리셔지만 subscriber에게 emit할 value는 State 중 하나. 즉 State 타입이 된다는 것. 이를 merge함수로 반환하거나 새로운 퍼블리셔 머지로 반환!!

 

여기서 flatMap을 사용할 경우 좋지 않습니다. flatMap의 특성상 매번 새로운 퍼블리셔를 반환하기 때문입니다. 내부적으로 demand를 unlimited로 할 경우 버퍼에 퍼블리셔들을 저장하기 때문입니다.

 

 

최대한 깔끔히 그렸는데 그림이 잘 안보일 수 있겠네요..

 

VC는 protocol인 ClockViewModelable을 의존합니다. ClockViewModel은 ClovkViewModelable을 준수합니다. IoC(역전 제어)를 VC 외부에서 하게 된다면, DI를 하게 됬을때 이와 같이 VC, VM 두 객체가 ViewModelable protocol을 의존하게 됩니다.

 

프로토콜을 준수한다는 것은 VC를 소유하는 상위 객체의 테스트를 할 때, 아니면 VC에서 뷰컨이 사라져야 하는 상황에서 strong reference cycle 이 발생되는지 ViewModel 변수를 제외하고 테스트를 한다고 하면 Stub이나 dummy 객체를 넣을때에도 이점이 있을 것입니다: )

 

https://developer.apple.com/videos/play/wwdc2019/722/

 

예를들어 뷰 컨트롤러의 특정 기능들을 테스트하는 과정에서, 뷰모델이 아닌 다른 작업에 대한 테스트를 할 때 실제 뷰컨트롤러 구현체를 넣었는다고 가정하겠습니다. ViewModelable의 transform에서 반환하는 퍼블리셔들이 많다면 위 사진 처럼 MergeMany 안에 선언한 각각의 퍼블리셔들 머지될 publisher's 구독과정들이 발생됩니다.(위의 함수들이 각각 호출됩니다.) 이런 과정 대신 DummyViewModel을 넣는다면 테스트가 좀 더 빨리 끝날 수 있음을 예상할 수 있습니다.

 

struct DummyClockViewModel: ClockViewModelable, ClockViewModelDataSource {
  func currentHour() -> String {""}
  
  func transform(_ input: ClockViewModelInput) -> Output {
    return Just(State.none).eraseToAnyPublisher()
  }
}

 


다시 프로젝트로 돌아와서,,

 

초기에 뷰가 로드됬다는 ViewController의 lifecycle event 가 발생됬을 때는 viewModel에서 아무런 작업(Input에서 Output으로 변하는transform 작업)을 하지 않을 것입니다. View가 새로 그려야 할 상황은 아닙니다. 그럼에도 upstream의 신호가 끊기지 않고 Ouput publihser를 통해 View에게 전달해야 해야 합니다. transform(_:)함수에서 input.viewDidLoad 퍼블리셔로부터 방출된 값을 map operator를 통해  " _ -> State in " 아무것도 받지않지만 반환값은 State enum 의 .none 캐이스로 Publihsers.Map<State, Never> 퍼블리셔로 반환할 것입니다.

 

왜일까요?

 

map operator를 사용했을때, 아니 그전에, operator를 사용할 때 어떻게 다른 combine operator를 연이어 쓸 수 있는 걸까요? (아시는 분은 스킵!!)
사실 map operator는 무언가 반환합니다. 그게 바로!! Publihsers.Map<Self, T> 퍼블리셔로 반환됩니다. 그리고 이상황은 반환 타입이 AnyPublihser이기 때문에 타입 erase를,, eraseToAnyPublihser()를 해야합니다. (operator 대부분이 이런 특징을 지녔고 가장 많이 사용할 combine map operator의 특징을 다른 포스트에 정리했습니다. - 링크 )

 

 

input.showDate 퍼블리셔에서 값이 보내질 때, 한번 버튼을 클릭했을 때 애니메이션이 동작됨으로 값 방출된 이후 0.4초간 동작을 하지 않도록한 후에 showDate 퍼블리셔로부터 전달받은 Date 값으로 "yyyy-MM-dd HH:mm:ss"타입의 string으로 변경 후 다시 view의 State를 .showTime(curTime)으로 anyPublihser타입으로 전달합니다.

 

hideDate도 마찬가지입니다. hideDate 의 경우 viewModel에서 가지고 있는 model의 특정 데이터를 반환합니다.

 

이렇게 현재 transform() 함수에는 3개의 publihser가 있습니다. viewDidLoadChains, showTimeChains, hideTimeChains publihser가 있습니다. 이들 전부 Merge 함수를 통해 한개의 publihser로 만듭니다. publhiser로 만들 때 value, error 타입이 같아야 하는데, 각각의 경우마다 State중의 한 값, Never타입으로 일치하기 때문에 merge가 가능합니다. 3개의 publhiser를 1개의 publisher로 모아서 반환!! 이때 transform 함수의 output 은 AnyPublisher<State, Never>이기 때문에 merge publihser의 타입을 지웁니다.

 

 

Publishers.MergeMany<Upstream>은 특징이 있습니다. Merge한 각각의 퍼블리셔들이 전부 completion방출해야만 최종 completion이 방출됩니다. merge한 각각의 퍼블리셔 중 특정한 퍼블리셔가 에러를 방출한다고 해서 Publishers.MergeMany 퍼블리셔는 곧바로 종료되지 않습니다.

 

6. ViewController에서 바인딩 및 Input 구조체 안에 있는 publihser각각에 대해 send() 보내기

이제 남은 것은 뷰 컨트롤러에서 viewModel.transform 함수를 바인딩 해주는 것과, viewController에서 발생할 수 있는 이벤트에 대한 publihser를 만든 후에 이벤트가 발생할 때마다 send()로 publish하는 것입니다. 이때 각각 전송되는 publisher를 Input 구조체로 묶어서 transform()함수의 input 매개변수로 보낼 것입니다.

 

protocol ViewBindable {
  associatedtype State
  associatedtype OutputError: Error
  
  func bind()
  func render(_ state: State)
  /// publisher의 occured error 에 대한 헨들링이 필요하다면, 이 메서드를 구현해서 사용하셔야 합니다.
  /// 기본적으로 동작x
  func handleError(_ error: OutputError)
}

extension ViewBindable {
  func handleError(_ error: OutputError) { }
}

 

우선 뷰 컨트롤러에서 뷰 모델의 transform(_:)에 대한 바인드 할 때를 protocol로 지정했습니다. 

 

 

이 세 publihser는 showDate버튼이 클릭됬을 때, hideDate 버튼이 클릭됬을 때, viewDidLoad 이벤트가 발생했을 때 곧바로 send()로 바인드 하고 있는 subscriber(sink)지점 에게 값을 전송 할 publhiser들입니다.

 

 

뷰 컨트롤러에는 3개의 UIComponent가 있습니다. viewModel도 보유하고 viewModel과의 바인딩을 유지할 수 있는 subscriptions 변수도 갖고 있습니다. 물론 뷰 모델도 갖고 있는데, DIP + DI를 위해!! protocol + composition으로 정리했습니다. 근데 여기서 DataSource까지 반환하는 예시를 만들려고 했지만 글이 너무 길어질 것 같아.. 이렇게 할 수 있다는 느낌만 주기 위해 추가적으로 컴포지션으로 선언했습니다.

 

 

중요! passthroughSubject는 그냥 신호가 가는게 아닙니다. send()를 호출해야 자신을 구독하는 subscriber에게 신호를 전달할 수 있습니다. (subject관련 개념 글을 정리해봤습니다)

showDateButton, hide DateButton은 각각 터치 될 때마다 이 이벤트 헨들러를 addTarget으로 달아줬습니다. 이 각각의 버튼이 터치될 때마다의 로직은 ViewController에서 처리를 하지 않고 위에서 정의한 viewModel의 transform() 함수 내부에서 특정 input 매개변수 구조체의 publihser에 operator (map, throttle등) 를 달아서 처리를 한 후에 그 흐름을 이어서 downstream인 output publihser로 보내주는 것입니다.

 

 

뷰가 나타날 때마다 viewLoad 퍼블리셔에게 send()신호를 보냅니다. 

 

ViewController 는 3개의 퍼블리셔 showDate, hideDate, viewLoad를 갖고 있습니다. 그리고 초기화를 선언과 동시에 해줬습니다. 이제 ViewControllerInput struct 를 생성하기 위한 인스턴스 3개가 정확하게 선언되고 특정 이벤트가  뷰컨트롤러에서 발생했을 때 위의 3개의 퍼블리셔를 통해 값을 전달합니다.

 

이제 남은 것은 transform 함수의 input과, output을 ViewController에서 바인딩 하는 것입니다. 

 


State를 편하게 쓰기 위해 typealias를 선언했습니다. (참고로 에러타입은 사용하지 않지만 사용해도 됩니다.)

 

bind함수는 viewDidLoad()시점에 딱 한번만 호출합니다.


이제 ViewController에서 발생된 특정 publhiser가 emit 되면 Input 구조체안에 프로퍼티 중 하나인 struct타입의 AnyPublisher 인스턴스가 emit 하게 됩니다. 이렇게 input 구조체의 초기화를 했으면, viewModel의 transform함수의 input으로 대입합니다. 이때 transform함수는 아까 Pulbihsers.MergeMany([...]).eraseToAnyPublihser() 로 타입을 지운 하나의 publihser를 방출합니다. 그 퍼블리셔를 output 변수에 저장 후, sink를  받습니다. 이때 $0은 OutPut의 value인 ViewControllerState(enum)가 도착합니다. 

 

 

참고로 이때, animate에서 애니메이션을 주었습니다.

 

cf. 서버에 데이터를 요청할 경우,,,

View에서 발생된 특정 이벤트가 transform(_:)의 input으로 왔고, input으로 인해 transform(_:) 내부에서 Restful api통신 약속한 서버에 requset를 한 상태입니다. 이때 서버에서 응답받는 response는 비동기적으로 올 것입니다.

 

Request 하자마자 즉시 response를 받는게 아니기에, 지금 당장 UI 는 업데이트 되지 않아도 됩니다. (그 대신 서버에서 요청 중이라는 것을 서버에서 응답 받기 전까지 화면에 사용자한테 잠깐만 기다려주라고 인디케이터를 보여주면 좋겠죠+_+)


그래서 우선 transform(_:)의 upstream input event를 통한 UI render는 none으로 Output으로 or 네트워크 중이라는 것을 식별할 networking이라는 case만들어서 전달해도 됩니다. 그러다 비동기적으로 response된 서버의 값이 transform(_:)함수에 completion handler로 오게 된다면 이에 따른 UIRender State를 Output으로 전송하고, 인디케이터를 종료하는 방식도 좋은 방법입니다

 

 

아래는 전체 코드입니다. 궁금한 점이나 틀린 부분 발견하시면 댓글로 알려주시면 정말 감사합니다 :)

하트 한번 눌러주세요🥹 

 

References:

프로젝트 예제 링크 바로 들여다보기!!!!