Jul 20, 2020

Declarative dependency injection in SwiftUI - 1

Dependency Injection (DI) is such a heavy word for a concept we all do all the time unknowingly 😎 It is simply passing "dependencies" of an object via initializer or properties or also via methods.

In software engineering, dependency injection is a technique in which an object receives other objects that it depends on.

SwiftUI declarative syntax made DI a bit non-obvious for beginners, this article (and the part 2 of it) will explain a way of DI in SwiftUI and hopefully you won't find it unusual after reading these.


Check the below code snippet.

struct ContentView: View {
    var body: some View {
        Text("Hello world")
            .font(.largeTitle)
    }
}

There is nothing interesting going on here, we created a Text object and calling a SwiftUI method available on Text to change the font.

  func font(_ font: Font?) -> Text

Let's add a default padding around Text


struct ContentView: View {
    var body: some View {
        Text("Hello world")
            **.padding()**
            .font(.largeTitle)
    }
}

What has changed, well nothing much you might say but a lot. We now have DI in picture 🤯

Note the following points here

  1. We now no longer calling the font method on Text object but on return type of padding() method
  2. padding a method on View returns another View of type SwiftUI.ModifiedContent<SwiftUI.Text, SwiftUI._PaddingLayout> so if you know about opaque return type (if not check Answer 3 here) we can also say return value is some View
  3. Setting font on non-text View like padding type make no sense but still font will get applied magically to Text inside that View.

Let's write above code in a non-declarative way (i.e. Imperative) to understand better what is going on here.

struct ContentViewImp: View {
    var body: some View {
        let text = Text("Hello world")
        let someView1 = text.padding()
      	let fontDependency = Font.largeText
        let someView2 = someView1.font(fontDependency)
        return someView2
    }
} 

The text is a child of someView1 and the font method called on someView1 is actually where you are passing/injecting the dependency Font to someView1. All Views are implicitly provided with many dependencies (called Environment Values) by SwiftUI and all those values make a View's Environment.


The call font on someView1 is actually just changing the default environment value of Font to Font.largeText in its environment. This overridden Font value will then be available (injected) to all child views no matter how deep they are in the view tree. This is the reason text shows up using largeText as it is reading the font from the environment (injected by parent someView1).


In fact the font method on View is just a syntactic sugar for setting font on environment. So the code below is equivalent

struct ContentView: View {
    var body: some View {
        Text("Hello world")
            .padding()
            **.environment(\EnvironmentValues.font, .caption)**
    }
}

The environment declaration is like this

func environment<V>(_ keyPath: WritableKeyPath<EnvironmentValues, V>, _ value: V) -> some View

It takes a keypath WritableKeyPath<EnvironmentValues, V> to change a value in environment of a particular view hierarchy. In this case we are injecting font value to one of the property of EnvironmentValues called font

Now you understand that when we apply font to a container like VStack we are just setting the value of font property of EnvironmentValues of VStack and all child views of VStack get the new Font value implicitly from the environment.

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello world")
        }
        .font(Font.largeTitle)
    }
}

Check EnvironmentValues and you will see that there are many useful properties that get implicitly be available to all subviews via Environment. You can override (inject) any of these for a view hierarchy as we did above for font.

This is how we inject a dependency via environment method. We haven't seen yet how the injected value is being received by the child views. Above examples you can't see as the consumer View is Text View part of SwiftUI framework.

In the second part we will dig deeper into how to receive the injected dependency in the subviews and also how to inject our own custom dependencies to a view hierarchy.

Tagged with: