You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 10:33:14