Featured Image

How to apply global tint & accentColor in SwiftUI

April 19, 2023
 · 
4 min read

Accent and tint colors are currently in transition in SwiftUI. The term accentColor had a few different meanings, depending on context. The accentColor() method could assign a global tint color throughout your app. But the .accentColor property has different behaviors on iOS and macOS. For example, in macOS, SwiftUI only applies accentColor properties if the user chooses Multicolor in System Preferences. These inconsistencies make it easy to understand why the accentColor() method was recently deprecated.

What’s harder to understand is the new approach for applying a global accent color. Apple recommends using the AccentColor color set in the Asset Catalog. This is easy for a beginner to work with, but it’s a really primitive solution.

Tangentially, SwiftUI doesn’t have a global styles system akin to web (CSS) and Android (XML styles). As a work-around, I created a custom Style class to define global style for stuff like frame sizes, padding, and colors. It’s especially helpful for developing a native “iOS macOS” app since each platform scales differently and has unique color libraries. (I’m still pretty new to Swift and SwiftUI, so maybe there’s a better approach.)

The Solution

Here’s how to apply global tint & accentColor in SwiftUI. It’s a solution for apps that target recent updates, iOS 16.4 and macOS 13.3, or later. With a little work (or in some case, a lot) it can also be dynamic.

AppColor class

I created an AppColor class. It extends ObservableObject and will be used throughout my app as an EnvironmentObject.

Swift
class AppColor: ObservableObject {
  @Published var tint:Color = .systemRed
}

Custom controls

The benefit of this approach is I can apply my tint color to non-native controls when needed:

Swift
@EnvironmentObject var appColor: AppColor

Button (action: {
	},label: {
	Image(systemName: image)
	.foregroundColor(appColor.tint)
	.font(.system(size: Style.TabBarButton.fontSize))
})

Global tint & accentColor

That works for custom controls. What about native controls? It’s simple. You do it the same way as before, but with a twist. You can’t use an EnvironmentObject in your App.swift, but you can use an ObservedObject 😁

Swift
@main
struct MyApp: App {

var body: some Scene {
		
  @ObservedObject var appColor = AppColor()

  WindowGroup {
    AppView()
    .environmentObject(AppColor())
    .tint(appColor.tint)
    .accentColor(appColor.tint)
  }
}

The .tint() method will apply your tint color to all iOS and some macOS controls. The .accentColor() method is necessary for any macOS controls that don’t yet have a .tint property.

global-tint-and-color-app-oversize-100x
Timer and Settings screens using custom tint color

Can I make it dynamic?

Yes, but…

.tint and .accentColor are only partially “finished” in SwiftUI. Yes, they will update the global colors when appColor.tint changes to a new color. But some child controls will not automatically update to the new color.

In most cases, you can fix this by manually assigning a tint color:

Swift
Button("My Label") {	
}.tint(appColor.tint)

In other cases, it will be a lot more work. For example, .tint will not update a macOS Picker. You may have to create a custom Picker style instead (I don’t know for sure; it’s a lot of code).

Dynamic color test

For completeness-sake, here’s a simple solution to testing dynamic colors:

Swift
@EnvironmentObject var appColor: AppColor

@State var colors: [Color] = [.systemRed, .systemBlue,
  .systemCyan, .systemGray, .systemMint, .systemPink,
  .systemTeal, .systemBrown, .systemGreen, .systemIndigo,
  .systemOrange, .systemPurple, .systemYellow]

@State var index = 0

Button (action: {
	self.index += 1
	if (index >= colors.count) { self.index = 0 }
	appColor.tint = colors[index]
}

My Style class

You may have noticed in my first example that I used Style.TabBarButton.fontSize to specify the font size for my TabBarButton icon. I’m experimenting with using a Style class to manage global styles. This is particularly useful for “iOS macOS” apps because they use different scales and colors.

Here’s an example of how I’m doing styles:

Swift
class Style {

  #if os(macOS)
    class Button {
      static let height: Double = 36.0
      static let cornerRadius: Double = 10.0
      static let opacityPressed: Double = 0.75
    }
    class TabBarButton {
      static let height: Double = 44.0
      static let width: Double = 48.0
      static let fontSize: Double = 18.0
      static var cornerRadius: Double {
        Button.cornerRadius
    }
  }
  
  #else // iOS
    class Button {
      static let height: Double = 48.0
      static let cornerRadius: Double = 10.0
      static let opacityPressed: Double = 0.75
    }
    class TabBarButton {
      static let height: Double = 56.0
      static let width: Double = 64.0
      static let fontSize: Double = 25.0
      static var cornerRadius: Double {
        Button.cornerRadius
      }
    }
  #endif
}

It’s not elegant, but it’s far better than nothing. One benefit is that it reduced a lot of #if os(macOS) code from my views. It also uses cascading property values like CSS and Android styles. Finally, it’s a class so my app will refer to a single instance, instead of making copies as a struct.

I’ll try to follow up with more details on styles (and colors) later on.

Thanks for reading! Be sure to follow me on Mastodon and Twitter.

Comments

No Comments.

Leave a replyReply to

footer-logo

Twitch   •   Mastodon
Twitter   •   LinkedIn   •   Mixcloud

thank you for visiting  ❤︎
View