Skip to content

Instantly share code, notes, and snippets.

@TheiOSDude
Created November 15, 2021 16:56
Show Gist options
  • Save TheiOSDude/929488c379ad17933e5f6b45041d6078 to your computer and use it in GitHub Desktop.
Save TheiOSDude/929488c379ad17933e5f6b45041d6078 to your computer and use it in GitHub Desktop.
Swift DI with Factories
import UIKit
// Services conforming to protocols.
// SOLID: Depend on abstractions, not concrete implementations. Dependency Inversion principle
// DECLARE FIRST SERVICE
protocol MyFirstServiceProtocol {
func myFirstMethod()
}
class MyFirstServiceImplementation: MyFirstServiceProtocol {
func myFirstMethod() {
print("My First Service")
}
}
// DECLARE SECOND SERVICE,
protocol MySecondServiceProtocol {
func mySecondMethod()
}
class MySecondServiceImplementation: MySecondServiceProtocol {
func mySecondMethod() {
print("My second service")
}
}
// DECLARE FACTORIES, by protocol, allows us to swap out the implementating class later, when we subclass the `container`.
protocol FirstServicesFactory {
var firstService: MyFirstServiceProtocol { get }
}
protocol SecondServiceFactory {
var secondService: MySecondServiceProtocol { get }
}
// Container, each dependency factory must be conformed to here, to provide its implementing class.
class ServicesContainer: FirstServicesFactory, SecondServiceFactory {
var firstService: MyFirstServiceProtocol {
MyFirstServiceImplementation()
}
var secondService: MySecondServiceProtocol {
MySecondServiceImplementation()
}
}
// now for an example VM
final class MyViewModel {
// declare what service factories (Dependencies) you need for this class.
let services: FirstServicesFactory & SecondServiceFactory
init(services: ServicesContainer = ServicesContainer()) {
self.services = services
}
func doThat() {
// They become available, like so
services.secondService.mySecondMethod()
services.firstService.myFirstMethod()
}
}
// Another example.
final class MySecondViewModel {
let services: FirstServicesFactory
init(services: ServicesContainer = ServicesContainer()) {
self.services = services
}
func doThis() {
services.firstService.myFirstMethod()
}
}
class MockFirstService: MyFirstServiceProtocol {
func myFirstMethod() {
print ("I am the mocked implementation")
}
}
// What about mocks/stubs.
// We can subclass the ServicesContainer here, for a Mock Container, containing mock dependencies.
// You can override as little or as many services as you desire.
class MockServicesContainer: ServicesContainer {
override var firstService: MyFirstServiceProtocol {
return MockFirstService()
}
}
// Putting that theory to test
let thisVM = MyViewModel(services: ServicesContainer())
thisVM.doThat()
let thisVMMocked = MyViewModel(services: MockServicesContainer())
thisVMMocked.doThat()
@AndreiVidrasco
Copy link

Here is a better version. Stop using protocols for everything. They're gonna look in protocol witness table anyway, which is less performant, so why not skip this lookup at all.

import UIKit

// Services conforming to protocols.
// SOLID: Depend on abstractions, not concrete implementations. Dependency Inversion principle

// DECLARE FIRST SERVICE
struct MyFirstServiceProtocol {
    var myFirstMethod: () -> Void
}

extension MyFirstServiceProtocol {
    static var myFirstServiceImplementation = Self(
        myFirstMethod: { print("My First Service") }
    )
}

// DECLARE SECOND SERVICE,
struct MySecondServiceProtocol {
    var mySecondMethod: () -> Void
}

extension MySecondServiceProtocol {
    static var mySecondServiceImplementation = Self(
        mySecondMethod: { print("My second service") }
    )
}

// DECLARE FACTORIES, by protocol, allows us to swap out the implementating class later, when we subclass the `container`.

protocol FirstServicesFactory {
    var firstService: MyFirstServiceProtocol { get }
}

protocol SecondServiceFactory {
    var secondService: MySecondServiceProtocol { get }
}

// Container, each dependency factory must be conformed to here, to provide its implementing class.
struct ServicesContainer: FirstServicesFactory, SecondServiceFactory {
    var firstService: MyFirstServiceProtocol
    var secondService: MySecondServiceProtocol
}

extension ServicesContainer {
    static var defaultServices = ServicesContainer(
        firstService: .myFirstServiceImplementation,
        secondService: .mySecondServiceImplementation
    )
}


// now for an example VM
final class MyViewModel {

    // declare what service factories (Dependencies) you need for this class.
    let services: ServicesContainer
    
    init(services: ServicesContainer = ServicesContainer.defaultServices) {
        self.services = services
    }
    
    func doThat() {
        // They become available, like so
        services.secondService.mySecondMethod()
        services.firstService.myFirstMethod()
    }
}

// Another example.
final class MySecondViewModel {
    
    let services: FirstServicesFactory
    init(services: ServicesContainer = ServicesContainer.defaultServices) {
        self.services = services
    }
    
    func doThis() {
        services.firstService.myFirstMethod()
    }
}

extension MyFirstServiceProtocol {
    static var mockFirstService = Self(
        myFirstMethod: { print ("I am the mocked implementation") }
    )
}

// What about mocks/stubs.
// We can subclass the ServicesContainer here, for a Mock Container, containing mock dependencies.
// You can override as little or as many services as you desire.

extension ServicesContainer {
    static var mockServicesContainer: ServicesContainer {
        var container = ServicesContainer.defaultServices
        container.firstService = .mockFirstService
        return container
    }
}
// Putting that theory to test

let thisVM = MyViewModel(services: .defaultServices)
thisVM.doThat()
let thisVMMocked = MyViewModel(services: .mockServicesContainer)
thisVMMocked.doThat()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment