Geek Culture
Published in

Geek Culture

Adapting Material Theming from Jetpack Compose to SwiftUI

Learn how to adapt Material Theming concepts from Jetpack Compose to SwiftUI with this comprehensive guide

let's first get to know what Material theming in Jetpack Compose is and what it does.

Jetpack Compose allows developers to incorporate Material Design into their user interfaces using Material theming. Material Design is a design system that includes various components such as buttons, cards, and switches, which can be customized to match your brand through Material Theming.

With Material Theming, developers can easily adjust the color, typography, and shape attributes of the app, and these changes will be automatically applied to the components. When a Jetpack Compose project is created, a Material Theme is automatically generated with the following default attributes.

Colors (Dark and Light)

Material Theming in Jetpack Compose allows developers to customize the color scheme of their app by passing in dark and light colors into their respective functions. These functions return a Colors object, which can then be passed to the theme. The theme will then automatically apply the colors to various components such as buttons, switches, and other elements. This feature allows developers to easily match the color scheme of their app to their brand, and provides a consistent look and feel across all components.

private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Teal200
)

private val LightColorPalette = lightColors(
primary = Purple500,
primaryVariant = Purple700,
secondary = Teal200

/* Other default colors to override
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
*/
)

Shapes:

Material surfaces can be displayed in different shapes. Shapes direct attention, identify components, communicate state, and express brand.
Components are grouped into shape categories based on their size. These categories provide a way to change multiple component values at once, by changing the category’s values.

val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)

Typography:

The Material Design type scale includes a range of contrasting styles that support the needs of your product and its content.
The type scale is a combination of thirteen styles that are supported by the type system. It contains reusable categories of text, each with an intended application and meaning.

val Typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
),
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.W500,
fontSize = 14.sp
),
caption = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
)

)

Material Theme:

Material Theming refers to the customization of your Material Design app to better reflect your product’s brand. This when made a parent composable of a composable hierarchy automatically provides all the colors, typography, and shapes with a very idiomatic syntax.

@Composable
fun MyApplicationTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}

MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
MyApplicationTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background
) {
Card(shape = MaterialTheme.shapes.large) {
Text(text = "Hello World", style = MaterialTheme.typography.h1)
}
}
}

This is the typical way to use a Material Theme in Jetpack Compose. In addition to adjusting typography, colors, and shapes, developers can also add custom parameters or edit other parameters to further customize the appearance of the app. For more information on these advanced customization options, developers can refer to the official Jetpack Compose documentation.

Theming in Swift UI:

When I first transitioned from Android to SwiftUI, one of my initial thoughts was to find an alternative to Material Theming, as I did not want to use “Color(“Primary”)” throughout my codebase, and also if the color name changes, I would have to change it everywhere in the code. One simple alternative that came to my mind was to use a centralized file where I can define all the color constants and use those constants throughout my codebase. This would make it easy to change the color scheme of my app by simply modifying the constants in the centralized file, and eliminating the need to change the color in multiple places in the codebase.

extension Color {
static var globalGreen: Color {
Color("GlobalGreen")
}
static var surfaceBlue: Color {
Color("SurfaceBlue")
}
static var lavendarBlush: Color {
Color("LavendarBlush")
}
static var primaryBlue: Color {
Color("DimGray")
}
static var cardBackGround: Color {
Color("CardBackGround")
}
static var screenBgColor: Color {
Color("ScreenBgColor")
}
static var marqueeBg: Color {
Color("MarqueeBg")
}
static var orangeClr: Color {
Color("Orange")
}
}

and then I can do something Like this

Color.orangeClr

While using a centralized file for color constants can simplify the process of changing the color scheme of the app, it does not address the issue of other Design elements such as typography, shapes, spacing, and shadows. These elements are also important for creating a consistent look and feel throughout the app and should also be considered when designing the user interface.

One solution to this problem would be to create a set of constants or utility functions for these other Design elements, similar to the centralized file for color constants. This would make it easy for developers to access and use these elements throughout the codebase, and also keep the design consistent.

However it is important to keep in mind that the developers working on the app would primarily be Android developers shifting to KMM, so it would be beneficial to use an approach that feels idiomatic to them and doesn’t create extra learning curve.

Building Theme:

After thinking for some time and looking through some code bases, we decided to build our own theme inspired by Material 2 for use in Swift UI.

Step 1:

To start, we defined the basics of Material Design: typography, colors, and shapes. We used Swift structs to define these elements. In this step, we defined basic font types that were part of our design system. You can add as many font types as your design system requires, such as “Button”, “Body3”, or any other font type you may need.

This step lays the foundation for our theming system and allows us to easily access and use these elements throughout our codebase. It also ensures that the design of our app remains consistent and adheres to the Design guidelines.

struct Typogrpahy{
var h1:Font
var h2:Font
var h3:Font
var h4:Font
var h5:Font
var h6:Font
var body1:Font
var body2:Font

}

We created structs for each color variant, such as primary, secondary, background, and error. Each struct contains different shades of each color.

This step allows us to easily access and use color variants throughout our codebase and makes it simple to change the color scheme of the app by modifying the color constants in the centralized file. With this approach, we can define the color variants once and use them throughout the app, making it easy to update the colors if needed.


struct Colors {
var primary: Color
var secondary: Color
var primaryVariant: Color
var backGround: Color
var surface: Color
var onPrimary: Color
var onBackGround: Color
var onSurface : Color
var onError: Color
var error: Color
var warning: Color
var success: Color
var outline: Color
var disabled: Color
var severeWarning: Color
var generalNotes: Color
var successNotes: Color
}

Now that we have defined the colors and typography, the next step is to define the shapes and spacing for our theming system. I wanted to make sure that spacing is also a part of the theme, so I created a struct for shapes and spacing. This struct includes different shape attributes such as large and small shapes, as well as spacing attributes that can be used as padding and margin.

struct Spacing{
var largeSpacing:CGFloat
var mediumSpacing:CGFloat
var smallSpacing:CGFloat
var extraLargeSpacing: CGFloat
}

struct Shapes{

var largeCornerRadius:CGFloat
var mediumCornerRadius:CGFloat
var smallCornerRadius:CGFloat
}

Now that we have defined all the elements of our design system, the next step is to create a centralized theme that will serve as the single source of truth for the whole design system. Instead of creating objects of each element and using them throughout the app, we can create a theme struct that contains all the elements we defined in the previous steps (typography, colors, shapes and spacing).

This centralized theme struct can be passed down to the different parts of the app and used to apply the design system consistently. This approach makes it easy to maintain and update the design system as it only needs to be done in one place and it will be reflected throughout the app.

Step 2:

We have now created the required structs that hold our design system’s information for the required components. The next step is to create a theme class that will serve as the central point for our design system. We created a class called “Theme” but you can name it anything, such as “MaterialTheme”, “MyAppTheme”, or “TheVeryCoolAppTheme”. Note that we have inherited it from an ObservableObject, we will see the reason for that later on.

class Theme:ObservableObject {


let colors : Colors
let shapes: Shapes
let spacing: Spacing
let typography: Typogrpahy
}

The theme class has four objects of the components that we defined as structs, with the information that we need to implement our design system. We need to initialize these objects, so we created an init method that takes in the objects of each component as arguments.

class Theme : ObservableObject{
let colors: Colors
let shapes: Shapes
let spacing: Spacing
let typography: Typography

init(colors: Colors, shapes: Shapes, spacing: Spacing, typography: Typography) {
self.colors = colors
self.shapes = shapes
self.spacing = spacing
self.typography = typography
}
}

The theme class is now complete and it serves as a single source of truth for our design system. All the components that make up our design system are now centralized in one place and can be easily accessed and used throughout the app

Step 3:

Now that we have our centralized theme class, we can create different variations of the theme to support different modes such as light and dark mode. In iOS, you can define different variables of the same color in assets, so the colors might not be making sense for you, but this approach is useful if you want to use different colors other than those in assets or more themes, similarly, typography etc.

We created two extensions of our theme class, “dark” and “light” mode, with the same components but with different values. These values can be changed according to your design system. This approach makes it easy to switch between different themes and also makes it easy to change the theme of the app.

It’s important to note that having everything centralized makes sense and is easier to use because all the elements of the design system are in one place and can be easily accessed and used throughout the app.

extension Theme {
static let dark = Theme(

colors: Colors(primary: Color("Primary"), secondary: Color("Secondary"), primaryVariant: Color("PrimaryVariant"), backGround: Color("Background"), surface: Color("Surface"), onPrimary: Color("OnPrimary"), onBackGround: Color("OnBackGround"), onSurface: Color("OnSurface"), onError: Color("OnError"), error: Color("Error"), warning: Color("Warning"), success: Color("Success"), outline: Color("OutlineColor"), disabled: Color("DisabledColor"), severeWarning: Color("SevereWarning"), generalNotes: Color("GeneralNotes"), successNotes: Color("SuccessNotes")),

shapes: Shapes(largeCornerRadius: 16, mediumCornerRadius: 12, smallCornerRadius: 8),

spacing: Spacing(largeSpacing: 24, mediumSpacing: 16, smallSpacing: 8, extraLargeSpacing: 32),

typography: Typography(h1: Font.custom("NunitoSans-Bold", size: 32), h2:Font.custom("NunitoSans-Bold", size: 24), h3: Font.custom("NunitoSans-Bold", size: 18), h4: Font.custom("NunitoSans-Bold", size: 14), h5: Font.custom("NunitoSans-Bold", size: 12), h6: Font.custom("NunitoSans-Bold", size: 10), body1: Font.custom("NunitoSans-Regular", size: 14), body2: Font.custom("NunitoSans-Regular", size: 14)))

static let light = Theme(

colors: Colors(primary: Color("Primary"), secondary: Color("Secondary"), primaryVariant: Color("PrimaryVariant"), backGround: Color("Background"), surface: Color("Surface"), onPrimary: Color("OnPrimary"), onBackGround: Color("OnBackGround"), onSurface: Color("OnSurface"), onError: Color("OnError"), error: Color("Error"), warning: Color("Warning"), success: Color("Success"), outline: Color("OutlineColor"), disabled: Color("DisabledColor"), severeWarning: Color("SevereWarning"), generalNotes: Color("GeneralNotes"), successNotes: Color("SuccessNotes")),

shapes: Shapes(largeCornerRadius: 16, mediumCornerRadius: 12, smallCornerRadius: 8),

spacing: Spacing(largeSpacing: 24, mediumSpacing: 16, smallSpacing: 8, extraLargeSpacing: 32),

typography: Typography(h1: Font.custom("NunitoSans-Bold", size: 32), h2:Font.custom("NunitoSans-Bold", size: 24), h3: Font.custom("NunitoSans-Bold", size: 18), h4: Font.custom("NunitoSans-Bold", size: 14), h5: Font.custom("NunitoSans-Bold", size: 12), h6: Font.custom("NunitoSans-Bold", size: 10), body1: Font.custom("NunitoSans-Regular", size: 14), body2: Font.custom("NunitoSans-Regular", size: 14)))

}

Step 4:

The final step of creating a custom theme system is to create a theme manager that will be responsible for switching and handling the current theme. The theme manager will hold the current theme and when we want to switch the theme throughout the app, we only need to change the theme in this manager, and it will reflect the changes everywhere inside the app.

We have created a class called “ThemeManager” which inherits from “ObservableObject” and has a Published variable named “current” which is of type Theme and is initialized with light mode as default. We can switch the theme by changing this variable, and the change will be propagated throughout the app.

With this approach, we have created a centralized and easily maintainable theme system for our app and allows for easy theme switching.

class ThemeManager: ObservableObject {
@Published var current: Theme = .light
}

Final Step:

In order to use our newly created theme in our views, we first need to create a ThemeManager object as a state object. The @EnvironmentObject is a smarter, simpler way of using @ObservedObject on lots of views. Rather than creating some data in view A, then passing it to view B, then view C, then view D before finally using it, you can create it in view A and put it into the environment so that views B, C, and D will automatically have access to it. Source.

In the iOSApp struct, we created a @StateObject variable themes of type ThemeManager, which will allow us to manage the current applied theme. Then in the body of the struct, we used environmentObject to make the theme manager available to all the views inside the WindowGroup.

struct iOSApp: App {


@StateObject var themes = ThemeManager()


var body: some Scene {
WindowGroup {
ContentView()
// This will allow us to manage the current applied theme
.environmentObject(themes)
//This will help us to access the members of current theme
.environmentObject(themes.current)
}
}

}

In the ContentView struct, we used the @EnvironmentObject to access the members of the current theme. The environmentObject makes it easy to access the theme throughout the views and change it as we like.

struct ContentView: View {
@EnvironmentObject var theme : Theme
@EnvironmentObject var themeManeger : ThemeManager
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!").font(theme.typography.h1).foregroundColor(theme.colors.warning)
ZStack{
Capsule().foregroundColor(theme.colors.error).onTapGesture {
themeManeger.current = .dark

}.frame(width: 200, height: 90)

}

Text("Change theme").foregroundColor(theme.colors.error).font(theme.typography.body2).onTapGesture {
themeManeger.current = .light

}
}
.padding()
}
}

It’s easy to use and you can change any theme that you want, and as you like. For example, if you want your users to customize the app, you can also do this with these few lines of code. The ThemeManager object can be exposed to the user interface, allowing them to switch between different themes with ease.

But are we done yet? NO

Creating custom view modifiers is a great way to make your theme more idiomatic and easy to use in SwiftUI. By creating view modifiers, you can encapsulate the font, color, and other styling properties of your theme into a single, reusable unit. This way, you can easily apply these styles to any view in your app, making it more consistent and easier to maintain.

struct LargeTitleStyle: ViewModifier {
@EnvironmentObject var theme: Theme
var defaultTextColor : Color?
init(defaultTextColor: Color? = nil) {
self.defaultTextColor = defaultTextColor
}
func body(content: Content) -> some View {
content.font(theme.typography.h2).foregroundColor(defaultTextColor ?? theme.colors.onSurface).multilineTextAlignment(.leading)
}
}

In the above example, we have defined a struct LargeTitleStyle which implements the ViewModifier protocol. We have used the @EnvironmentObject to access the theme, as well as a defaultTextColor argument to set the color of the text. In the body of the struct, we have defined the font, color, and alignment properties for the text.

Text(heading).modifier(LargeTitleStyle())

You can use this modifier by calling the .modifier() method on any Text view and passing in the modifier, like this: Text(heading).modifier(LargeTitleStyle()) This is a great way to make your code more readable and maintainable. You can also create other modifiers for different text styles and use them throughout your app.

Bonus:
In order to make it more theme oriented. You can also do something like this

struct H3Style :  ViewModifier {
@EnvironmentObject var theme: Theme
var defaultTextColor : Color? = Color("OnSurface")

init(defaultTextColor: Color? = nil) {
self.defaultTextColor = defaultTextColor
}

func body(content: Content) -> some View {

return content.font(theme.typography.h3).foregroundColor(defaultTextColor ?? theme.colors.primaryVariant).multilineTextAlignment(.leading)
}
}

this uses the h3 font and makes a style with different attributes that you like as i am using color and then in your typography struct, you can add functions to handle these modifiers.

struct Typography{
var h1:Font
var h2:Font
var h3:Font
var h4:Font
var h5:Font
var h6:Font
var body1:Font
var body2:Font

func h6Style(color: Color = Color("OnSurface")) -> some ViewModifier {
return H6Style(defaultTextColor: color)
}
func h5Style(color: Color = Color("OnSurface"))-> some ViewModifier {
return H6Style(defaultTextColor: color)
}
func h4Style(color: Color = Color("OnSurface")) -> some ViewModifier {
return H6Style(defaultTextColor: color)
}
func h3Style(color: Color = Color("OnSurface"))-> some ViewModifier {
return H6Style(defaultTextColor: color)
}
func h2Style(color: Color = Color("OnSurface")) -> some ViewModifier {
return H6Style(defaultTextColor: color)
}
func h1Style(color: Color = Color("OnSurface")) -> some ViewModifier {
return H6Style(defaultTextColor: color)
}


}

and then finally,

Text("I have got the fonts like compose").modifier(theme.typography.h3Style())

Overall, using Theming and creating custom view modifiers can make developing SwiftUI applications much more fun, efficient, and easy to maintain. I hope this article has been helpful and if you liked it, don’t forget to check out the sample project available on Github.

Connect with me on Twitter:

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Kashif Mehmood

Engineer, Speaker, Writer, Open Source Contributor, Kotlin lover, XR geek, Bibliophile, and much more