Sharing data through SwiftUI Views

Passing data through SwiftUI Views

The aim of this post is just to explain three different mechanisms for passing data through SwiftUI views  presenting its pros and cons.

We have create a blank Swift UI app with three simple linked views with a navigation bar.

First and Third view share common data, but the one that is in the middle is a transition view that does not have to deal with such data.

☝️🚨Before starting needless to say that minimum iOS version for running SwiftUI is iOS13.🚨

Share via @State-@Binding

This is the first mechanism that is presented with SwitUI. Is very useful when you want to decompose a view in reusable subviews and parent and subviews share common data.

This is a simple example:

Just declare the object to share with @State wrapper in the parent view:

import SwiftUI

struct ContentView: View {
    @State private var currentPoints: Int = 0
    var body: some View {
        NavigationView {
            VStack {
                // A button that writes to the environment settings
                Button("Points @State-@Binding: \(currentPoints)") {
                    currentPoints += 1
                }
                NavigationLink(destination: IntermediateView(currentPoints: <strong>$currentPoints</strong>)) {
                    Text("Show Detail View")
                }
            }
        }
    }
}

Now you have to bind shared data through all its subviews via @Binding wrapper. In that case, the second view does not have to deal with it, but has to handle because the third view is using it.

import SwiftUI

struct IntermediateView: View {
    @Binding var currentPoints: Int
    var body: some View {
        NavigationLink(destination: PointsView(currentPoints: <strong>$currentPoints</strong>)) {
            Text("Show Detail View (again)")
        }
    }
}

And finally third view:

import SwiftUI

struct PointsView: View {
    @Binding var currentPoints: Int
    var body: some View {

        Button("Decrease  @State-@Binding \(currentPoints)") {
            currentPoints -= 1
        }
    }
}

👍 Advantage is clear, you can decompose a view in smaller and simple views and finnaly pass view model data through them.

⚠️ But when app starts to grow and starts appearing views in the middle that does not have to do with that data, as the example presented, this is totally unsuitable.

⚠️Another issue that I have found whilst I was implementing the example is that when value was changed in the third view, this was popped 🔙.

Share via environment

This is the other mechanism also for sharing data between views, it complements @State-Binding. When I say complements It means that when @State-Binding wins, Environment loses (and in the other way around).

Via environment we can share a primitive variable or a composite type, a class only, 1️⃣ further on you will see why not an struct.

Sharing a variable

First of all you need to create an Environment key that will hold the shared variable itself.

struct CurrentPointsKey: EnvironmentKey {
    static var defaultValue: Binding<Int> = .constant(0)
}

extension EnvironmentValues {
    var currentPointsValue: Binding<Int> {
        get { self[CurrentPointsKey.self] }
        set { self[CurrentPointsKey.self] = newValue }
    }
}

Just declare variable primitive to share by using @State wrapper, and inject value using .environment modifier.

struct ContentView: View {
    @State private var currentPoints: Int = 0
    <strong>@State private var currentPointsEnv: Int = 0</strong>
    var body: some View {
        NavigationView {
            VStack {
                // A button that writes to the environment settings
                Button("Points @State-@Binding: \(currentPoints)") {
                    currentPoints += 1
                }
                Button("Points @Environment: \(currentPointsEnv)") {
                    currentPointsEnv += 1
                }

                NavigationLink(destination: IntermediateView(currentPoints: $currentPoints)) {
                    Text("Show Detail View")
                }
            }
        }
        <strong>.environment(\.currentPointsValue, $currentPointsEnv)</strong>
    }
}

And finally recover value from the third subview by using @Environment wrapper:

struct PointsView: View {
    <strong>@Environment(\.currentPointsValue) var currentPointsEnv</strong>
    @Binding var currentPoints: Int
    var body: some View {

        Button("Decrease  @State-@Binding \(currentPoints)") {
            currentPoints -= 1
        }
        
        Button("Decrease @Environment: \(currentPointsEnv.wrappedValue)") {
            <strong>currentPointsEnv.wrappedValue -= 1</strong>
        }
    }
}

Shareing a class

Instead of sharing a basic primitive type, perhaps we are more interested in sharing class. In that way when its state changes all views that consumes its data will get refreshed. 🙅‍♀️ Well,  this can not be taken litarally, you only track state of attributes marked with @Published attribute.

First of all, lets create the class that we want to share:

import Combine

class AppSettings: ObservableObject {
    @Published var points = 0
}

1️⃣ The aggregated type must be a class because it has to inherit from ObserbableObject.

In the parent view is declared a class instance with the @StateObject wrapper, and is injected to child views via .environmentObject

struct ContentView: View {
    <strong>@StateObject var settings = AppSettings()</strong>
    @State private var currentPoints: Int = 0
    @State private var currentPointsEnv: Int = 0
    var body: some View {
        NavigationView {
            VStack {
                Button("Points @State-@Binding: \(currentPoints)") {
                    currentPoints += 1
                }
                Button("Points @EnvironmentObject: \(settings.points)") {
                    <strong>settings.points += 1</strong>
                }
                Button("Points @Environment: \(currentPointsEnv)") {
                    currentPointsEnv += 1
                }

                NavigationLink(destination: IntermediateView(currentPoints: $currentPoints)) {
                    Text("Show Detail View")
                }
            }
        }
        <strong>.environmentObject(settings)</strong>
        .environment(\.currentPointsValue, $currentPointsEnv)
    }
}

In any child view we can recover the class in the following way:

struct PointsView: View {
    <strong>@EnvironmentObject var settings: AppSettings</strong>
    @Environment(\.currentPointsValue) var currentPointsEnv
    @Binding var currentPoints: Int
    var body: some View {

        Button("Decrease  @State-@Binding \(currentPoints)") {
            currentPoints -= 1
        }
        
        Button("Decrease @EnvironmentObject \(settings.points)") {
            <strong>settings.points -= 1</strong>
        }

        Button("Decrease @Environment: \(currentPointsEnv.wrappedValue)") {
            currentPointsEnv.wrappedValue -= 1
        }
    }
}

👍  Advantage is clear, no need to pass same data through its subviews, cleaner code. As you can figure it out, this data is stored in a common repository, and this has sense when you want to access to Managers, I mean, code components that handle DDBB, Notifications, Deeplinks, Authentications,…. .⚠️   But not in case that you wanted to decompose a single view in more simple ones and pass data through them. For that was designed @State-@BindingMechanism.

Conclusion

In that post I have tried to present, in a very simple way, three mechanisms for sharing data between views with SwiftUI. You can fetch the example code from here 🔗.

References:

State and Data Flow: [https://developer.apple.com/documentation/swiftui/state-and-data-flow]

EnvironmentObject: https://developer.apple.com/documentation/swiftui/environmentobject

Environment: https://developer.apple.com/documentation/swiftui/environment