gbitaudeau
(Guillaume BITAUDEAU)
1
Hi,
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()))
}
}
Is there a better way to achieve that ?
1 Like
To use @EnvironmentObject you need to know the concrete type, any solution that fails to provide that is not feasible. You have a couple of options:
- Inverse the concrete class and the protocol:
protocol ItemManagerImplProtocol {
func validate(manager: ItemManager)
}
struct ItemManagerMock: ItemManagerImplProtocol {
func validate(manager: ItemManager) {}
}
struct ItemManagerImpl: ItemManagerImplProtocol {
func validate(manager: ItemManager) {}
}
class ItemManager: ObservableObject {
@Published var name: String = ""
private let _underlyingImpl: ItemManagerImplProtocol
init(implementation: ItemManagerImplProtocol) {
_underlyingImpl = implementation
}
func validate() {
_underlyingImpl.validate(manager: self)
}
}
- Drop the protocol altogether:
struct ItemManagerImpl {
let validate: (ItemManager) -> Void
}
class ItemManager: ObservableObject {
@Published var name: String = ""
private let _underlyingImpl: ItemManagerImpl
init(implementation: ItemManagerImpl) {
_underlyingImpl = implementation
}
func validate() {
_underlyingImpl.validate(self)
}
}
extension ItemManagerImpl {
static let mock = ItemManagerImpl(validate: { _ in })
static let `default` = ItemManagerImpl(validate: { _ in })
}
These two also separate the state from the implementation, which is something you might want.
- Perhaps the cleanest way is to use inheritance:
class ItemManager: ObservableObject {
@Published var name: String = ""
func validate() {}
}
class ItemManagerMock: ItemManager {
override func validate() {}
}
But make sure you satisfy SwiftUI's type matching: use .environmentObject(ItemManagerMock() as ItemManager) not .environmentObject(ItemManagerMock())
1 Like
gbitaudeau
(Guillaume BITAUDEAU)
3
Thanks for your ideas
.
- 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?
4 Likes
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.