Skip to content

Commit

Permalink
Notes on dependency injection in swift
Browse files Browse the repository at this point in the history
  • Loading branch information
younata committed Mar 6, 2024
1 parent 8b93b86 commit 1ba2aaf
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .spelling
Expand Up @@ -557,6 +557,12 @@ Sunpower
flox
Cyanoacry's
80g
m12x1.25
14s
52v
staffeng.com
Swinject
ORMs
- src/astronomy/astrophotography/index.md
Cafuego's
astrodslr
Expand Down
2 changes: 1 addition & 1 deletion src/food/recipes/pinto_beans.md
Expand Up @@ -4,7 +4,7 @@ Dried pinto beans are very nutritious and are significantly cheaper than canned

3 parts water to 1 part beans. (e.g. 3 cups water for every cup of beans).

Combine in a slow cooker. Heat on low until the water level is where the beans are (4-8 hours depending on how powerful "low" is on your slow cooker).
Combine in a slow cooker. Heat on low for 6-8 hours. Replenish the water if it gets too low.

Strain the beans into a container. It keeps reasonably well in the fridge.

Expand Down
82 changes: 82 additions & 0 deletions src/programming/swift/dependency_injection.md
@@ -0,0 +1,82 @@
# Dependency Injection

The 5th letter of SOLID refers to the usage of Dependency Injection. Or providing an instance with all of the things it needs to do its job - instead of having that instance having to reach out to other things for methods/functions.

DI is great. It's what enables you to follow the rest of SOLID. It greatly eases tests (instead of using a real thing, you can use a test double), and it cleans up your code.

This page is meant to be an examination of DI strategies in Swift that I'm aware of.

I'm familiar with and have used [Blindside](https://github.com/jbsf/blindside) (which is Objective-C, but the same principles apply), [Swinject](https://github.com/Swinject/Swinject), [Swift-dependencies](https://github.com/pointfreeco/swift-dependencies), as well as various proprietary approaches.

## Liskov Substitution, but for Packages

One App architecture opinion I strongly hold is that no third-party dependencies should be made integral to your app. For example, if you build your app on Core Data, and weave it throughout the app, you will have a very hard time if you ever decide to switch ORMs (even if it's to, say, Swift Data, which uses Core Data under the hood), let alone databases. This is essentially Liskov Substitution, but applied to Packages.

This same principle applies to my opinions of DI frameworks. In my opinion, the best way to do DI is to have functions or initializers take in protocols for what they use, and elsewhere, use a DI framework to create instances use those initializers.

For example:

```swift
protocol AProtocol { ... }
protocol BProtocol { ... }

struct AStruct: AProtocol {
let b: BProtocol
init(b: BProtocol) { self.b = b }
}

// elsewhere

final class DependencyProvider {
func a() -> AProtocol {
AStruct(b: b())
}

func b() -> BProtocol { ... }
}
```

In my own projects, the `DependencyProvider` is actually a custom async-aware fork of Swinject.

## Handling Makers

Sometimes, an instance needs to be able to make another instance. For example, needing to make the view model for the next view in a hierarchy. These should be injected as closures that take any dynamic dependencies. In other words, the Factory pattern.

For example:

```swift
protocol ViewModel {
associatedType View: SwiftUI.View
var view: Self.View { get }
}

protocol AViewModelProtocol {
var bViewModelFactory: (String) -> any ViewModel
}

final class AViewModel: ViewModel, AViewModelProtocol {
let bViewModelFactory: (String) -> any ViewModel

init(bViewModelFactory: @escaping (String) -> any ViewModel) {
self.bViewModelFactory = bViewModelFactory
}

var view: some View { AView(viewModel: self) }
}

struct AView: View {
@State var viewModel: some ViewModel
var body: some View {
VStack {
NavigationLink("Hello") {
viewModel.bViewModelFactory("hello").view
}
NavigationLink("Goodbye") {
viewModel.bViewModelFactory("goodbye").view
}
}
}
}
```

You might think it might be worth it to wrap this in a type. But, really, you're only saving a small amount of characters, in exchange for the boilerplate of creating yet another type to essentially shuffle a closure around. If Swift had Objective-C's frankly horrific closure syntax, then creating a separate type to wrap that would be worth it. But thankfully, Swift has relatively sane closure syntax.

0 comments on commit 1ba2aaf

Please sign in to comment.