Unified ViewModel for UIKit and SwiftUI
A Practical Approach
Introduction
In the evolving landscape of iOS development, many projects find themselves at a crossroads between UIKit and SwiftUI. While SwiftUI offers a modern, declarative approach to building user interfaces, UIKit remains relevant due to its maturity and extensive feature set. This article explores a practical approach to creating a unified ViewModel
protocol that can be applied in both UIKit and SwiftUI frameworks, allowing for a smooth transition or coexistence of these frameworks within a single project.
Project Overview
Our sample project demonstrates a simple search functionality implemented in both UIKit and SwiftUI. The key components of this architecture are:
- A
ViewModel
that transforms userInput
into view-readyOutput
- SwiftUI and UIKit views that consume the
ViewModel
- A protocol-based
DataService
for fetching data - A tab-based interface that allows switching between SwiftUI and UIKit implementations
The Unified ViewModel
At the heart of our architecture is the ViewModel
class. Let’s break down its key components:
class ViewModel: ViewModelType {
private let dataService: DataServiceType
init(dataService: DataServiceType) {
self.dataService = dataService
}
func transform(input: ViewModelInput) -> ViewModelOutput {
// Implementation details...
}
}
The ViewModel takes a DataServiceType
as a dependency, allowing for easy testing and flexibility. It conforms to a ViewModelType
protocol, which defines the transform
method:
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
This protocol-based approach allows us to create a consistent interface for our ViewModel, regardless of whether it’s used in a UIKit or SwiftUI context.
Handling Input and Output
The ViewModel uses a transform
method to convert user input into view-ready output:
struct ViewModelInput {
let query: AnyPublisher<String, Never>
}
struct ViewModelOutput {
let result: AnyPublisher<String, Never>
let isLoading: AnyPublisher<Bool, Never>
let error: AnyPublisher<String?, Never>
}
By using AnyPublisher
, we can easily work with both UIKit’s imperative style and SwiftUI’s declarative style.
SwiftUI Implementation
In the SwiftUI view, we create a ViewState
class that acts as a bridge between the ViewModel and the view:
class ViewState: ObservableObject {
@Published var query: String = ""
@Published var result: String = ""
@Published var isLoading: Bool = false
@Published var error: String?
private var cancellables = Set<AnyCancellable>()
init(viewModel: ViewModel) {
self.subscribe(to: viewModel)
}
private func subscribe(to viewModel: ViewModel) {
// Subscription logic...
}
}
The SwiftUI view then uses this ViewState
:
struct SwiftUIContentView: View {
@StateObject private var viewState: ViewState
init(viewModel: ViewModel) {
self._viewState = StateObject(
wrappedValue: ViewState(viewModel: viewModel)
)
}
var body: some View {
// View implementation...
}
}
UIKit Implementation
For the UIKit view, we directly use the ViewModel in the ViewController
:
class ViewController: UIViewController {
private let viewModel: ViewModel
private var cancellables = Set<AnyCancellable>()
init(viewModel: ViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
bindViewModel()
}
private func bindViewModel() {
// Binding logic...
}
}
Conclusion
By creating a unified ViewModel that works seamlessly with both UIKit and SwiftUI, we’ve demonstrated a flexible architecture that allows for:
- Gradual migration from UIKit to SwiftUI
- Mixing UIKit and SwiftUI views within the same project
- Reusing business logic across different view implementations
This approach provides a practical solution for projects that need to bridge the gap between UIKit and SwiftUI, allowing developers to leverage the strengths of both frameworks while maintaining a clean and consistent architecture.