Environment 참조 SwiftUI에서 @StateObject ViewModel의 의존 관계

24075 단어 iOSSwiftSwiftUItech
StateObject 초기 값이 필요하지만 일반적으로 해당 시점Environment에서는 정확한 값을 읽을 수 없습니다.
이 글에서 소개한 LazyStateObject를 이용하면 그 문제를 해결할 수 있다👌
사용LazyStateObject의 샘플 코드는 다음과 같다.
sample.swift
import SwiftUI

struct Dependency: DynamicProperty {
    @Environment(\.networkClient) var networkClient
    @Environment(\.storageClient) var storageClient
}

@MainActor
class ViewModel: ObservableObject {
    let dependency: Dependency

    @Published var count = 0

    init(dependency: Dependency) {
        self.dependency = dependency
    }

    func increment() async {
        count = await dependency.networkClient.fetchCount(...)
    }
}

struct ContentView: View {
    @LazyStateObject(dependency: Dependency(), { dependency in
        ViewModel(dependency: dependency)
    })
    var viewModel

    var body: some View {
        VStack {
            // このタイミングで初回アクセスが走るため適切な値を持って初期化処理が行われる
            Text("\(viewModel.count)")  

            Button(action: increment) {
                Text("increment")
            }
        }
    }

    func increment() {
        Task {
            await viewModel.increment()
        }
    }
}
중점 일치DynamicProperty
이렇게 하면 렌더 트리와 연관된 객체Environment 등을 참조할 수 있습니다.
응용 프로그램이 시작될 때 의존성을 주입하는 것이 좋다
또한 미리보기 시 모델 등을 주입할 수 있다
app.swift
import SwiftUI

@main
struct App: SwiftUI.App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.networkClient, NetworkClientImpl())
                .environment(\.storageClient, StorageClientImpl())
        }
    }
}
그러나Environment 단일 테스트 시 임의의 설치를 주입할 수 없기 때문에 아래에 소개된 OverridableEnvironment를 이용하여
!ViewModel(networkClient: ..., storageClient: ...) 같은 이니셜은 앞으로 대응할 필요가 없지만 의존성이 증가하면 개작하는 시간이 늘어난다💧
샘플 코드에 대해 다음과 같은 변경을 진행하다
struct Dependency: DynamicProperty {
+    @OverridableEnvironment(\.networkClient) var networkClient
+    @OverridableEnvironment(\.storageClient) var storageClient
-    @Environment(\.networkClient) var networkClient
-    @Environment(\.storageClient) var storageClient
}
이렇게 하면 테스트 코드를 아래의 모쿠 등의 설치로 바꿀 수 있다
import XCTest

class SampleTests: XCTestCase {
    @MainActor
    func test() async {
        var deps = Dependency()
        deps.networkClient = ...
        deps.storageClient = ...

        let viewModel = ViewModel(dependency: deps)
        await viewModel.increment()

        XCTAssertEqual(viewModel.count, ...)
    }
}

소스 코드


LazyStateObject.swift
@propertyWrapper
public struct LazyStateObject<ObjectType, Dependency>: DynamicProperty
where ObjectType: ObservableObject {
    public var wrappedValue: ObjectType {
        if let object = holder.object {
            return object
        }
        let newObject = initializer(dependency)
        holder.object = newObject
        return newObject
    }

    @StateObject private var holder = ObjectHolder<ObjectType>()

    private let dependency: Dependency
    private let initializer: (Dependency) -> ObjectType

    public init(dependency: Dependency, _ initializer: @escaping (Dependency) -> ObjectType) {
        self.dependency = dependency
        self.initializer = initializer
    }
}

private final class ObjectHolder<ObjectType>: ObservableObject
where ObjectType: ObservableObject {
    var object: ObjectType? {
        willSet {
            cancellable = newValue?.objectWillChange
                .sink(receiveValue: { [weak self] _ in
                    self?.objectWillChange.send()
                })
            objectWillChange.send()
        }
    }

    private var cancellable: Cancellable? {
        willSet { cancellable?.cancel() }
    }
}
OverridableEnvironment.swift
#if DEBUG

@propertyWrapper
public struct OverridableEnvironment<Value>: DynamicProperty {
    public var wrappedValue: Value {
        get { stateOverrideValue ?? overrideValue ?? environment.wrappedValue }
        set {
            overrideValue = newValue
            stateOverrideValue = newValue
        }
    }

    private var environment: Environment<Value>
    private var overrideValue: Value?
    @State private var stateOverrideValue: Value?

    public init(_ keyPath: KeyPath<EnvironmentValues, Value>) {
        environment = Environment(keyPath)
    }

    public mutating func reset() {
        overrideValue = nil
        stateOverrideValue = nil
    }
}

extension OverridableEnvironment: Sendable where Value: Sendable {}

#else

// テスト以外では通常の方法で値を注入すれば良いので DEBUG 以外では標準の `Environment` として振る舞う
public typealias OverridableEnvironment = Environment

#endif

좋은 웹페이지 즐겨찾기