Dev Notes
Unified ViewModel for UIKit and SwiftUI

Unified ViewModel for UIKit and SwiftUI

A Practical Approach

SwiftMVVMSwiftUIUIKit

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 (opens in a new tab) demonstrates a simple search functionality implemented in both UIKit and SwiftUI. The key components of this architecture are:

  1. A ViewModel that transforms user Input into view-ready Output
  2. SwiftUI and UIKit views that consume the ViewModel
  3. A protocol-based DataService for fetching data
  4. 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:

  1. Gradual migration from UIKit to SwiftUI
  2. Mixing UIKit and SwiftUI views within the same project
  3. 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.