NEW: Subscribe to Hacking with Swift+ and accelerate your learning! >>

Sharing data across tabs using @EnvironmentObject

Paul Hudson    @twostraws   

SwiftUI’s environment lets us share data in a really beautiful way: any view can send objects into the environment, then any child view can read those objects back out from the environment at a later date. Even better, if one view changes the object all other views automatically get updated – it’s an incredibly smart way to share data in larger applications.

In our app we have a TabView that contains three instances of ProspectView, and we want all three of those to work as different views on the same shared data. This is a great example of where SwiftUI’s environment makes sense: we can define a class that stores one prospect, then place an array of those prospects into the environment so all our views can read it if needed.

So, start by making a new Swift file called Prospect.swift, replacing its Foundation import with SwiftUI, then giving it this code:

class Prospect: Identifiable, Codable {
    let id = UUID()
    var name = "Anonymous"
    var emailAddress = ""
    var isContacted = false
}

Yes, that’s a class rather than a struct. This is intentional, because it allows us to change instances of the class directly and have it be updated in all other views at the same time. Remember, SwiftUI takes care of propagating that change to our views automatically, so there’s no risk of views getting stale.

When it comes to sharing that across multiple views, one of the best things about SwiftUI’s environment is that it uses the same ObservableObject protocol we’ve been using with the @ObservedObject property wrapper. This means we can mark properties that should be announced using the @Published property wrapper – SwiftUI takes care of most of the work for us.

So, add this class in Prospect.swift:

class Prospects: ObservableObject {
    @Published var people: [Prospect]

    init() {
        self.people = []
    }
}

We’ll come back to that later on, not least to make the initializer do more than just create an empty array, but it’s good enough for now.

Now, we want all our ProspectView instances to share a single instance of the Prospects class, so they are all pointing to the same data. If we were writing UIKit code here I’d go into long explanation about how difficult this is to get right and how careful we need to be to ensure all changes get propagated cleanly, but with SwiftUI it requires just three steps.

First, we need to add a property to ContentView that creates and stores a single instance of the Prospects class:

var prospects = Prospects()

Second, we need to post that property into the SwiftUI environment, so that all child views can access it. Because tabs are considered children of the tab view they are inside, if we add it to the environment for the TabView then all our ProspectsView instances will get that object.

So, add this modifier to the TabView in ContentView:

.environmentObject(prospects)

And now we want all instances of ProspectsView to read that object back out of the environment when they are created. This uses a new @EnvironmentObject property wrapper that does all the work of finding the object, attaching it to a property, and keeping it up to date over time. So, the final step is just adding this property to ProspectsView:

@EnvironmentObject var prospects: Prospects

That really is all it takes – I don’t think there’s a way SwiftUI could make this any easier.

Important: When you use @EnvironmentObject you are explicitly telling SwiftUI that your object will exist in the environment by the time the view is created. If it isn’t present, your app will crash immediately – be careful, and treat it like an implicitly unwrapped optional.

Soon we’re going to be adding code to add prospects by scanning QR codes, but for now we’re going to add a navigation bar item that just adds test data and shows it on-screen.

Change the body property of ProspectsView to this:

NavigationView {
    Text("People: \(prospects.people.count)")
        .navigationBarTitle(title)
        .navigationBarItems(trailing: Button(action: {
            let prospect = Prospect()
            prospect.name = "Paul Hudson"
            prospect.emailAddress = "paul@hackingwithswift.com"
            self.prospects.people.append(prospect)
        }) {
            Image(systemName: "qrcode.viewfinder")
            Text("Scan")
        })
}

Now you’ll see a “Scan” button on the first three views of our tab view, and tapping it adds a person to all three simultaneously – you’ll see the count increment no matter which button you tap.

Hacking with Swift is sponsored by Instabug

SPONSORED Are you tired of wasting time debugging your Swift app? Instabug’s SDK is here to help you minimize debugging time by providing you with complete device details, network logs, and reproduction steps with every bug report. All data is attached automatically, and it only takes a line of code to setup. Start your free trial now and get 3 months off exclusively for the Hacking with Swift Community.

Start your free trial!

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.9/5

Link copied to your pasteboard.