본문 바로가기

iOS/Combine Framework

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

728x90

 

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


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

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


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

  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 새로 그림 또는 화면 이동

 

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"으로 변경해서 분홍색 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
  associatedtype Output
  
  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으로 내보내면 됩니다.

 

이를 수행하기 위해선 transform() 함수 하나만 있어도 View layer에게 특정 State를 기반으로 UI 렌더링하라는 notify 내용을 담기 충분하기 때문입니다. 그러나 viewModel이 computed property나 dataSource 등을 반환해야 할 경우, 뷰에서 사용해야 할 경우 그에 맞는 protocol을 선언하고 protocol composition으로 뷰 계층에서 프로토콜을 추가로 선언하면 됩니다. 제가 위에서 소개한 프로젝트에서 View에서 ViewModel을 추가할 때의 타입 선언처럼 말입니다.

 

transform() 함수의 주 역할은 view에서 오는 여러 inputs 이벤트를 transform() 함수의 input(구조체) 매개변수로 받아서, 그 input 구조체 안에 있는 각각의 publisher에서 발생된 이벤트에 맞는 적절한 로직 수행 후 Output으로 반환하는 것입니다. 

 

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 ClockViewControllerInput {
  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를 보내야 합니다.

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 ClockViewControllerState {
  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,
      Output == AnyPublisher<State, Never> { }

 

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

 

  • Input은 위에서 정의한 ClockViewControllerInput ( 3개 퍼블리셔 인스턴스 있음 )으로 제약합니다.
  • State는 위에서 정의한 ViewControllerState으로 제약합니다.
  • 이 특정 State에 대해 특정 .case 값을  publisher로 방출할 AnyPublihser<State, never>타입의 Output을 지정합니다.

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의 특성상 매번 새로운 퍼블리셔를 반환하기 때문입니다.

 

 

ViewModel에서 바인딩한 후에 다시 그 바인딩 됬을 때에 뷰한테 전달할 수 있는 신호들을 각각의  퍼블리셔로 만들어서 View의 Output으로 전달는 경우(Input == struct, Output == struct)나, Input, Output 둘다 한개의 퍼블리셔로 하는 경우(Input == enum, Output== enum)가, Input은 enum으로, Output은 Struct로 정의하는 방법을 연구해봤는데, 각각 장단점은 아래서 소개하려고 합니다. 저는 이 방법이 제일 깔끔하다고 생각합니다.


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

 

초기에 뷰가 로드됬다는 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의 타입을 지웁니다.

 

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(_:) 내부에서 서버에 requset를 한 상태입니다. 이때 서버에서 response는 비동기적입니다. Request 하자마자 즉시 response를 받는게 아니기에, 지금 당장 UI 는 업데이트 되지 않아도 됩니다.
그래서 우선 transform(_:)의 upstream input event를 통한 UI render는 none으로 Output으로 전달합니다. 그러다 비동기적으로 response된 서버의 값이 transform(_:)함수에 completion handler로 오게 된다면 이에 따른 UIrender State를 Output으로 전송해 주는 방식도 좋은 방식이라 생각이 듭니다.

 

 

그리고 간략하게 Input/output를 정의할 수 있는 여러가지 방법들을 실험해봤습니다. 이 경우가 가장 베스트라고 생각하는 이유를 말씀드리려 고합니다.

1. Input == struct, Output == struct

Input을 Struct(안에 여러개 퍼블리셔), Output을 Struct(안에 여러개 퍼블리셔)로 정의할 경우 ViewModel에서 Input에 대한, 바인딩그리고 또 새로 Output 각각의 퍼블리셔의 send -> View에서 바인딩. (일을 두번합니다. 근데 그만큼 퍼블리셔가 많음으로 유연하게 operator chain를 달 수 있습니다.)

2. Input == Struct, State == enum, Output == 1개의 AnyPublihser<State, T> 

이 경우는 제가 작성한 경우 입니다. 장점은 input 퍼블리셔가 여러개이기 때문에, 예를들어 위에서 정의한 퍼블리셔 중 throttle을 사용하는 경우가 있는데, 이 operator를 사용하면 그 특정 시간동안 해당 upstream 퍼블리셔는 새로운 값을 emit할 수 없습니다. 그러나 다른 Input 퍼블리셔는 전달할 수 있습니다. 1처럼 유연합니다. 차이점은 Input에서 발생된 각각의 stream은 viewModel에서 바인딩하지 않고,  그대로 view로 흘러갑니다. 이게 장점이고, Output또한 하나의 publisher와 enum으로 값을 받기에 코드가 적어집니다. 1번의 경우가 하는 모든 상황을 구현할 수 있습니다

3. Input == 하나의 퍼블리셔(값 타입 == enum), Output == 하나의 퍼블리셔(값타입 == enum)

이 경우가 코드가 간소화 되는 가장 최적의 방법임을 석현이형과 연구하다가 발견했습니다. 세계 최초가 되는가? 싶었는데,,, 위 코드에서 적용해본 결과 .... 제 경우에는 버튼이 눌릴때, throttle == 0.4초를 가정했습니다. 만약 showDate enum에서 처리할 throttle이 10초로 제약 했다면, upstream publihser는 10초동안 hideDate enum을 처리하지 못합니다. 그 이유는 upstream publihser는 하나이고, 그 하나지만 값타입이 enum이었고, 특정 enum case일 때 throttle operator를 사용한다면, 그 동안 하나뿐인 Input upstream publihser는 값을 emit할 수 없습니다. 단순히 showDate일 때만 10초 제약이 아니라, hideDate 등 다른 case도 전송할 수 없습니다. 퍼블리셔가 하나이기 때문입니다.

 

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

 

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

728x90