Basically, I want to be able to use Mock when I work with preview in Xcode.
Here a simple situation :
I have a business logic describe by an ItemManager protocol
I have a view ItemView to display and make operation on these Item
I have two ItemManager implementations : ItemManagerImpl (which make the real stuff) and ItemManagerMock
I want to use the mock in Xcode preview and the real manager in my app
This pattern is familiar for me in UIKit, but I not able to write it elegantly in SwiftUI due to the fact that ObservaleObject is PAT.
Naive first try :
struct Item: Identifiable {
var id: String { name } // to keep this simple
var name: String
}
protocol ItemManager: ObservableObject {
@MainActor var items:[Item] { get }
}
@MainActor
class ItemManagerImpl: ItemManager {
//Network and other stuff to get the real items
@Published var items:[Item] = [Item(name: "itemManagerImpl")]
}
@MainActor
class ItemManagerMock: ItemManager {
@Published var items:[Item] = [Item(name: "itemManagerMock")]
}
struct ContentView: View {
@EnvironmentObject // Compiler error : Type 'any ItemManager' cannot conform to 'ObservableObject'
var itemManager: any ItemManager
var body: some View {
List(itemManager.items) { item in
Text(item.name)
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
//.environmentObject(ItemManagerImpl())
.environmentObject(ItemManagerMock())
}
}
With this version, it's not possible to use ItemManager as an EnvironmentObject
Finally I found a way to do this using an any type erasure, but this solution add boilerplate I would prefer to not have to write ...
struct Item: Identifiable {
var id: String { name } // to keep this simple
var name: String
}
protocol ItemManager: ObservableObject {
@MainActor var items:[Item] { get }
var objectWillChange: ObservableObjectPublisher { get }
}
class AnyItemManager: ItemManager {
let wrappedManager: any ItemManager
init(_ wrappedManager: any ItemManager) {
self.wrappedManager = wrappedManager
}
var items: [Item] { self.wrappedManager.items }
var objectWillChange: ObservableObjectPublisher { self.wrappedManager.objectWillChange }
}
@MainActor
class ItemManagerImpl: ItemManager {
//Network and other stuff to get the real items
@Published var items:[Item] = [Item(name: "itemManagerImpl")]
}
@MainActor
class ItemManagerMock: ItemManager {
@Published var items:[Item] = [Item(name: "itemManagerMock")]
}
struct ContentView: View {
@EnvironmentObject
var itemManager: AnyItemManager
var body: some View {
List(itemManager.items) { item in
Text(item.name)
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
// .environmentObject(AnyItemManager(ItemManagerImpl()))
.environmentObject(AnyItemManager(ItemManagerMock()))
}
}
Your 1st option is nice because even if it's not far from mine, I like the fact that clients don't use AnyXXX.
Option 2 could become not easy to read if the number of methods and attributes grow. Also It might be tricky to instantiate ItemManagerImpl if I need dependencies.
Option 3 is perhaps the solution with less boilerplate code and it could be a good choice in many situation, but init method of ItemManager should be make with care to avoid the mock to do an unwanted startup routine.
If I look at the first try I made, It's a pain that any ItemManager don't do the job. I'm not sure to understand why ... Is there something plan on any to make this compile ?
So basically with the SwiftUI dependency injection mechanisms like @EnvironmentObject we have lost the convenience of defining dependencies as Protocols ( an to apply the "program to interface" principle AKA Inversion of Dependency Principle) and we are forced to use concrete classes.
I think this is a big loss and as a result we need to add some boilerplate code to achieve something obvious like mocking a view dependency.
Another big problem, is that this way we have lost forever the possibility to polymorphically choose the dependency at run-time with protocols.
I thought that swift was promoting a more protocol oriented style rather than relying on class inheritance.
What do you think about it?
I just realised this, and it's such a bummer! I guess that, then, the way Brandon Williams and Stephen Celis (from http://pointfree.co) started designing their dependencies is the way to go if you care a lot about testability and ergonomics. Each dependency is concrete, but is initialised with closures you can control whenever you want. They even provide shortcuts to live and mock values.