Introducing the SwiftUI Environment
This section introduces the SwiftUI Environment, a powerful feature that facilitates data sharing across multiple views. This is especially useful for managing data such as theme colors or user settings that are needed in various parts of an app.
The Challenge: Centralizing Color Management
Consider the budget tracker app, where the text colors for income and expenses are hard-coded in the FinancialEntryRow view. Assume these colors need to be used in other views. You’ll centralize them in the BudgetTrackerApp. This allows the color values to be passed to any view that needs them. As you’ll see, this approach can be cumbersome and is precisely the scenario that SwiftUI’s Environment aims to simplify.
In this section, you’ll update the budget tracker app to centralize the text colors and pass them down to the financial entry rows. Then, you’ll learn how to use Environment to share these values more easily.
Preparing the Starter Project
Open the starter Xcode project. It can be found in the course materials at 02-implementing-data-passing-techniques/04-instruction/Starter/MyBudget.xcodeproj.
Adding Color Properties to BudgetTrackerApp
Before diving into the code, you should know this section’s goal. Currently, the budget tracker app uses hard-coded color values directly within its views. Although functional, this method lacks flexibility. Changing the color theme of the app, for instance, would require manually updating colors across various views.
To enhance the app’s maintainability and scalability, the following steps involve centralizing the management of color properties. By integrating these properties at the app’s top level, color settings can be managed from a single location, streamlining adjustments and maintenance as the app evolves. With that in mind, go ahead and centralize the text colors:
- Navigate to the
BudgetTrackerApp.swiftfile. - Find the
BudgetTrackerAppstruct in the file. - Add two new properties to
BudgetTrackerAppto manage the colors for income and expenses in one place:
let expenseTextColor = Color.red
let incomeTextColor = Color.green
With these properties, the BudgetTrackerApp struct should now include the color properties that can be passed through the view hierarchy:
@main
struct BudgetTrackerApp: App {
// ...
let expenseTextColor = Color.red
let incomeTextColor = Color.green
// ...
}
Propagating the Colors Through the View Hierarchy
Next, pass the centralized color values through the view hierarchy.
- Add
expenseTextColorandincomeTextColorcolor properties toContentView:
struct ContentView: View {
// ...
let expenseTextColor: Color
let incomeTextColor: Color
// ...
}
This allows you to pass the centralized colors into ContentView.
- Pass the centralized colors to the
ContentViewinitialization in theWindowGroupinside thebodyproperty of theBudgetTrackerAppstruct. The compiler will help you find the location by giving an error for missing arguments.
@main
struct BudgetTrackerApp: App {
// ...
var body: some Scene {
WindowGroup {
ContentView(
entries: entries,
expenseTextColor: expenseTextColor,
incomeTextColor: incomeTextColor
)
}
}
// ...
}
Now, you’ve passed the color values from BudgetTrackerApp into ContentView.
- Add
expenseTextColorandincomeTextColorcolor properties to theFinancialEntryRowstruct:
struct FinancialEntryRow: View {
// ...
let expenseTextColor: Color
let incomeTextColor: Color
// ...
}
This enables you to pass the centralized colors into FinancialEntryRow.
- Now, look for the
FinancialEntryRowinitialization in thebodyproperty of theContentView. Once again, the compiler will give you an error at this position to tell you to pass the colors toFinancialEntryRow, so do that:
struct ContentView: View {
// ...
var body: some View {
NavigationView {
List {
Section(header: Text("Entries")) {
ForEach(entries) { entry in
FinancialEntryRow(
entry: entry,
expenseTextColor: expenseTextColor,
incomeTextColor: incomeTextColor
)
}
}
}
// ...
}
}
}
You’ve passed the color values from BudgetTrackerApp into ContentView and finally into FinancialEntryRow.
- You can now use the propagated values from
BudgetTrackerAppinFinancialEntryRow. Replace the hard-coded color values inFinancialEntryRowwith the passed colors:
struct FinancialEntryRow: View {
// ...
var body: some View {
HStack {
// ...
.foregroundColor(entry.isExpense ?
expenseTextColor : incomeTextColor)
}
}
}
- To try out how you can reuse the centralized values, apply the expense and income text colors to the Expense/Income
Textview:
struct FinancialEntryRow: View {
// ...
var body: some View {
HStack {
Text(entry.isExpense ? "Expense" : "Income")
.foregroundColor(entry.isExpense ?
expenseTextColor : incomeTextColor)
// ...
}
}
}
Imagine having to pass these colors into every view in a large codebase! The good news is there’s a better way, using SwiftUI’s Environment. Before diving into Environment, you’ll run the code in the following section.
Testing the Changes
Build and run the app to see the modifications.
The text colors for Income and Expense should now be determined by the properties in the BudgetTrackerApp struct. To validate that you can change the colors centrally from BudgetTrackerApp, change the expense color from Color.red to Color.orange:
@main
struct BudgetTrackerApp: App {
// ...
let expenseTextColor = Color.orange
// ...
}
Rerun the app to see how the color scheme can now be changed from a single place!
Although this method works, it’s not the most efficient, especially if these colors must be referenced in multiple places throughout the app. This is where the power of the SwiftUI Environment comes into play.
Understanding the SwiftUI Environment
The SwiftUI Environment allows for the central storage and access of shared data. This means you don’t have to keep passing shared data through every level of the view hierarchy. This makes accessing shared data much neater and easier to handle. Data is stored and accessed in the Environment as Environment Values.
SwiftUI comes with many predefined Environment values you can use in your apps. Additionally, you can define custom Environment values.
You’ll implement custom Environment values in the next video demo. Before going to the demo, you’ll look at how custom Environment values are defined. For the rest of this section, don’t worry about placing the code samples in Xcode. You’ll do that in the demo.
Defining Environment Keys
To add custom Environment values, you first define Environment Keys for the data to be shared. These keys act as identifiers to share the data across the app. For the text colors, two Environment keys can be created: one for the expense text color and one for the income text color:
struct ExpenseTextColorKey: EnvironmentKey {
static let defaultValue: Color = .red
}
struct IncomeTextColorKey: EnvironmentKey {
static let defaultValue: Color = .green
}
ExpenseTextColorKey and IncomeTextColorKey are defined with default values of red and green, respectively.
Extending Environment Values
After defining the Environment keys, the next step is to make these keys accessible as properties within SwiftUI’s Environment. This is achieved by extending the EnvironmentValues struct. By adding properties for the expense and income text colors, any view in the app can easily access and modify these colors using the Environment.
Here’s how you implement the extension:
extension EnvironmentValues {
var expenseTextColor: Color {
get { self[ExpenseTextColorKey.self] }
set { self[ExpenseTextColorKey.self] = newValue }
}
var incomeTextColor: Color {
get { self[IncomeTextColorKey.self] }
set { self[IncomeTextColorKey.self] = newValue }
}
}
This extension adds expenseTextColor and incomeTextColor properties to the EnvironmentValues. The get and set blocks in each property manage the retrieval and updating of the color values in the Environment, using the defined keys. This design allows for straightforward access to these colors from anywhere in the view hierarchy.
Using Environment Values in Views
Once the Environment keys have been defined and EnvironmentValues has been extended, the default values can be accessed using the @Environment property wrapper. For example, in FinancialEntryRow, the text colors can be accessed from the Environment instead of being hard-coded:
struct FinancialEntryRow: View {
let entry: FinancialEntry
@Environment(\.expenseTextColor)
var expenseTextColor
@Environment(\.incomeTextColor)
var incomeTextColor
var body: some View {
HStack {
Text(entry.isExpense ? "Expense" : "Income")
.foregroundColor(entry.isExpense ? expenseTextColor : incomeTextColor)
Spacer()
Text("$\(entry.amount, specifier: "%.2f")")
.foregroundColor(entry.isExpense ? expenseTextColor : incomeTextColor)
}
}
}
Here, the @Environment property wrappers are used to access expenseTextColor and incomeTextColor from the Environment. Then, the properties are used in the view’s body to determine the text color of the text views. This makes it easy to access the centralized color values without having to pass them through the view hierarchy.
Overriding Environment Values
Additionally, you can override default values using the Environment view modifier.
For example, in BudgetTrackerApp, you can override the default red expense value from the Environment key setup with a new global value:
WindowGroup {
ContentView(entries: entries)
.environment(\.expenseTextColor, Color.orange)
}
Using the .environment modifier overrides the default expenseTextColor from red to orange for all views in this ContentView, which means for the entire app.
Conclusion
In conclusion, the SwiftUI Environment offers a powerful solution for managing shared data in SwiftUI apps. Here’s a summary of the key benefits:
- Centralized Data Management: Introduced the SwiftUI Environment for centralized management of shared data.
- Simplified Data Access: Utilized Environment values for convenient access to shared data across the view hierarchy.
- Enhanced Maintainability: Improved app maintainability and scalability by streamlining data access and modification.
- Flexibility with Overrides: Demonstrated the flexibility of Environment values through overrides for customization.
In the next section, you’ll put these concepts into action, implementing these Environment values in the budget tracker app.