SwiftUI中深层子视图的无层层传递依赖注入方案咨询
在SwiftUI中为子视图注入依赖的优雅方案
我完全理解你的痛点:既不想把依赖层层传递给子视图(也就是所谓的"prop drilling"),又不想用单例破坏可测试性。针对SwiftUI的场景,这里有几个非常实用的方案,咱们一步步来看:
方案1:使用SwiftUI Environment传递依赖
这是最贴合SwiftUI设计理念的方案——利用SwiftUI的环境系统,把依赖注入到视图树中,子视图可以直接从环境中获取,不需要父视图手动传递。
步骤1:定义依赖协议(便于测试替换)
首先把图片加载器抽象成协议,这样测试时可以轻松替换成Mock实现:
import Combine protocol ImageLoaderProtocol { func loadImage(from url: URL) -> AnyPublisher<Data, Error> } // 实际环境的图片加载器实现 final class DefaultImageLoader: ImageLoaderProtocol { func loadImage(from url: URL) -> AnyPublisher<Data, Error> { URLSession.shared.dataTaskPublisher(for: url) .map(\.data) .mapError { $0 as Error } .eraseToAnyPublisher() } }
步骤2:创建Environment Key
让图片加载器可以被注入到SwiftUI环境中:
import SwiftUI struct ImageLoaderEnvironmentKey: EnvironmentKey { // 设置默认值为实际加载器,开发时无需额外配置即可使用 static let defaultValue: ImageLoaderProtocol = DefaultImageLoader() } // 扩展EnvironmentValues,添加便捷访问属性 extension EnvironmentValues { var imageLoader: ImageLoaderProtocol { get { self[ImageLoaderEnvironmentKey.self] } set { self[ImageLoaderEnvironmentKey.self] = newValue } } }
步骤3:修改子视图和ViewModel
更新FeedListItemViewModel,让它依赖ImageLoaderProtocol:
final class FeedListItemViewModel: ObservableObject { private let post: Post private let imageLoader: ImageLoaderProtocol private var cancellables = Set<AnyCancellable>() @Published var imageData: Data? // 用于存储加载后的图片数据 init(post: Post, imageLoader: ImageLoaderProtocol) { self.post = post self.imageLoader = imageLoader } func loadImage() { guard let imageURL = post.imageURL else { return } imageLoader.loadImage(from: imageURL) .receive(on: DispatchQueue.main) .sink( receiveCompletion: { completion in if case .failure(let error) = completion { print("图片加载失败: \(error.localizedDescription)") } }, receiveValue: { [weak self] data in self?.imageData = data } ) .store(in: &cancellables) } }
然后修改FeedListItemView,保留测试用初始化方法的同时,从环境中获取加载器:
struct FeedListItemView: View { private let post: Post @Environment(\.imageLoader) private var imageLoader @StateObject private var viewModel: FeedListItemViewModel // 生产用初始化:仅传入Post,自动从环境获取加载器 init(post: Post) { self.post = post _viewModel = StateObject(wrappedValue: FeedListItemViewModel(post: post, imageLoader: DefaultImageLoader())) } // 测试用初始化:直接传入ViewModel(便于注入Mock) init(viewModel: FeedListItemViewModel) { self.post = viewModel.post _viewModel = StateObject(wrappedValue: viewModel) } var body: some View { VStack { Text("My Awesome Post") if let data = viewModel.imageData, let image = UIImage(data: data) { Image(uiImage: image) .resizable() .scaledToFit() .frame(height: 200) } else { ProgressView() } } .onAppear(perform: viewModel.loadImage) } }
顶层注入环境依赖
在App入口或者顶层视图注入依赖:
@main struct MyApp: App { var body: some Scene { WindowGroup { FeedListView(viewModel: FeedViewModel(loader: { completion in // 模拟加载数据 DispatchQueue.main.asyncAfter(deadline: .now() + 1) { completion(.success([Post(id: "1", imageURL: URL(string: "https://example.com/image.jpg"))])) } })) .environment(\.imageLoader, DefaultImageLoader()) } } }
方案2:使用可替换的依赖容器(非单例)
这种方式不需要依赖SwiftUI的环境系统,而是用一个全局的可替换容器来存储依赖,既避免了单例的不可测试性,又不需要层层传递。
步骤1:定义容器协议和实现
import Combine // 定义依赖容器的协议,包含所有需要的依赖 protocol DIContainerProtocol { var imageLoader: ImageLoaderProtocol { get } // 可以添加其他依赖,比如网络请求器、存储服务等 } // 实际环境的容器实现 final class DefaultDIContainer: DIContainerProtocol { let imageLoader: ImageLoaderProtocol = DefaultImageLoader() } // 全局容器变量,默认使用实际环境的容器(可替换) var DIContainer: DIContainerProtocol = DefaultDIContainer()
步骤2:修改ViewModel
让FeedListItemViewModel直接从容器中获取依赖,不需要父视图传递:
final class FeedListItemViewModel: ObservableObject { private let post: Post private let imageLoader: ImageLoaderProtocol private var cancellables = Set<AnyCancellable>() @Published var imageData: Data? // 只需要传入Post,依赖自动从容器中获取 init(post: Post) { self.post = post self.imageLoader = DIContainer.imageLoader } func loadImage() { guard let imageURL = post.imageURL else { return } imageLoader.loadImage(from: imageURL) .receive(on: DispatchQueue.main) .sink( receiveCompletion: { completion in if case .failure(let error) = completion { print("图片加载失败: \(error.localizedDescription)") } }, receiveValue: { [weak self] data in self?.imageData = data } ) .store(in: &cancellables) } }
步骤3:简化子视图
现在FeedListItemView只需要传入Post即可:
struct FeedListItemView: View { @StateObject private var viewModel: FeedListItemViewModel // 生产用初始化:仅传入Post init(post: Post) { _viewModel = StateObject(wrappedValue: FeedListItemViewModel(post: post)) } // 测试用初始化:直接传入ViewModel(便于注入Mock) init(viewModel: FeedListItemViewModel) { _viewModel = StateObject(wrappedValue: viewModel) } var body: some View { VStack { Text("My Awesome Post") if let data = viewModel.imageData, let image = UIImage(data: data) { Image(uiImage: image) .resizable() .scaledToFit() .frame(height: 200) } else { ProgressView() } } .onAppear(perform: viewModel.loadImage) } }
测试时替换容器
在测试代码中,只需要替换全局的DIContainer为测试用的容器即可:
// 定义Mock图片加载器 class MockImageLoader: ImageLoaderProtocol { func loadImage(from url: URL) -> AnyPublisher<Data, Error> { // 返回模拟的图片数据 Just(Data()) .setFailureType(to: Error.self) .eraseToAnyPublisher() } } // 测试用容器 class TestDIContainer: DIContainerProtocol { let imageLoader: ImageLoaderProtocol = MockImageLoader() } // 在测试开始前替换容器 override func setUp() { super.setUp() DIContainer = TestDIContainer() }
方案3:使用ViewModel工厂(结合Environment)
这种方案结合了前两个方案的优点:用工厂来创建ViewModel,工厂被注入到环境中,子视图从环境中获取工厂来创建ViewModel,不需要父视图传递任何依赖。
步骤1:定义ViewModel工厂协议
protocol FeedListItemViewModelFactoryProtocol { func makeViewModel(for post: Post) -> FeedListItemViewModel } // 实际环境的工厂实现 final class DefaultFeedListItemViewModelFactory: FeedListItemViewModelFactoryProtocol { private let imageLoader: ImageLoaderProtocol init(imageLoader: ImageLoaderProtocol) { self.imageLoader = imageLoader } func makeViewModel(for post: Post) -> FeedListItemViewModel { FeedListItemViewModel(post: post, imageLoader: imageLoader) } }
步骤2:将工厂注入到Environment中
struct FeedListItemViewModelFactoryKey: EnvironmentKey { static let defaultValue: FeedListItemViewModelFactoryProtocol = DefaultFeedListItemViewModelFactory(imageLoader: DefaultImageLoader()) } extension EnvironmentValues { var feedListItemViewModelFactory: FeedListItemViewModelFactoryProtocol { get { self[FeedListItemViewModelFactoryKey.self] } set { self[FeedListItemViewModelFactoryKey.self] = newValue } } }
步骤3:修改子视图
让FeedListItemView从环境中获取工厂,创建ViewModel:
struct FeedListItemView: View { private let post: Post @Environment(\.feedListItemViewModelFactory) private var factory @StateObject private var viewModel: FeedListItemViewModel // 生产用初始化:仅传入Post,通过工厂创建ViewModel init(post: Post) { self.post = post _viewModel = StateObject(wrappedValue: factory.makeViewModel(for: post)) } // 测试用初始化:直接传入ViewModel init(viewModel: FeedListItemViewModel) { self.post = viewModel.post _viewModel = StateObject(wrappedValue: viewModel) } var body: some View { VStack { Text("My Awesome Post") if let data = viewModel.imageData, let image = UIImage(data: data) { Image(uiImage: image) .resizable() .scaledToFit() .frame(height: 200) } else { ProgressView() } } .onAppear(perform: viewModel.loadImage) } }
顶层注入工厂
在App入口注入工厂:
@main struct MyApp: App { private let diContainer = DefaultDIContainer() var body: some Scene { WindowGroup { FeedListView(viewModel: FeedViewModel(loader: { completion in // 模拟加载数据 DispatchQueue.main.asyncAfter(deadline: .now() + 1) { completion(.success([Post(id: "1", imageURL: URL(string: "https://example.com/image.jpg"))])) } })) .environment(\.feedListItemViewModelFactory, DefaultFeedListItemViewModelFactory(imageLoader: diContainer.imageLoader)) } } }
总结
- 如果你的项目比较小,推荐使用方案2,代码简洁,可测试性好;
- 如果你的项目比较大,或者更倾向于SwiftUI的原生方式,推荐使用方案3,结构更清晰,扩展性更好;
- 方案1适合简单的依赖传递,是SwiftUI原生的解决方案。
内容的提问来源于stack exchange,提问作者Teddy K
相关产品推荐
相关产品推荐

