Using Mock/Protocol as @EnvironmentObject

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 ?

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:

  1. 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)
    }
}
  1. 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.

  1. 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

Thanks for your ideas :pray:.

  • 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?

3 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.