Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Hey, I’m Peter

Slide 4

Slide 4 text

Hey, I’m Peter

Slide 5

Slide 5 text

Environment

Slide 6

Slide 6 text

/ɪnˈvaɪ.r ə .m ə nt/

Slide 7

Slide 7 text

/ɪnˈvaɪ.r ə .m ə nt/ The air, water, and land in or on which people, animals, and plants live.

Slide 8

Slide 8 text

/ɪ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.

Slide 9

Slide 9 text

Why?

Slide 10

Slide 10 text

Why? SwiftUI Environment

Slide 11

Slide 11 text

SwiftUI Environment 1/ React to external conditions (e.g. color scheme) 2/ Define our own custom conditions 3/ State management 4/ Build better views

Slide 12

Slide 12 text

1/ React to external conditions (e.g. color scheme)

Slide 13

Slide 13 text

Environment values

Slide 14

Slide 14 text

Environment values

Slide 15

Slide 15 text

Environment values 101 - reading struct SomeView: View { @Environment(\.colorScheme) var colorScheme } Read value using key path

Slide 16

Slide 16 text

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)

Slide 17

Slide 17 text

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)

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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…

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

1/ React to external conditions (e.g. color scheme)

Slide 24

Slide 24 text

SwiftUI Environment 1/ React to external conditions (e.g. color scheme) 2/ Define our own custom conditions 3/ State management 4/ Build better views

Slide 25

Slide 25 text

2/ Define our own custom conditions

Slide 26

Slide 26 text

Environment values

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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 { } }

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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()

Slide 34

Slide 34 text

2/ Define our own custom conditions

Slide 35

Slide 35 text

SwiftUI Environment 1/ React to external conditions (e.g. color scheme) 2/ Define our own custom conditions 3/ State management 4/ Build better views

Slide 36

Slide 36 text

3/ State management

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

KitchenStore - activeOrders: [Order] - foodItems: [String] DiningAreaStore - tables: [Table]

Slide 39

Slide 39 text

@Observable class KitchenStore { var activeOrders: [Order] = [] var foodItems: [String] = [ "Pizza Margherita", "Pasta Carbonara", "Risotto ai Funghi", "Lasagna alla Bolognese", "Bruschetta al Pomodoro", "Tiramisu" ] } @Observable public class DiningAreaStore { public var tables: [Table] }

Slide 40

Slide 40 text

struct MainRestaurantView: View { var kitchenStore: KitchenStore var diningAreaStore: DiningAreaStore var body: some View { Section("Tables") { NavigationLink { DiningAreaView(kitchenStore: kitchenStore) } } } } struct OrderView: View { var kitchenStore: KitchenStore var body: some View { List { Section("Menu Selection") { } } } } ⚠ Prop Drilling ⚠ struct DiningAreaView: View { var kitchenStore: KitchenStore var body: some View { Section("Table Information") { NavigationLink { OrderView(kitchenStore: kitchenStore) } } } }

Slide 41

Slide 41 text

Environment KitchenStore - activeOrders: [Order] - foodItems: [String] DiningAreaStore - tables: [Table]

Slide 42

Slide 42 text

struct RootView: View { @State private var kitchenStore = KitchenStore() @State private var diningAreaStore = DiningAreaStore() var body: some View { NavigationStack { EnvMainRestaurantView() } .environment(kitchenStore) .environment(diningAreaStore) } } struct MainRestaurantView: View { @Environment(DiningAreaStore.self) private var diningAreaStore @Environment(KitchenStore.self) private var kitchenStore var body: some View { } } Environment

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

Re-implementing Text

Slide 45

Slide 45 text

Re-implementing Text 1: Using the Initialiser

Slide 46

Slide 46 text

struct MyText: View { var text: String var body: some View { } } #Preview { MyText( text: "Hello world" ) }

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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, ) }

Slide 49

Slide 49 text

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 ) }

Slide 50

Slide 50 text

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 ) }

Slide 51

Slide 51 text

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 ) }

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Re-implementing Text 2: Decorators

Slide 54

Slide 54 text

struct MyText: View { var text: String var body: some View { } }

Slide 55

Slide 55 text

struct MyText: View { var text: String var body: some View { } } struct FontDecorator: View { var content: Content var font: Font? var body: some View { } } struct ForegroundColorDecorator: View { var content: Content var color: Color? var body: some View {

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

#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"),

Slide 58

Slide 58 text

#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"),

Slide 59

Slide 59 text

#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

Slide 60

Slide 60 text

Re-implementing Text 3: Decorator functions

Slide 61

Slide 61 text

Attempt 1: Using @State for View configuration

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

#Preview { MyText(text: "Hello world") .myFont(.title) .myForegroundColor(.swift) .myFontWeight(.bold) .myLineLimit(2) .myTextCase(.uppercase) .myMultilineTextAlignment(.center) .myItalic() } 🤔

Slide 64

Slide 64 text

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!

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

dump(view) ▿ SwiftUIEnvironmentDemos.MyText - text: "Hello world" ▿ _style: SwiftUI.State ▿ _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

Slide 67

Slide 67 text

Attempt 2: Making copies, lots of copies

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

#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

Slide 70

Slide 70 text

dump(view) ▿ SwiftUIEnvironmentDemos.MyText - text: "Hello world" ▿ style: SwiftUIEnvironmentDemos.TextStyle ▿ font: Optional(SwiftUI.Font(provider: SwiftUI.(unknown context at $1d4c016c0).FontBox)) ▿ some: SwiftUI.Font ▿ provider: SwiftUI.(unknown context at $1d4c016c0).FontBox #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

Slide 71

Slide 71 text

No content

Slide 72

Slide 72 text

Attempt 3: Using the environment

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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?

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

3/ State management

Slide 77

Slide 77 text

SwiftUI Environment 1/ React to external conditions (e.g. color scheme) 2/ Define our own custom conditions 3/ State management 4/ Build better views

Slide 78

Slide 78 text

4/ Build better views

Slide 79

Slide 79 text

Advanced Use Cases Action Handlers

Slide 80

Slide 80 text

SwiftUI Styling Action Handlers

Slide 81

Slide 81 text

Configuring Features SwiftUI Styling Action Handlers

Slide 82

Slide 82 text

Action Handlers Configuring Features SwiftUI Styling

Slide 83

Slide 83 text

Action Handlers

Slide 84

Slide 84 text

No content

Slide 85

Slide 85 text

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) } } }

Slide 86

Slide 86 text

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) } } }

Slide 87

Slide 87 text

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 = {} }

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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) } } }

Slide 91

Slide 91 text

Learn how to build a reusable Avatar view Building Reusable SwiftUI Components https://bit.ly/swiftui-components-tutorial

Slide 92

Slide 92 text

Action Handlers Configuring Features SwiftUI Styling

Slide 93

Slide 93 text

SwiftUI Styling Configuring Features Action Handlers

Slide 94

Slide 94 text

SwiftUI Styling

Slide 95

Slide 95 text

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)

Slide 96

Slide 96 text

Let ’ s build a button, shall we? Button("Bordered Prominent") { print("Button tapped!") } .buttonStyle(.borderedProminent)

Slide 97

Slide 97 text

What does it take to build your own style?

Slide 98

Slide 98 text

Content Warning The following slides contain scenes that may be traumatizing to some audiences 􀋯

Slide 99

Slide 99 text

👀

Slide 100

Slide 100 text

.toggleStyle()

Slide 101

Slide 101 text

Toggle("Adding ketchup to ajvar", isOn: $ketchupInAjvar) .toggleStyle(.reminder) I know - it’s a horrible crime, but let’s focus here for a moment, Ok?

Slide 102

Slide 102 text

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 } } }

Slide 103

Slide 103 text

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 } } }

Slide 104

Slide 104 text

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 } } }

Slide 105

Slide 105 text

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 } } }

Slide 106

Slide 106 text

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 } } }

Slide 107

Slide 107 text

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() } }

Slide 108

Slide 108 text

.toggleStyle(.reminder)

Slide 109

Slide 109 text

Content Warning The following slides contain scenes that may be extremely traumatizing to some audiences 􀋯

Slide 110

Slide 110 text

VStack(alignment: .leading) { Toggle("Adding ketchup to ajvar", isOn: $ketchupInAjvar) Toggle("Calling tavče gravče 'just beans'", isOn: $justBeans) Toggle("Drinking rakija with ice", isOn: $rakijaWithIce) Toggle("Making burek with pineapple", isOn: $burekWithPineapple) if hasMacedonianFoodCrime { 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?

Slide 111

Slide 111 text

SwiftUI Styling Configuring Features Action Handlers

Slide 112

Slide 112 text

Action Handlers Configuring Features SwiftUI Styling

Slide 113

Slide 113 text

Configuring Features

Slide 114

Slide 114 text

No content

Slide 115

Slide 115 text

enum AuthenticationProvider { case email case google case apple } extension EnvironmentValues { @Entry var authenticationProviders: [AuthenticationProvider] = [] } extension View { func authenticationProviders(_ providers: [AuthenticationProvider]) -> some View { environment(\.authenticationProviders, providers) } }

Slide 116

Slide 116 text

public struct AuthenticationScreen: View { @Environment(AuthenticationService.self) private var authenticationService @Environment(\.authenticationProviders) private var authenticationProviders public var body: some View { VStack { if authenticationProviders.contains(.email) { EmailPasswordAuthenticationView() .environment(\.authenticationFlow, flow) } VStack(spacing: 16) { if authenticationProviders.contains(.apple) { AuthenticateWithAppleButton(flow = = .login ? .signIn : .signUp) } if authenticationProviders.contains(.google) { AuthenticateWithGoogleButton(flow == .login ? .signIn : .signUp) } } } } }

Slide 117

Slide 117 text

Button("Sign in") { if !authenticationService.isAuthenticated { presentingAuthenticationDialog.toggle() } } .sheet(isPresented: $presentingAuthenticationDialog) { AuthenticationScreen() .environment(authenticationService) .authenticationProviders([ .email, .apple ]) }

Slide 118

Slide 118 text

Button("Sign in") { if !authenticationService.isAuthenticated { presentingAuthenticationDialog.toggle() } } .sheet(isPresented: $presentingAuthenticationDialog) { AuthenticationScreen() .environment(authenticationService) .authenticationProviders([ .email, .apple, .google ]) }

Slide 119

Slide 119 text

4/ Build better views

Slide 120

Slide 120 text

SwiftUI Environment 1/ React to external conditions (e.g. color scheme) 2/ Define our own custom conditions 3/ State management 4/ Build better views

Slide 121

Slide 121 text

Resources Source Code YouTube series https://bit.ly/3n99fis https://bit.ly/building-swiftui-components Composable Styles (Moving Parts) https://bit.ly/47w7wJY

Slide 122

Slide 122 text

Resources Creating a Styleable Toggle Slides for this talk https://bit.ly/3siupAk https://speakerdeck.com/peterfriese

Slide 123

Slide 123 text

FREE all-in-one bundle! Content bundle https://bit.ly/swiftui-content-bundle