Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Why every SwiftUI developer should care about t...

Why every SwiftUI developer should care about the Environment

The SwiftUI environment is one of the backbones of SwiftUI, yet – in my opinion – it is completely underrated. In this talk, I am going to explain what the SwiftUI environment is, and how it powers SwiftUI. You are going to learn how to use the SwiftUI environment to avoid prop drilling, how to use it do make your views configurable, and what are the commonalities between the environment and SwiftUI preferences. In addition to taking a look behind the scenes, we will also look at some practical use cases.

Peter Friese

April 09, 2025
Tweet

More Decks by Peter Friese

Other Decks in Technology

Transcript

  1. Peter Friese, Developer Relations Engineer, Google Why every SwiftUI developer

    should care about the Environment @peterfriese.dev SwiftHeroes, 2025
  2. Peter Friese, Developer Relations Engineer, Google Why every SwiftUI developer

    should care about the Environment @peterfriese.dev SwiftHeroes, 2025 Environment
  3. /ɪnˈvaɪ.r ə .m ə nt/ The air, water, and land

    in or on which people, animals, and plants live.
  4. /ɪnˈvaɪ.r ə .m ə nt/ The conditions that you live

    or work in and the way that they influence how you feel or how effectively you can work. The conditions that you live or work in and the way that they influence how you feel or how effectively you can work.
  5. SwiftUI Environment 1/ React to external conditions (e.g. color scheme)

    2/ Define our own custom conditions 3/ State management 4/ Building better views
  6. Environment values 101 - reading struct SomeView: View { @Environment(\.colorScheme)

    var colorScheme } Use wrapped value in your view body This works because @Environment conforms to DynamicProperty var body: some View { VStack { } } Text("Hello World - dark mode treatment") .padding() .background(colorScheme == .dark ? Color.white : Color.red) .foregroundColor(colorScheme == .dark ? .red : .white) .cornerRadius(10)
  7. Environment values 101 - reading struct SomeView: View { @Environment(\.colorScheme)

    var colorScheme } var body: some View { VStack { } } Text("Hello World - dark mode treatment") .padding() .background(colorScheme == .dark ? Color.white : Color.red) .foregroundColor(colorScheme == .dark ? .red : .white) .cornerRadius(10) Text("Hello World - no dark mode treatment") .padding() .background(Color(.systemRed)) .cornerRadius(10)
  8. Environment values 101 - writing struct SomeView: View { @Environment(\.colorScheme)

    var colorScheme } var body: some View { VStack { } } Button("Toggle case") { isUppercase.toggle() } .environment(\.textCase, isUppercase ? .uppercase : nil) @State private var isUppercase: Bool = false Update environment values
  9. Environment values 101 - writing struct SomeView: View { @Environment(\.colorScheme)

    var colorScheme } var body: some View { VStack { } } Button("Toggle lights") { isDarkModeActive.toggle() } .environment(\.colorScheme, isDarkModeActive ? .light : .dark) @State private var isDarkModeActive: Bool = false This doesn’t work as expected for colorScheme…
  10. Environment values 101 - writing struct SomeView: View { @Environment(\.colorScheme)

    var colorScheme } var body: some View { VStack { } } Button("Toggle lights") { isDarkModeActive.toggle() } .preferredColorScheme(isDarkModeActive ? .light : .dark) @State private var isDarkModeActive: Bool = false Use this instead
  11. Environment values layoutDirection / isEnabled / isLuminanceReduced / redactionReasons /

    controlSize / backgroundMaterial / materialActiveAppearance / symbolRenderingMode / truncationMode / multilineTextAlignment / lineSpacing / allowsTightening / minimumScaleFactor / textCase / lineLimit / backgroundProminence / backgroundStyle / appearsActive / dynamicTypeSize / sizeCategory / headerProminence / allowedDynamicRange / symbolVariants / displayScale / contentTransitionAddsDrawingGroup / backgroundProminence / font / legibilityWeight / imageScale / pixelLength / locale / calendar / timeZone / horizontalSizeClass / verticalSizeClass / accessibilityEnabled / accessibilityDifferentiateWithoutColor / accessibilityReduceTransparency / accessibilityReduceMotion / accessibilityInvertColors / accessibilityShowButtonShapes / accessibilityDimFlashingLights / accessibilityPlayAnimatedImages / colorScheme / colorSchemeContrast
  12. Environment values 101 - custom keys extension EnvironmentValues { @Entry

    var isHighlighted: Bool = false } Use the @Entry macro to define the environment key
  13. Environment values 101 - custom keys extension EnvironmentValues { @Entry

    var isHighlighted: Bool = false } Set the default value
  14. Environment values 101 - custom keys extension EnvironmentValues { @Entry

    var isHighlighted: Bool = false } Access the value in the environment struct HighlightedText: View { let text: String @Environment(\.isHighlighted) private var isHighlighted var body: some View { } }
  15. Environment values 101 - custom keys extension EnvironmentValues { @Entry

    var isHighlighted: Bool = false } struct HighlightedText: View { let text: String @Environment(\.isHighlighted) private var isHighlighted var body: some View { } } Text(text) .background(isHighlighted ? Color.yellow : Color.clear) Use the value in the body
  16. Environment values 101 - custom keys struct HighlightedText: View {

    let text: String @Environment(\.isHighlighted) private var isHighlighted var body: some View { } } Text(text) .background(isHighlighted ? Color.yellow : Color.clear) struct ContentView: View { var body: some View { VStack(spacing: 20) { HighlightedText(text: "This text is not highlighted") HighlightedText(text: "This text is highlighted") .environment(\.isHighlighted, true) } } } Sample usage
  17. Environment values 101 - custom keys extension EnvironmentValues { @Entry

    var isHighlighted: Bool = false } struct ContentView: View { var body: some View { VStack(spacing: 20) { HighlightedText(text: "This text is not highlighted") HighlightedText(text: "This text is highlighted") .environment(\.isHighlighted, true) } } } extension View { func highlighted(_ isOn: Bool = true) -> some View { } } Move this up here
  18. Environment values 101 - custom keys extension EnvironmentValues { @Entry

    var isHighlighted: Bool = false } struct ContentView: View { var body: some View { VStack(spacing: 20) { HighlightedText(text: "This text is not highlighted") HighlightedText(text: "This text is highlighted") } } } extension View { func highlighted(_ isOn: Bool = true) -> some View { } } environment(\.isHighlighted, isOn) .highlighted()
  19. KitchenStore - activeOrders: [Order] - foodItems: [String] DiningAreaStore - tables:

    [Table] struct MainRestaurantView: View { var kitchenStore: KitchenStore } struct DiningAreaView: View { var diningAreaStore: DiningAreaStore var kitchenStore: KitchenStore } struct OrderView: View { var kitchenStore: KitchenStore }
  20. struct MyText: View { var text: String var body: some

    View { } } #Preview { MyText( text: "Hello world" ) }
  21. struct MyText: View { var text: String var font: Font?

    var body: some View { } } #Preview { MyText( text: "Hello world", font: .title ) }
  22. struct MyText: View { var text: String var font: Font?

    var foregroundColor: Color? var body: some View { } } #Preview { MyText( text: "Hello world", font: .title, foregroundColor: .swift, ) }
  23. struct MyText: View { var text: String var font: Font?

    var foregroundColor: Color? var fontWeight: Font.Weight? var body: some View { } } #Preview { MyText( text: "Hello world", font: .title, foregroundColor: .swift, fontWeight: .bold ) }
  24. struct MyText: View { var text: String var font: Font?

    var foregroundColor: Color? var fontWeight: Font.Weight? var textCase: Text.Case? var body: some View { } } #Preview { MyText( text: "Hello world", font: .title, foregroundColor: .swift, fontWeight: .bold, textCase: .uppercase ) }
  25. struct MyText: View { var text: String var font: Font?

    var foregroundColor: Color? var fontWeight: Font.Weight? var textCase: Text.Case? var italic: Bool? var body: some View { } } #Preview { MyText( text: "Hello world", font: .title, foregroundColor: .swift, fontWeight: .bold, textCase: .uppercase, italic: true ) }
  26. struct MyText: View { var text: String var font: Font?

    var foregroundColor: Color? var fontWeight: Font.Weight? var textCase: Text.Case? var italic: Bool? var body: some View { } } #Preview { MyText( text: "Hello world", font: .title, foregroundColor: .swift, fontWeight: .bold, textCase: .uppercase, italic: true ) } Fixed order Adding new parameters: breaking change Incredibly difficult to type at call site Default values: band aid
  27. struct MyText: View { var text: String var body: some

    View { } } struct FontDecorator<Content: View >: View { var content: Content var font: Font? var body: some View { } } struct ForegroundColorDecorator<Content: View >: View { var content: Content var color: Color? var body: some View {
  28. struct FontDecorator<Content: View >: View { var content: Content var

    font: Font? var body: some View { } } struct ForegroundColorDecorator<Content: View >: View { var content: Content var color: Color? var body: some View { } }
  29. #Preview { ItalicDecorator( content: TextCaseDecorator( content: MultilineTextAlignmentDecorator( content: LineLimitDecorator( content:

    FontWeightDecorator( content: ForegroundColorDecorator( content: FontDecorator( content: MyText(text: "Hello world"), font: .title ), color: .swift ), weight: .bold ), lineLimit: 2 ), alignment: .center ), textCase: .uppercase ), italic: true ) } content: content: content: content: content: content: content: #Preview { ItalicDecorator( font: .title ), color: .swift ), weight: .bold ), lineLimit: 2 ), alignment: .center ), textCase: .uppercase ), italic: true ) } TextCaseDecorator( MultilineTextAlignmentDecorator( LineLimitDecorator( FontWeightDecorator( ForegroundColorDecorator( FontDecorator( MyText(text: "Hello world"),
  30. #Preview { ItalicDecorator( font: .title ), color: .swift ), weight:

    .bold ), lineLimit: 2 ), alignment: .center ), textCase: .uppercase ), italic: true ) } TextCaseDecorator( MultilineTextAlignmentDecorator( LineLimitDecorator( FontWeightDecorator( ForegroundColorDecorator( FontDecorator( MyText(text: "Hello world"),
  31. #Preview { ItalicDecorator( font: .title ), color: .swift ), weight:

    .bold ), lineLimit: 2 ), alignment: .center ), textCase: .uppercase ), italic: true ) } TextCaseDecorator( MultilineTextAlignmentDecorator( LineLimitDecorator( FontWeightDecorator( ForegroundColorDecorator( FontDecorator( MyText(text: "Hello world"), Easy to add new decorators No breaking changes Order significant Even harder to read Changes incredibly difficult
  32. struct TextStyle { var font: Font? = nil var foregroundColor:

    Color? = nil } struct MyText: View { var text: String @State private var style = TextStyle() var body: some View { } func myFont(_ font: Font?) -> MyText { style.font = font return self } func myForegroundColor(_ color: Color?) -> MyText { style.foregroundColor = color return self } } Use a private state to hold the view configuration Use decorator functions to update the view configuration
  33. struct TextStyle { var font: Font? = nil var foregroundColor:

    Color? = nil } struct MyText: View { var text: String @State private var style = TextStyle() var body: some View { } func myFont(_ font: Font?) -> MyText { style.font = font return self } func myForegroundColor(_ color: Color?) -> MyText { style.foregroundColor = color return self } } These run during view initialization - state handling hasn’t been fully set up yet!
  34. dump(view) ▿ SwiftUIEnvironmentDemos.MyText - text: "Hello world" ▿ _style: SwiftUI.State<SwiftUIEnvironmentDemos.TextStyle>

    ▿ _value: SwiftUIEnvironmentDemos.TextStyle - font: nil - foregroundColor: nil - fontWeight: nil - lineLimit: nil - textCase: nil - multilineTextAlignment: SwiftUI.TextAlignment.leading - minimumScaleFactor: 1.0 - truncationMode: SwiftUI.Text.TruncationMode.tail - italic: false - _location: nil
  35. struct TextStyle { var font: Font? = nil var foregroundColor:

    Color? = nil } struct MyText: View { var text: String var style = TextStyle() var body: some View { } func myFont(_ font: Font?) -> MyText { var newStyle = style newStyle.font = font return MyText(text: text, style: newStyle) } func myForegroundColor(_ color: Color?) -> MyText { var newStyle = style newStyle.foregroundColor = color return MyText(text: text, style: newStyle) Remove the @State Change the view configuration Return a copy of the view
  36. #Preview { MyText(text: "Hello world") .myFont(.title) .myForegroundColor(.swift) .myFontWeight(.bold) .myLineLimit(2) .myTextCase(.uppercase)

    .myMultilineTextAlignment(.center) .myItalic() } Initialiser will be called for each decorator function
  37. dump(view) ▿ SwiftUIEnvironmentDemos.MyText - text: "Hello world" ▿ style: SwiftUIEnvironmentDemos.TextStyle

    ▿ font: Optional(SwiftUI.Font(provider: SwiftUI.(unknown context at $1d4c016c0).FontBox<SwiftUI.Font.(unknown context at $1d4c14e0c).TextStyleProvider>)) ▿ some: SwiftUI.Font ▿ provider: SwiftUI.(unknown context at $1d4c016c0).FontBox<SwiftUI.Font.(unknown context at $1d4c14e0c).TextStyleProvider> #0 - super: SwiftUI.AnyFontBox ▿ base: SwiftUI.Font.(unknown context at $1d4c14e0c).TextStyleProvider - style: SwiftUI.Font.TextStyle.title - design: nil - weight: nil ▿ foregroundColor: Optional(UIExtendedSRGBColorSpace 0.94 0.303056 0.2162 1) ▿ some: UIExtendedSRGBColorSpace 0.94 0.303056 0.2162 1 ▿ provider: SwiftUI.(unknown context at $1d4c18ab0).ColorBox< __ C.UIColor> #1 - super: SwiftUI.AnyColorBox - super: SwiftUI.AnyShapeStyleBox - base: UIExtendedSRGBColorSpace 0.94 0.303056 0.2162 1 #2 - super: UIColor - super: NSObject ▿ fontWeight: Optional(SwiftUI.Font.Weight(value: 0.4)) ▿ some: SwiftUI.Font.Weight - value: 0.4 ▿ lineLimit: Optional(2) - some: 2 ▿ textCase: Optional(SwiftUI.Text.Case.uppercase) - some: SwiftUI.Text.Case.uppercase - multilineTextAlignment: SwiftUI.TextAlignment.center - minimumScaleFactor: 1.0 - truncationMode: SwiftUI.Text.TruncationMode.tail - italic: true
  38. struct TextStyle { var font: Font? = nil var foregroundColor:

    Color? = nil } extension EnvironmentValues { @Entry var textStyle: TextStyle = TextStyle() } struct MyText: View { var text: String @Environment(\.textStyle) var style var body: some View { } } Define a custom environment key Read the environment value
  39. extension View { func myFont(_ font: Font?) -> some View

    { style.font = font } func myForegroundColor(_ color: Color?) -> some View { style.foregroundColor = color } struct MyText: View { var text: String @Environment(\.textStyle) var style var body: some View { } } Set the style in the decorator function How can we access the environment value here?
  40. extension View { func myFont(_ font: Font?) -> some View

    { style.font = font } func myForegroundColor(_ color: Color?) -> some View { style.foregroundColor = color } struct MyText: View { var text: String @Environment(\.textStyle) var style var body: some View { } } self.transformEnvironment(\.textStyle) { style in } transformEnvironment(\.textStyle) { style in } This gives us convenient access to the environment
  41. struct ActionHandlerDemo: View { @State private var showingPopover = false

    var body: some View { Avatar(image: Image("peter"), title: "Peter Friese", subtitle: "Google", size: 100 ) .onEditProfile { showingPopover.toggle() } .popover(isPresented: $showingPopover, attachmentAnchor: .point(.top), arrowEdge: .bottom) { Text("@peterfriese.dev") .font(.body) .padding() .presentationCompactAdaptation(.popover) } } }
  42. struct Avatar: View { let image: Image let title: String

    let subtitle: String var size: CGFloat = 44 var body: some View { VStack { image .resizable() .aspectRatio(contentMode: .fill) .frame(width: size, height: size) .clipShape(Circle()) Text(title) .font(.headline) Text(subtitle) .font(.subheadline) .foregroundStyle(.secondary) } } }
  43. struct Avatar: View { let image: Image let title: String

    let subtitle: String var size: CGFloat = 44 var body: some View { VStack { image .resizable() .aspectRatio(contentMode: .fill) .frame(width: size, height: size) .clipShape(Circle()) Text(title) .font(.headline) Text(subtitle) .font(.subheadline) extension EnvironmentValues { @Entry var onEditProfile: () - > Void = {} }
  44. struct Avatar: View { let image: Image let title: String

    let subtitle: String var size: CGFloat = 44 var body: some View { VStack { image .resizable() .aspectRatio(contentMode: .fill) .frame(width: size, height: size) .clipShape(Circle()) Text(title) .font(.headline) extension EnvironmentValues { @Entry var onEditProfile: () - > Void = {} } @Environment(\.onEditProfile) private var onEditProfile
  45. struct Avatar: View { let image: Image let title: String

    let subtitle: String var size: CGFloat = 44 var body: some View { VStack { image .resizable() extension EnvironmentValues { @Entry var onEditProfile: () - > Void = {} } @Environment(\.onEditProfile) private var onEditProfile extension View { func onEditProfile(_ action: @escaping () -> Void) -> some View { environment(\.onEditProfile, action) } } Convenience view modifier
  46. struct ActionHandlerDemo: View { @State private var showingPopover = false

    var body: some View { Avatar(image: Image("peter"), title: "Peter Friese", subtitle: "Google", size: 100 ) .onEditProfile { showingPopover.toggle() } .popover(isPresented: $showingPopover, attachmentAnchor: .point(.top), arrowEdge: .bottom) { Text("@peterfriese.dev") .font(.body) .padding() .presentationCompactAdaptation(.popover) } } }
  47. Learn how to build a reusable Avatar view Building Reusable

    SwiftUI Components https://bit.ly/swiftui-components-tutorial
  48. Let ’ s build a button, shall we? Button(action: {

    print("Button tapped!") }) { Text("Home-made button") .font(.headline) .fontWeight(.semibold) .foregroundStyle(Color.white) .padding(.horizontal, 8) .padding(.vertical, 8) .background( RoundedRectangle(cornerRadius: 8) .fill(Color.blue) ) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(Color.blue.opacity(0.8), lineWidth: 1) ) } .shadow(color: .black.opacity(0.1), radius: 4, y: 2)
  49. Let ’ s build a button, shall we? Button("Bordered Prominent")

    { print("Button tapped!") } .buttonStyle(.borderedProminent)
  50. struct ReminderToggleStyle: ToggleStyle { func makeBody(configuration: Configuration) - > some

    View { HStack { Image( systemName: configuration.isOn ? "largecircle.fill.circle" : "circle" ) .resizable() .frame(width: 24, height: 24) .foregroundColor(configuration.isOn ? .accentColor : .gray) .onTapGesture { configuration.isOn.toggle() } configuration.label } } }
  51. struct ReminderToggleStyle: ToggleStyle { func makeBody(configuration: Configuration) - > some

    View { HStack { Image( systemName: configuration.isOn ? "largecircle.fill.circle" : "circle" ) .resizable() .frame(width: 24, height: 24) .foregroundColor(configuration.isOn ? .accentColor : .gray) .onTapGesture { configuration.isOn.toggle() } configuration.label } } }
  52. struct ReminderToggleStyle: ToggleStyle { func makeBody(configuration: Configuration) - > some

    View { HStack { Image( systemName: configuration.isOn ? "largecircle.fill.circle" : "circle" ) .resizable() .frame(width: 24, height: 24) .foregroundColor(configuration.isOn ? .accentColor : .gray) .onTapGesture { configuration.isOn.toggle() } configuration.label } } }
  53. struct ReminderToggleStyle: ToggleStyle { func makeBody(configuration: Configuration) - > some

    View { HStack { Image( systemName: configuration.isOn ? "largecircle.fill.circle" : "circle" ) .resizable() .frame(width: 24, height: 24) .foregroundColor(configuration.isOn ? .accentColor : .gray) .onTapGesture { configuration.isOn.toggle() } configuration.label } } }
  54. struct ReminderToggleStyle: ToggleStyle { func makeBody(configuration: Configuration) - > some

    View { HStack { Image( systemName: configuration.isOn ? "largecircle.fill.circle" : "circle" ) .resizable() .frame(width: 24, height: 24) .foregroundColor(configuration.isOn ? .accentColor : .gray) .onTapGesture { configuration.isOn.toggle() } configuration.label } } }
  55. struct ReminderToggleStyle: ToggleStyle { func makeBody(configuration: Configuration) - > some

    View { HStack { Image( systemName: configuration.isOn ? "largecircle.fill.circle" : "circle" ) .resizable() .frame(width: 24, height: 24) .foregroundColor(configuration.isOn ? .accentColor : .gray) .onTapGesture { configuration.isOn.toggle() } configuration.label } } } extension ToggleStyle where Self = = ReminderToggleStyle { static var reminder: ReminderToggleStyle { ReminderToggleStyle() } }
  56. Content Warning The following slides contain scenes that may be

    extremely traumatizing to some audiences 􀋯
  57. VStack(alignment: .leading) { Toggle("Pizza with Pineapple", isOn: $pizzaWithPineapple) Toggle("Cappuccino after

    12", isOn: $cappuccinoAfter12) Toggle("Cook Pasta al Dente", isOn: $alDente) Toggle("Break Spaghetti", isOn: $breakSpaghetti) Toggle("Oil in Pasta Water", isOn: $oilInPasta) if hasItalianFoodCrime { Image("nogodno") .resizable() .scaledToFit() .frame(maxWidth: .infinity) .padding(.top, 8) } } .toggleStyle(.reminder) I know - it’s a horrible crime, but let’s focus here for a moment, Ok?