How to Apply Global Styling in SwiftUI Apps

How to Apply Global Styling in SwiftUI Apps
Although not strictly required, it's good to have a basic understanding of creating custom Environment Keys in SwiftUI. If you haven't already read it, I strongly recommend you to go through my previous blog article about creating a custom environment key-value pair in SwiftUI so that you will get more context from the post I am about to write

Introduction

In today's post, I am going to talk about how to apply the global style to the app with one click with the help of EnvironmentKey protocol. Often time we need to apply different styles to the app which may include fonts, colors, and icons that cater to specific users or app environments. Before SwiftUI, we had to maintain a global state and manually apply styles to the app.

Now with custom environment keys created using EnvironmentKey protocol, we can instantly change the whole app layout by updating styles associated with the current environment and automatically propagating that change to everywhere in the app that is using these styles to design the layout.

At the end of this tutorial, you will have a better understanding of,

  1. Creating a custom environment key
  2. Creating and updating custom styles on the fly
  3. Updating custom environment properties

Creating Custom Environment Key

In the first step, we will create a struct that represents a custom environment key. We will add required properties to it and update it on the fly as per the requirement - In this case, we will add a computed property called styling that represents the current app style.

First, we will create a Styling struct that represents app styles such as spacing, colors, icons, etc.


import Combine
import SwiftUI

struct Styling: Identifiable, Hashable {
    let id = UUID()

    var spacing: Spacings
    var colors: Colors
    var icons: Icons
    var fonts: Fonts
    var title: String

    init(spacing: Spacings = Spacings(), colors: Colors = Colors(), icons: Icons = Icons(), fonts: Fonts = Fonts()) {
        self.spacing = spacing
        self.colors = colors
        self.icons = icons
        self.fonts = fonts
        self.title = "Marigold"
    }
}

This struct represents a custom style applied from the app. We will add four core styles components as follow,

  1. Marigold
  2. Sunflower
  3. Brinjal
  4. default style (Same as Marigold)

extension Styling {
    static var `default` = Styling()

    static var marigold = Styling()

    static var sunflower: Styling = {
        var styling = Styling()
        styling.colors = Colors()
        styling.colors.background = .green
        styling.colors.subText = styling.colors.yellow
        styling.colors.text = .red
        styling.icons.homePageIconName = "paperplane.fill"
        styling.icons.stylePageIconName = "tray.2"
        styling.title = "Sunflower"
        return styling
    }()

    static var brinjal: Styling = {
        var styling = Styling()
        styling.colors = Colors()
        styling.colors.text = .purple
        styling.colors.subText = styling.colors.purple
        styling.colors.background = styling.colors.yellow
        styling.icons.homePageIconName = "bookmark.fill"
        styling.icons.stylePageIconName = "person.circle"
        styling.title = "Brinjal"
        return styling
    }()
}


Next, we will create a custom environment key conforming to EnvironmentKey protocol. To satisfy the protocol conformance, the struct needs to implement the following property,

static var defaultValue: Self.Value { get }

We want to use the default style when no style is assigned. So we will use the default Style initializer to create a style struct with default style values and assign it to defaultValue on the custom environment key.


struct StylingEnvironmentKey: EnvironmentKey {
    static var defaultValue: Styling = Styling()
}

We will add a computed property styling on the built-in type EnvironmentValues which can be written to and read from using getter and setter. This will be used to read and write custom styles to the newly created environment key.


extension EnvironmentValues {
    var styling: Styling {
        get { self[StylingEnvironmentKey.self] }
        set { self[StylingEnvironmentKey.self] = newValue }
    }
}

When you create a new view, you may need to assign custom styles to the current environment variable before presenting it to the user. If you don't specify anything, it will use the default styles. In order to facilitate it, we will add a utility function by creating an extension on the View type.


extension View {
    func setStyling(_ value: Styling) -> some View {
        environment(\.styling, value)
    }
}


Using this extension, you can assign any styles you want to the custom view using the following one-liner,


HomeView()
    .setStyling(.brinjal)
    
    

Creating Structs for custom Colors, Spacings, Fonts, and Icons

Before we jump into applying styles, we need to create custom structs for our styles. These include the following components,

  1. Spacings
  2. Colors
  3. Fonts
  4. Icons

We will also add a couple of utilities to convert SwiftUI Color type into UIKit UIColor type


//Spacings.swift

import Foundation
import SwiftUI

struct Spacings: Hashable {

    static let _8: CGFloat = 8
    static let _12: CGFloat = 12
    static let _16: CGFloat = 16
    static let _20: CGFloat = 20

    init() {

    }

    var large: CGFloat {
        return Spacings._16
    }

    var medium: CGFloat {
        return Spacings._12
    }

    var small: CGFloat {
        return Spacings._8
    }

    var margin: CGFloat {
        return Spacings._20
    }
}



//Colors.swift

import SwiftUI

struct Colors: Hashable {

    var background = Color(hex: 0xffddff)
    var text = Color(hex: 0x181A1B)
    var subText = Color(hex: 0x94999E)
    var tintColor = Color.red

    var yellow = Color.yellow
    var purple = Color.purple

    var navyBlue = Color(hex: 0x3D73CD)
    lazy var primary = navyBlue

    init() {

    }
}

// Color + Utilities
extension Color {
    init(hex: UInt) {
        self.init(
            red: Double((hex >> 16) & 0xff) / 255,
            green: Double((hex >> 08) & 0xff) / 255,
            blue: Double((hex >> 00) & 0xff) / 255,
            opacity: 1.0
        )
    }

    var uiKitColor: UIColor {
        UIColor(self)
    }
}



//Fonts.swift

import SwiftUI

struct Fonts: Hashable {

    var title: Font = Font.system(.headline)
    var bodyFont: Font = Font.system(.body)

    init() {

    }
}


//Icons.swift

import SwiftUI

struct Icons: Hashable, Identifiable {

    var homePageIconName = "checkmark.circle.fill"
    var stylePageIconName = "smallcircle.fill.circle.fill"

    func hash(into hasher: inout Hasher) {
        hasher.combine(homePageIconName)
        hasher.combine(stylePageIconName)
    }

    var id: String {
        homePageIconName + stylePageIconName
    }

    var homePageIcon: Image {
        Image(systemName: homePageIconName)
    }
    var stylePageIcon: Image {
        Image(systemName: stylePageIconName)
    }

    init() {

    }
}


Creating a Custom Environment Object

Now that we have styles and custom environment key ready, let's start with the custom environment object with styling property that encodes all the appearance settings.

We will use the singleton instance of this class applicable to all views in the app.


import SwiftUI

final class PlantEnvironment {
    static var shared = PlantEnvironment()

	var styling: Styling

    init(styling: Styling = .marigold) {
        self.styling = styling
    }
}

In the above example, we are using the shared variable which represents the singleton of type PlantEnvironment. In case the app doesn't explicitly specify, we are using marigold as a default style.

We are expecting the whole app to change its appearance as soon as the style is changed. However, the above code only works on the SwiftUI views. Presumably, your app may also be making use of UINavigationBar APIs which will have no effect from the style change.

To apply styles to UIKit elements, we need to create an observer on styling property and update the navigation bar style as soon as a style change is detected.

The PlantEnvironment class looks like this after an update


import Combine
import SwiftUI

final class PlantEnvironment: ObservableObject {
    static var shared = PlantEnvironment()

    @Published var styling: Styling

    private var stylingCancellable: AnyCancellable?

    init(styling: Styling = .marigold) {
        self.styling = styling
        updateAppearances(with: styling)

        stylingCancellable = $styling.sink {
            self.updateAppearances(with: $0)
        }
    }

    private func updateAppearances(with styling: Styling) {
        let backgroundColor = styling.colors.background.uiKitColor
        let titleColor = styling.colors.text.uiKitColor
        let tintColor = styling.colors.tintColor.uiKitColor
        let titleFont = UIFont.boldSystemFont(ofSize: 25)
        let largeTitleFont = UIFont.systemFont(ofSize: 34)

        let navigationAppearance = UINavigationBarAppearance()
        navigationAppearance.configureWithOpaqueBackground()
        navigationAppearance.backgroundColor = backgroundColor

        navigationAppearance.titleTextAttributes = [.foregroundColor: titleColor, .font: titleFont]
        navigationAppearance.largeTitleTextAttributes = [.foregroundColor: titleColor, .font: largeTitleFont]

        UINavigationBar.appearance().standardAppearance = navigationAppearance
        UINavigationBar.appearance().compactAppearance = navigationAppearance
        UINavigationBar.appearance().scrollEdgeAppearance = navigationAppearance

        UINavigationBar.appearance().tintColor = tintColor

        for window in UIApplication.shared.windows {
            for view in window.subviews {
                view.removeFromSuperview()
                window.addSubview(view)
            }
        }
    }
}


We will call updateAppearances the first time when the PlantEnvironment object is instantiated and then every time after styling is changed.

We also need a utility to assign the current style the styling key associated with the environment and by default pass the PlantEnvironment to SwiftUI views in the form of EnvironmentObject.


extension View {
    func plantEnvironment(_ plantEnvironment: PlantEnvironment = .shared) -> some View {
        self
            .environment(\.styling, plantEnvironment.styling)
            .environmentObject(plantEnvironment)
    }
}

Using this utility, you can apply any style and pass the pre-defined PlantEnvironment object to view before it is presented to the user. That way, the view will style itself using the style property defined in the current plant environment object.

This utility can be used on any view using the following syntax. By default, we will use the shared singleton instance of PlantEnvironment


HomeView()
    .plantEnvironment(.shared)
    
    

Building an app with Custom Styles and Environments

Now that our foundation is ready, we will create an app that will make use of custom styles and a global environment that will globally apply styles to all parts of the app as long as that screen is using the styles associated with the custom PlantEnvironment object in the form of EnvironmentObject.

First, we will create a top-level view that will use the shared singleton object of type PlantEnvironment and implicitly pass it down to all its children.


import SwiftUI

@main
struct GlobalStylingApp: App {

    @StateObject var plantEnvironment: PlantEnvironment = .shared

    var body: some Scene {
        WindowGroup {
            NavigationStack {
                HomeView()
            }
            .plantEnvironment(plantEnvironment)
            .navigationBarTitleDisplayMode(.inline)
            .background(plantEnvironment.styling.colors.background)
        }
    }
}


Children can reference, access, and use the styles associated with this passed object by referring to it with the following line,

@EnvironmentObject var plantEnvironment: PlantEnvironment

on the top level of the file.

As long as the plantEnvironment object is passed to the view, you can access the styles and apply color, spacing, and images associated with it to the corresponding view.


import SwiftUI

struct HomeView: View {

    @EnvironmentObject var plantEnvironment: PlantEnvironment

    var body: some View {
        VStack {
            plantEnvironment.styling.icons.homePageIcon

            NavigationLink {
                
            } label: {
                Text("Customize Styles")
                    .foregroundStyle(plantEnvironment.styling.colors.text)
                    .padding()
            }
        }
        .navigationTitle(plantEnvironment.styling.title)
    }
}

If you run the app now, you will see the default marigold style applied to the home screen.

Changing Styles

So far we saw how to create custom environment keys and styles that can be applied throughout the app. In this section, we will see how to update app styles on the fly and verify those changes.

We will use the SegmentedPicker to allow users to choose any style they want. As soon as the style choice is made, we will assign the chosen value to the styling property on plantEnvironment environment object passed to this view.


@EnvironmentObject var plantEnvironment: PlantEnvironment

Picker("Picker", selection: $plantEnvironment.styling) {
    Text("Marigold")
        .tag(Styling.marigold)

    Text("Sunflower")
        .tag(Styling.sunflower)

    Text("Brinjal")
        .tag(Styling.brinjal)
}
.pickerStyle(SegmentedPickerStyle())


We are assigning the tag to each of the segmented picker choices. As the user makes the choice, it will take the chosen value and automatically assign it to styling property on the plantEnvironment object.

With this main portion ready, let's create a style view from where user can change styles and add navigation to it from the home screen,


//StyleView.swift

import SwiftUI

struct StyleView: View {

    @EnvironmentObject var plantEnvironment: PlantEnvironment

    var body: some View {
        VStack {
            Text("Customize Styles")

            plantEnvironment.styling.icons.stylePageIcon

            Picker("Picker", selection: $plantEnvironment.styling) {
                Text("Marigold")
                    .tag(Styling.marigold)

                Text("Sunflower")
                    .tag(Styling.sunflower)

                Text("Brinjal")
                    .tag(Styling.brinjal)
            }
            .pickerStyle(SegmentedPickerStyle())
            }

        }
        .padding()
        .navigationTitle(plantEnvironment.styling.title)
    }
}

Navigating from the Home screen


//HomeView.swift

NavigationLink {
    StyleView()
} label: {
    Text("Customize Styles")
        .foregroundStyle(plantEnvironment.styling.colors.text)
        .padding()
}

Now let's run the app and observe styles changes as the user toggles between available choices,

0:00
/

Verifying Styles Changes

To make sure our style changes get propagated everywhere in the app, we will add a navigation link to StyleView screen which will take us further to the styles demo screen where we will be able to see all the changes associated with the current ly applied style.

Navigation from StyleView.swift screen


......
...

Picker("Picker", selection: $plantEnvironment.styling) {
    Text("Marigold")
        .tag(Styling.marigold)

    Text("Sunflower")
        .tag(Styling.sunflower)

    Text("Brinjal")
        .tag(Styling.brinjal)
}
.pickerStyle(SegmentedPickerStyle())

NavigationLink {
    StyleUpdateDemoView()
} label: {
    Text("Tap to view updated styles")
        .foregroundStyle(plantEnvironment.styling.colors.text)
}

......
....
..


StyleUpdateDemoView.swift Screen


import SwiftUI

struct StyleUpdateDemoView: View {

    @EnvironmentObject var plantEnvironment: PlantEnvironment

    var body: some View {
        VStack {
            Text("Title")
                .foregroundStyle(plantEnvironment.styling.colors.text)
            Text("Subtitle")
                .foregroundStyle(plantEnvironment.styling.colors.subText)
        }
        .background(plantEnvironment.styling.colors.background)
        .navigationTitle(plantEnvironment.styling.title)
    }
}

Now let's run the app, change styles, and view style changes on all three screens.

0:00
/

Source Code

The full source code for this tutorial is available on the GitHub GlobalStyles Repo for further reference

Summary

So this was all about applying global styles in the iOS app using SwiftUI APIs. Hope you liked it and will be helpful for you to improve styles in your existing app. Have you used any other global styling framework? Do you have any suggestions for improvement in the current code or this blog post? Let me know. I would love to hear your thoughts on this topic.

Support and Feedback

If you have any comments or questions, please feel free to reach out to me on LinkedIn.

If you like my blog content and wish to keep me going, please consider donating on Buy Me a Coffee or Patreon. Help, in any form or amount, is highly appreciated and it's a big motivation to keep me writing more articles like this.

Consulting Services

I also provide a few consulting services on Topmate.io, and you can reach out to me there too. These services include,

  1. Let's Connect
  2. Resume Review
  3. 1:1 Mentorship
  4. Interview Preparation & Tips
  5. Conference Speaking
  6. Take-home Exercise Help (iOS)
  7. Career Guidance
  8. Mock Interview