Search
Duplicate

(SwiftUI) ViewModel간 데이터를 전달하고 싶을 때(communication between viewmodel)

간단소개
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
SwiftUI
Scrap
태그
9 more properties
MVVM 패턴으로 iOS 앱개발을 진행하고 있었는데, 진행하다가 ViewModel간에 데이터 통신이 필요한 경우가 생겼다.
사실 ViewModel간 데이터 통신을 한다는 것 자체가 MVVM패턴에 맞지 않는 구조라서 이 문제를 어떻게 해결해야하나 고민이 많이 되었다.
지금의 구조는 지도를 보여주는 MapView와 검색을 진행하는 SearchView가 있고, 각 뷰가 ViewModel을 가지고 있다. (VM = ViewModel)
이때 Search에서 나온 결과 (장소)를 클릭하면 해당 좌표로 Map이 이동해야 한다.
Map의 center focus를 옮겨주는 부분은 MapVM에 들어있고, 검색을 진행하고 검색한 결과 배열을 리턴하는 부분은 SearchVM에 들어있다.
이 때, SearchVM에서 나온 결과 배열 중 클릭한 MapItem을 MapVM 내부의 함수에 전달해 주어야 했다. 처음엔 EnvironmentObject를 활용해서 해당 변수를 두 ViewModel에서 접근하는 방식으로 생각했었다. 하지만 하나의 모델에 두 ViewModel이 접근하는 형태가 MVVM패턴에 적합하지 않다고 판단했고, 많은 시간을 삽질하다가 두가지 장치를 활용해서 해당 문제를 해결하였다.
심지어 처음 생각한 방식은 정상적으로 작동하지도 않았다.
MVVM에 대한 이야기는 다음 포스팅에서 진행하기로 하고, 해결 방법을 알아보자.

EnvironmentObject 사용하기! 하지만 ViewModel에서는 EnvironmentObject를 사용할 수 없다!

SwiftUI에서 ViewModel을 적용하려면, class를 활용해서 ObservableObject를 채택해서 해당 클래스를 ViewModel로 활용하는 방법을 가장 많이 사용한다. 이 때, struct를 활용하면 아래와 같은 오류가 발생한다.
ObservavleObject는 프로토콜이지만, class 타입에만 사용할 수 있다. 이유는 ObservableObject가 AnyObject를 상속하고 있기 때문이다.
결과적으로 ObservableObject를 활용한 ViewModel은 아래와 같은 형태가 기본이 된다.
class SomeViewModel: ObservableObject { ... }
Swift
복사
이 모델을 적용해서 코드를 작성해보면 아래와 같이 나온다. 처음에 생각했던 방식대로 코드를 작성해 보았다.
ViewModel에서 EnvironmentObject의 값에 접근해볼까?
final class SearchViewModel: NSObject, ObservableObject, MKLocalSearchCompleterDelegate { ... @EnvironmentObject var test: MySchedule ... private func search(using searchRequest: MKLocalSearch.Request) { ... mapItem = response?.mapItems[0] if let mapItem = mapItem { ... print(test.item) } }
Swift
복사
하지만 역시나~ 바로 에러가 나와버렸다.
찾아보니 EnvironmentObject는 SwiftUI의 View안에서만 동작한다.
어차피 디자인 패턴에 위배되는 방식이기도 했고 조금 더 괜찮은 방법을 찾아보기로 했다.

View에서 State - Binding 활용하기

ViewModel에서 직접 Environment를 바라보면서 값에 접근할 수 없다면, 징검다리를 두기로 하였다.
상위 View에서 @State를 활용해서 변수를 생성하고 해당 변수를 Binding을 해서 각 ViewModel에 전달해 주는 방식을 적용하기로 하였다.

ViewModel

SearchViewModel은 아래와 같이 Published를 활용한 Model하나를 추가하였다.
final class SearchViewModel: NSObject, ObservableObject, MKLocalSearchCompleterDelegate { ... @Published var resultItem: MKMapItem? = nil ... private func search(using searchRequest: MKLocalSearch.Request) { ... mapItem = response?.mapItems[0] if let mapItem = mapItem { ... resultItem = mapItem } }
Swift
복사
기존에 사용하던 EnvironmentObject는 동일하게 사용한다.
class MapItemModel: ObservableObject { @Published var mapItem: MKMapItem? }
Swift
복사

View

SearchViewModel과 연결되어 있는 View이다.
struct SearchView: View { @ObservedObject var viewModel: SearchViewModel @EnvironmentObject var mapItem: MapItemModel var body: some View { VStack { ... List(viewModel.searchResult) { result in Button(action: { viewModel.moveToLocation(result: result) mapItem.mapItem = viewModel.resultItem }) { ... } } ... } } }
Swift
복사
이제 ViewModel은 View에서 action이 발생하면 해당 event를 처리하고 ViewModel에서 Model을 수정한다. 이 때, View는 ObservedObject로 viewModel을 가지고 있기 때문에 ViewModel이 가지고 있는 Model이 수정될 때, 캐치할 수 있다.
이 때, View가 refresh 되고, mapItem은 viewModel의 resultItem을 받게된다.
우리는 이렇게 EnvironmentObject에 값을 넣을 수 있게 되었다. 이제 이 EnvironmentObject를 활용해서 MapView를 이동시키면 된다.
값을 전달할 때에도 동일한 방식을 적용한다. 위치를 받아올 때에는 아래와 같은 방향이였다면,
ObservableObject → EnvironmentObject
위치를 전달할 때에는 반대 방향으로 보내면 된다.
EnvironmentObject → ObservableObject

ViewModel이 다중으로 연결되는 구조를 피하자

결국 여러개의 ViewModel이 하나의 Model에 연결되는 구조를 피하기 위해서 다양한 방법을 시도해 보다가 가장 간단한 방법을 사용하였다.
Notification을 활용하는 방법도 있었지만, 간단한 구조에 복잡한 코드를 활용하고 싶지 않았다.

Reference