[SwiftUI] Dependency Injection for ViewModel

정유진·2023년 3월 20일
0

swift

목록 보기
18/25
post-thumbnail
post-custom-banner

들어가며

의존성 주입은 종속성을 줄여 코드의 결합도를 낮추기 위한 프로그래밍 테크닉이다. MVMM 디자인 패턴을 채택하면서 사용자 액션을 처리하고 데이터 바인딩을 해주는 ViewModel을 View마다 property로 가지는데 View 간에 hierarchy가 있다보니 모델도 그러해서 이 위계 상 어느 지점에서 객체를 생성하고 의존성을 주입할 것인가에 대한 고민이 생겼다. 그 고민을 해결하고자 참고한 문서들을 정리해보겠다.

StateObject

App, Scene, View 안에서 생성하여 Observable Object를 참조하는 싱글톤 소스를 만든다.

기본 사용

  1. @Environment*
    viewmodel이 자신이 책임지는 view보다 outlive

  2. @StateObject
    SwiftUI view와 생애 주기를 함께한다.

  3. @ObservedObject
    자신의 생애를 application의 어딘가에 위임해둔 상태. 어딘가에서 생성된 viewModel 객체를 주고받을 때 사용한다.

이와 같은 특성 때문에 최초 ViewModel을 생성할 때에는 StateObject로 선언하고 하위 View의 EnvironmentObject로 넘겨 의존성을 주입한다.

class MyModel: ObservableObject {
    @Published var name = "yujinj"
    @Published var age = 100
}

struct MyView: View {
    @StateObject var viewmodel = MyModel()

    var body: some View {
        Text(viewmodel.name) 
        MySubView()
            .environmentObject(viewmodel)
    }
}

코드를 보면 class인 DataModel을 MyView에서 StateObject로 선언하였다. model 내의 name 값이 바뀔 경우 view가 새로 그려질 것이다. type이 class라는 것에 주목!🧐 공식 문서에 따르면 structure, string, integer와 같은 value type은 @State wrapper를 쓰도록 권고하고 있다.

생성자를 통한 의존성 주입

struct MyInitializableView: View {
    @StateObject private var model: DataModel

    init(name: String) {
        // SwiftUI ensures that the following initialization uses the
        // closure only once during the lifetime of the view, so
        // later changes to the view's name input have no effect.
        _model = StateObject(wrappedValue: { DataModel(name: name) }())
    }

    var body: some View {
        VStack {
            Text("Name: \(model.name)")
        }
    }
}

이런 방법도 가능하구나 정도로 참고하면 좋을 것 같다. 이 방법은 view init 시점 이후에 변하지 않을 값을 넘길 때에나 의미가 있다. 상위 view의 name이 변하더라도 하위 뷰는 알 방법이 없다.

물론 name이 바뀔 때에 강제로 view를 다시 그리도록 강제할 수는 있다. view에 id를 주어 이름이 바뀔 때에 id가 바뀌도록 하면 이에 따라 view 또한 새로 그려진다. 하지만 성능을 포기해야할 것이다.

MyView(name: name)
    .id(name)

// value가 2개 이상일 경우?
var hash: Int {
    var hasher = Hasher()
    hasher.combine(name)
    hasher.combine(age)
    return hasher.finalize()
}

MyView(name: name, age: age)
    .id(hash)

참고자료

profile
느려도 한 걸음 씩 끝까지
post-custom-banner

0개의 댓글