Passing Data in SwiftUI via the View’s Environment

Passing Data in SwiftUI via the View’s Environment

Learn how to access and share data using @EnvironmentObject, @Published, the ObservableObject protocol, and the .environmentObject(_:) modifier, among multiple views in SwiftUI.

In SwiftUI apps with multiple views, you often need to share data among them. If you've been developing apps for iOS/iPadOS using UIKit you might be wondering how to do this in SwiftUI. Anyway, if you're a beginner, you won't have any problems following this tutorial: no previous knowledge of UIKit is required.

Using SwiftUI's @EnvironmentObject property wrapper, we can share data between many views in an app. By doing so, we can share model data anywhere it's needed while ensuring that our views are refreshed when the model data is updated.

This tutorial is the third of a series that explores 4 different solutions for passing data between views:

Last time we explored how to pass data using a property from a primary to a secondary view using hierarchical navigation. This time we are going to share the same object with multiple views.

Introduction

To demonstrate how to share data with all the views in a project, we are going to create a Jukebox app made of 2 views :

  • a first View, called ListView, which shows the songs in a list.
  • a second View, called GridView, which shows the song in a grid.

Navigation from one screen to another is possible using a TabView (Flat Navigation).

Tab bars - Navigation and search - Components - Human Interface Guidelines - Design - Apple Developer

The 2 views will access to the same object containing all the information needed.

ListView and GridView views are linked via a TabView.

Model and Environment Setup

This is the Song structure we are going to use to build our jukebox:

struct Song: Identifiable {
    var id = UUID()
    var title: String
    var artist: String
    var imageName: String
}

c

Nothing new here!

Let’s now create our Jukebox class. Multiple views will share an instance of this class.

For doing this we need the class to conform to the ObservableObject protocol. (1.1)

ObservableObject allows objects to be observed and listened to by other objects in your app. This is useful when working with data that can change over time and needs to be updated in multiple places in your app.

Apple Developer Documentation

Apple Developer Documentation - ObservableObject - Protocol

Also, the property called songs, which is an array of Song objects needs to be marked with the @Published property wrapper, which means that any changes to this property will be broadcast to any objects that are observing it. (1.2)

Apple Developer Documentation

Apple Developer Documentation - Published - Protocol

// 1.1 Jukebox class conforms to ObservableObject
class Jukebox: ObservableObject {

// 1.2 The property songs is marked with a @Published wrapper
    @Published var songs: [Song] = [
        Song(title: "Inner Light",
             artist: "Elderbrook & Bob Moses",
             imageName: "album1"),
        Song(title: "Feels Like I'm Flying",
             artist: "Yeek",
             imageName: "album2"),
        Song(title: "I Look Good",
             artist: "O.T. Genasis",
             imageName: "album3"),
        Song(
            title: "Become a Mountain",
             artist: "Dan Deacon",
             imageName: "album4")
    ]
    
}

Overall, this code defines a class that manages a list of songs and allows them to be observed by other objects in your app.

Last but not least let’s create the Jukebox instance that is going to be shared with all the views in the project. There are multiple possibilities on where to store this. In the example, we are creating the instance jukebox in the main struct of the App (the one conforming to the App protocol). (1.3)

The last step of our setup is to use the .environmentObject() modifier. .environmentObject() is a modifier that we can use to inject an object of a specific type into the environment of your app. This environment can include things like the app's preferences, settings, or the state of your app.

By using .environmentObject(jukebox), we can make the jukebox object and its songs available to any view in your app, without having to pass it down the view hierarchy manually. (1.4)

@main
struct CreateWithSwift_PassingDataApp: App {
    // 1.3 Create a Jukebox instance
    var jukebox = Jukebox()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
            // 1.4 jukebox is injected into the environment
                .environmentObject(jukebox)
        }
    }
}

Views Setup

It's time to quickly set up the Views we need in our App. As mentioned we are going to need a ListView and a GridView. The user can navigate from one to the other using a Tab bar and the 2 views need access to the same object.

Creating views is not really part of the goal of this tutorial. Feel free to use the code shown below.

Let's start by creating the TabView that connects them:

struct ContentView: View {
    var body: some View {
            TabView {
                ListView()
                    .tabItem {
                        Image(systemName: "list.bullet.rectangle.fill")
                        Text("List")
                    }
                GridView()
                    .tabItem {
                        Image(systemName: "rectangle.split.2x2.fill")
                        Text("Albums")
                    }
            }
        }
}

c

Obviously, the code above will not work until we create ListView and GridView.

The creation of a Jukebox song List will lead to a code more or less similar to the following.

struct ListView: View {

    // Alert: Jukebox new instance!
    var jukebox = Jukebox()
    
    var body: some View {
        NavigationStack {
            List {
                Section {
                    ForEach(jukebox.songs) { song in
                        HStack {
                            Image(song.imageName)
                                .resizable()
                                .aspectRatio(1, contentMode: .fit)
                                .frame(width: 60, height: 60, alignment: .center)
                                .cornerRadius(4.0)
                                .shadow(radius: 4.0)
                            VStack(alignment: .leading) {
                                Text(song.title)
                                    .font(.title3)
                                    .bold()
                                Text(song.artist)
                            }
                        }
                        .padding(.vertical, 8.0)
                    }
                } footer: {
                    HStack {
                        Spacer()
                        Text("Made by CreateWithSwift ♥︎")
                        Spacer()
                    }
                }
            }
            .navigationTitle("Jukebox List")
        }
    }
}

The creation of a Jukebox song Grid will lead to a code more or less similar to the following.

struct GridView: View {
    
    // Alert: Jukebox new instance!
    var jukebox = Jukebox()
    
    let columns = [
        GridItem(.flexible()),
        GridItem(.flexible())
    ]
    
    var body: some View {
        NavigationStack {
            ScrollView {
                LazyVGrid(columns: columns) {
                    ForEach(jukebox.songs) { song in
                        ZStack(alignment: .bottom) {
                            Image(song.imageName)
                                .resizable()
                                .aspectRatio(contentMode: .fill)
                            Rectangle()
                                .fill(
                                    LinearGradient(
                                        colors: [
                                            .clear,
                                            .black.opacity(0.8)
                                        ],
                                        startPoint: .top,
                                        endPoint: .bottom
                                    )
                                )
                            VStack {
                                Text(song.title)
                                    .multilineTextAlignment(.center)
                                    .bold()
                                Text(song.artist)
                                    .font(.caption2)
                                    .multilineTextAlignment(.center)
                            }
                            .foregroundColor(.white)
                            .padding()
                        }
                        .cornerRadius(8.0)
                        .shadow(radius: 4.0)
                    }
                }
                .padding(.horizontal)
                HStack {
                    Spacer()
                    Text("Made by CreateWithSwift ♥︎")
                        .font(.footnote)
                        .foregroundColor(.secondary)
                    Spacer()
                }
            }
            .navigationTitle("Jukebox Grid")
        }
    }
}

It’s working, but this view is using a new instance of the Jukebox, while we want to access the one injected in the app environment.

Access the data injected into the environment

Instead of creating two different Jukebox instances per view, the purpose is to access the same data from both views. To be specific, we want to access from both ListView and GridView the same instance that has been injected into the environment. This requires a refactoring of our code which makes use of an @EnvironmentObject property wrapper. (2.1)

struct ListView: View {
    
    // 2.1 Access the injected data
    @EnvironmentObject var jukebox: Jukebox
    
    var body: some View {
        NavigationStack {
            List {
                Section {
                    ForEach(jukebox.songs) { song in
                        HStack {
                            Image(song.imageName)
                                .resizable()
                                .aspectRatio(1, contentMode: .fit)
                                .frame(width: 60, height: 60, alignment: .center)
                                .cornerRadius(4.0)
                                .shadow(radius: 4.0)
                            VStack(alignment: .leading) {
                                Text(song.title)
                                    .font(.title3)
                                    .bold()
                                Text(song.artist)
                            }
                        }
                        .padding(.vertical, 8.0)
                    }
                } footer: {
                    HStack {
                        Spacer()
                        Text("Made by CreateWithSwift ♥︎")
                        Spacer()
                    }
                }
            }
            .navigationTitle("Jukebox List")
        }
    }
}

This refactoring causes our view preview to stop working. This is because ListView needs now a Jukebox environment object to work. To fix this let's add in the struct ListView_Previews an appropriate .environmentObject() modifier. (2.2)

struct ListView_Previews: PreviewProvider {
    static var previews: some View {
        ListView()
        // 2.2 Inject Jukebox data for the Canvas preview
            .environmentObject(Jukebox())
    }
}

We are going now to repeat everything again for the GridView and GridView_Previews. (2.3) (2.4)

struct GridView: View {
    
    // 2.3. Access the injected data
    @EnvironmentObject var jukebox: Jukebox
    
    let columns = [
        GridItem(.flexible()),
        GridItem(.flexible())
    ]
    
    var body: some View {
        NavigationStack {
            ScrollView {
                LazyVGrid(columns: columns) {
                    ForEach(jukebox.songs) { song in
                        ZStack(alignment: .bottom) {
                            Image(song.imageName)
                                .resizable()
                                .aspectRatio(contentMode: .fill)
                            Rectangle()
                                .fill(
                                    LinearGradient(
                                        colors: [
                                            .clear,
                                            .black.opacity(0.8)
                                        ],
                                        startPoint: .top,
                                        endPoint: .bottom
                                    )
                                )
                            VStack {
                                Text(song.title)
                                    .multilineTextAlignment(.center)
                                    .bold()
                                Text(song.artist)
                                    .font(.caption2)
                                    .multilineTextAlignment(.center)
                            }
                            .foregroundColor(.white)
                            .padding()
                        }
                        .cornerRadius(8.0)
                        .shadow(radius: 4.0)
                    }
                }
                .padding(.horizontal)
                HStack {
                    Spacer()
                    Text("Made by CreateWithSwift ♥︎")
                        .font(.footnote)
                        .foregroundColor(.secondary)
                    Spacer()
                }
            }
            .navigationTitle("Jukebox Grid")
        }
    }
}
struct GridView_Previews: PreviewProvider {
    static var previews: some View {
        GridView()
        // 2.4 Inject Jukebox data for the Canvas preview
            .environmentObject(Jukebox())
    }
}

Last but not least, also the ContentView preview will stop working. This is because both ListView and GridView now need an environment object. Because of this also ContentView_Previews needs .environmentObject() to inject a Jukebox object into the preview environment. (2.5)

struct ContentView: View {
    var body: some View {
        TabView {
            ListView()
                .tabItem {
                    Image(systemName: "list.bullet.rectangle.fill")
                    Text("List")
                }
            GridView()
                .tabItem {
                    Image(systemName: "rectangle.split.2x2.fill")
                    Text("Albums")
                }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
        // 2.5 Inject Jukebox data for the Canvas preview
            .environmentObject(Jukebox())
    }
}

Wrapping up

As a result of following a few simple steps, we have been able to set up two different views, namely a ListView and a GridView, to be able to access the same instance of a class conforming to the ObservableObject protocol.

It is relevant to point out that in the example we gave, we are only accessing data. There is, however, more to it than that: if the injected instance is altered in one of the views, the change is reflected in all the other views as well.

Please keep in mind that this is not the only way to proceed. Check out other articles on CreateWithSwift and stay tuned for the next tutorial.