UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Sharing data across tabs using @Environment

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 ProspectsView, 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 {
    var 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 @StateObject 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:

@MainActor 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 ProspectsView 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:

@StateObject 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)

Important: Make sure you add the same modifier to the preview struct for ProspectsView, so your previews continue working.

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)")
        .navigationTitle(title)
        .toolbar {
            Button {
                let prospect = Prospect()
                prospect.name = "Paul Hudson"
                prospect.emailAddress = "paul@hackingwithswift.com"
                prospects.people.append(prospect)
            } label: {
                Label("Scan", systemImage: "qrcode.viewfinder")
            }
        }
}

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.

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

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

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI 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 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 Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.