Refactor a UIKit MVVM ProductViewModel to Combine bindings
The view model below uses a custom Observable<T> and takes a context: UIViewController it never uses. Refactor it to expose its state through Combine and remove the UIKit dependency, so the view model becomes framework-agnostic and unit-testable.
Requirements:
- Expose
product,inCartCount,favouriteButtonTextas Combine publishers (no customObservable). - Remove the
context: UIViewControllerparameter entirely. - Inject the repositories as protocols rather than constructing them inline.
final class ProductViewModel {
let product: Observable<Product?>
let inCartCount: Observable<Int>
let favouriteButtonText: Observable<String>
init(productId: Int, context: UIViewController) {
// your code here — redesign this initializer and the stored state
}
}
Rewrite the implementation.
Expose state as @Published properties (or CurrentValueSubject) instead of the custom Observable, and drop the unused context: UIViewController so the view model no longer depends on UIKit. Inject the repositories as protocols, and let the controller subscribe and store its AnyCancellables.
- ✗Subscribing without storing cancellables, so bindings are torn down immediately
- ✗Keeping repository construction inline in init instead of injecting protocols
- →How would protocol-based repository injection make this view model unit-testable?
- →Where should the button-title formatting live, and why not in the controller's closure?
Inject repository protocols, expose @Published state, and drop the UIKit parameter:
protocol ProductRepositoryProtocol { func getProduct(by id: Int, completion: @escaping (Product?) -> Void) }
protocol CartRepositoryProtocol { func inCartCount(for id: Int, completion: @escaping (Int) -> Void) }
protocol FavouriteRepositoryProtocol { func isFavourite(_ id: Int, completion: @escaping (Bool) -> Void) }
final class ProductViewModel {
@Published private(set) var product: Product?
@Published private(set) var inCartCount: Int = 0
@Published private(set) var favouriteButtonText: String = ""
init(productId: Int,
products: ProductRepositoryProtocol,
cart: CartRepositoryProtocol,
favourites: FavouriteRepositoryProtocol) {
products.getProduct(by: productId) { [weak self] in self?.product = $0 }
cart.inCartCount(for: productId) { [weak self] in self?.inCartCount = $0 }
favourites.isFavourite(productId) { [weak self] in
self?.favouriteButtonText = $0 ? "Remove from Favourites" : "Add to Favourites"
}
}
}
The controller subscribes to $inCartCount, $favouriteButtonText, $product and stores the returned values in a Set<AnyCancellable>. The view model now has no UIKit import, so it can be tested with mock repositories. Button-title formatting ("In Cart (n)") belongs in the view model.