Storyboard中嵌套对象作为IBOutlet的MVVM实现疑问
Great question—this is a super common point of confusion when learning MVVM alongside Storyboards, especially since Apple's tooling sometimes leans into patterns that blur traditional MVVM boundaries. Let’s break this down clearly:
Short Answer
No, this implementation does not align with standard MVVM best practices. Using IBOutlets to nest ViewModel inside a ViewController and ApiClient inside a ViewModel breaks core MVVM principles like separation of concerns, testability, and clear dependency flow.
Why This Approach Falls Short
Let’s unpack the issues with this setup:
- Broken Dependency Flow: MVVM relies on a clear one-way dependency chain:
View (ViewController)→ViewModel→Model/ApiClient. Using IBOutlets reverses this implicit binding—instead of the View holding a reference to its ViewModel (and the ViewModel holding its own dependencies intentionally), the Storyboard is creating and wiring these objects for you. This makes the relationship between components opaque and hard to trace. - Poor Testability: One of MVVM’s biggest strengths is testability. If your
ViewModelgets itsApiClientvia an IBOutlet, you can’t easily swap in a mockApiClientfor unit tests. The Storyboard controls the creation of these objects, so you’re stuck with the real implementation in tests, making it impossible to isolate and test the ViewModel’s logic independently. - Tight Coupling: By tying
ViewModelandApiClientto Storyboard IBOutlets, you’re making these classes dependent on UIKit and Storyboard infrastructure. A proper ViewModel should be a plain Swift class (no need to inherit fromNSObject) that can be reused across different UI layers (like SwiftUI or even command-line tools) without being tied to Storyboards. - Confused Responsibilities: ViewModels exist to handle business logic, data transformation, and expose data to the View. When you use IBOutlets to inject dependencies, you’re mixing setup logic with business logic, making the ViewModel harder to reason about and maintain.
A Compliant MVVM Implementation
Here’s how you’d restructure this to follow MVVM best practices, with clear dependency injection and separation of concerns:
1. ApiClient: Independent and Testable
Keep your API client as a standalone class (no need for NSObject unless you have specific UIKit requirements):
class ApiClient { func fetchData(completion: @escaping (Result<YourDataType, Error>) -> Void) { // Actual API call logic here } }
2. ViewModel: Dependencies Injected via Initializer
The ViewModel receives its ApiClient dependency explicitly through its initializer, making dependencies clear and easy to mock:
class ViewModel { private let apiClient: ApiClient // Inject the ApiClient upfront init(apiClient: ApiClient) { self.apiClient = apiClient } // Example business logic method func loadData() { apiClient.fetchData { [weak self] result in // Process result, update ViewModel state, etc. } } }
3. ViewController: Holds ViewModel, No IBOutlets for Business Logic
The ViewController (part of the View layer) holds a reference to the ViewModel, either via initializer injection (ideal for testing) or lazy initialization if you’re using Storyboards:
class ViewController: UIViewController { private let viewModel: ViewModel // For programmatic navigation/testing: inject ViewModel directly init(viewModel: ViewModel) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } // For Storyboards: initialize ViewModel with ApiClient here required init?(coder: NSCoder) { let apiClient = ApiClient() self.viewModel = ViewModel(apiClient: apiClient) super.init(coder: coder) } override func viewDidLoad() { super.viewDidLoad() // Bind ViewModel state to UI elements here viewModel.loadData() } }
Why Tutorials Might Use the IBOutlet Approach
It’s possible the tutorial uses IBOutlets to keep the example simple and avoid getting into dependency injection details upfront. While it’s a quick way to show the relationship between components, it’s not a pattern you should use in production code.
内容的提问来源于stack exchange,提问作者Nitish

