All articles
iOS14 min read

SwiftUI Styling Guide: Fonts, Themes, Dark Mode & Why Order Matters

🎨 Give your SwiftUI app a complete style upgrade! Learn how to add custom fonts, build adaptive themes, support dark mode like a pro —…

K
Karan Pal
Author
Image generated by AI
Image generated by AI

✨ Why Styling in SwiftUI Deserves Your Attention

Let’s be honest — building the logic of an app is fun… but making it look good? That’s what takes it from “meh” to “whoa, did you design this?!” 😎

In UIKit, styling meant juggling UIAppearance, setting properties on every UIButton, and making sure your app didn’t explode when switching to dark mode. Totally doable — but not exactly elegant.

With SwiftUI, you can:

And the best part? You can preview all your styles live without running the app. If you’ve been following my MVVM in SwiftUI or Layout Guide articles, you already know how much SwiftUI loves clean structure and composability. Styling is no different.

In this guide, we’ll break down:

By the end, you’ll be styling your apps like a seasoned designer — without ever touching Interface Builder or fighting with storyboards again 💅

**MVVM in SwiftUI Explained: Build Scalable & Testable Apps with Clean Architecture** _Let’s unravel the mystery of MVVM and learn how to structure your SwiftUI apps with clean, testable architecture…_medium.comhttps://medium.com/swift-pal/mvvm-in-swiftui-explained-build-scalable-testable-apps-with-clean-architecture-2b36443ebfca

**SwiftUI Layout Guide: VStack, HStack, ZStack & Grids Explained (2025 Edition)** _Confused about SwiftUI layouts? 🤔 This complete 2025 guide breaks down VStack, HStack, ZStack, and Apple’s official…_medium.comhttps://medium.com/swift-pal/swiftui-layout-guide-vstack-hstack-zstack-grids-explained-2025-edition-285fb89b5de5

🖋️ Custom Fonts in SwiftUI: Make It Personal

You can only get so far with system fonts before your designer (or inner design snob) starts crying in SF Pro. Whether you’re building a playful kids’ app or a sleek fintech dashboard, custom fonts help give your app its own personality.

Here’s how to make them work in SwiftUI, without jumping through UIKit hoops.

📁 Step 1: Add the Font to Your Project

Drag your .ttf or .otf files into the Assets folder or directly into your project. Make sure:

Example:

<key>UIAppFonts</key>
<array>
    <string>MyCustomFont-Regular.ttf</string>
    <string>MyCustomFont-Bold.ttf</string>
</array>

💬 Step 2: Use the Font in SwiftUI

Now you’re ready to use it:

Text("Styled with custom font")
    .font(.custom("MyCustomFont-Regular", size: 20))

Important: The name used in .custom() is not always the filename. Use the PostScript name from the font metadata. (You can check it using macOS Font Book or a quick print from UIFont.familyNames in UIKit.)

📏 Step 3: Support Dynamic Type (Don’t Skip This!)

If you want your app to be accessible (which you do, right?), make it scale automatically:

Text("Scalable Custom Font")
    .font(.custom("MyCustomFont-Regular", size: 20, relativeTo: .body))

now your text resizes based on the user’s preferred settings. SwiftUI handles it beautifully.

🧼 Pro Tip: Create a Font Extension

Tired of writing .custom(“MyCustomFont-Regular”, size: 18) everywhere? Clean it up like this:

extension Font {
    static let heading = Font.custom("MyCustomFont-Bold", size: 24, relativeTo: .title)
    static let bodyText = Font.custom("MyCustomFont-Regular", size: 16, relativeTo: .body)
}

Now your view code looks much nicer:

Text("Hello, design world!")
    .font(.heading)

✅ Where This Fits in Real Projects

When paired with MVVM (like we do here), custom fonts live beautifully at the design layer. You keep UI details modular and reusable — exactly how SwiftUI wants it.

**MVVM in SwiftUI Explained: Build Scalable & Testable Apps with Clean Architecture** _Let’s unravel the mystery of MVVM and learn how to structure your SwiftUI apps with clean, testable architecture…_medium.comhttps://medium.com/swift-pal/mvvm-in-swiftui-explained-build-scalable-testable-apps-with-clean-architecture-2b36443ebfca

🎨 Custom Colors & Themes with Asset Catalogs

You’ve got the fonts, now let’s paint the app! Whether you want your app to pop in pastel, go full goth in dark mode, or switch themes dynamically like a tech-savvy chameleon — SwiftUI’s got your back.

🎨 Step 1: Define Colors in Your Asset Catalog

Xcode’s Asset Catalog makes defining color themes a breeze:

  1. Open Assets.xcassets
  2. Click the “+” → choose “Color Set”
  3. Name it something meaningful like BrandPrimary or TextOnDark
  4. Click on the color → set “Any, Light, Dark” appearances
  5. Choose colors for each theme as needed

💡 Bonus: Use semantic names! Avoid naming your color Blue654. Go with ButtonBackground or AccentText.

🌗 Step 2: Use Your Colors in SwiftUI

Access your color by name, just like system colors:

Text("Themed Text")
    .foregroundColor(Color("TextOnDark"))

These automatically adapt to light/dark mode — no extra effort needed.

Want a background view?

Rectangle()
    .fill(Color("CardBackground"))
    .frame(height: 200)

Easy-peasy, and it just works.

🎨 Step 3: Mixing System Colors & Custom Colors

SwiftUI also gives you a set of adaptive system colors like .primary, .secondary, .accentColor, .background, etc. You can mix them with your own:

Text("Hello")
    .foregroundColor(.primary) // Auto-adapts

But if you need brand-specific colors? Stick with your custom ones for full control.

👀 Bonus: Previewing Colors in Light & Dark Mode

SwiftUI previews let you toggle appearances quickly:

#Preview {
    ContentView()
        .preferredColorScheme(.dark)
}

This lets you spot color contrast issues before your designer does 😅

🧠 Why Asset Catalog Colors Rock

If you’re building forms, inputs, or layouts using ZStack/VStack, pairing them with color themes creates beautiful, consistent sections. We covered these components in the Forms & Inputs guide — worth checking out after this.

**Building Forms and Inputs in SwiftUI: TextFields, Pickers, Toggles & More** _Learn how to build beautiful and functional forms in SwiftUI using TextFields, Pickers, Toggles, and more. From simple…_medium.comhttps://medium.com/swift-pal/building-forms-and-inputs-in-swiftui-textfields-pickers-toggles-more-14d1aafcffe9

🌘 Embracing Dark Mode in SwiftUI (Without Losing Your Mind)

Remember back in UIKit when supporting dark mode meant juggling hex codes and praying your white backgrounds didn’t blind users at 2 AM? Yeah, good times. 😅

SwiftUI, thankfully, makes dark mode support so seamless you might already have it without realizing it.

🪄 SwiftUI Does Most of the Work for You

SwiftUI automatically respects the system appearance. If you’re using:

…your UI probably already adapts to dark mode like a pro 🧙‍♂️

Here’s a super simple example:

Text("Welcome to the Dark Side")
    .padding()
    .background(Color("CardBackground"))
    .cornerRadius(12)

If CardBackground has different values for light and dark in the asset catalog — you’re done. No extra code!

👁️ Preview in Dark Mode

No need to build and run to test dark mode. Use the preview engine:

#Preview {
    ContentView()
        .preferredColorScheme(.dark)
}

You can even add side-by-side previews:

#Preview("Light") {
    ContentView()
        .preferredColorScheme(.light)
}

#Preview("Dark") {
    ContentView()
        .preferredColorScheme(.dark)
}

Beautiful and productive 🙌

🎛️ Force a Color Scheme (But Be Careful)

Sometimes, for branding or testing, you may want to lock your app into a color scheme:

.someView()
    .preferredColorScheme(.dark)

But tread lightly. Overriding system preferences too often can lead to UX confusion — or worse, Apple review rejections 😬

🧪 Dark Mode Testing Tips

You’d be surprised how quickly white text on a light gray background can slip through in dark mode. (It’s the silent killer of design QA.)

🎭 Building a Custom Theming System in SwiftUI

Want to support multiple themes in your app? Like light/dark and “Unicorn Mode” 🦄 or “Developer Mode” 🧑‍💻? With a bit of setup, SwiftUI makes it easy to create a scalable, maintainable theming system — no hacks, no mess.

🧱 Step 1: Define a Theme Model

Let’s start by creating a basic AppTheme struct that holds your design values:

struct AppTheme {
    let primaryColor: Color
    let backgroundColor: Color
    let font: Font
}

Now define a few themes:

struct ThemeManager {
    static let light = AppTheme(
        primaryColor: Color("BrandPrimary"),
        backgroundColor: Color.white,
        font: .custom("MyCustomFont-Regular", size: 16)
    )
    
    static let dark = AppTheme(
        primaryColor: Color("BrandPrimary"),
        backgroundColor: Color.black,
        font: .custom("MyCustomFont-Regular", size: 16)
    )
}

💡 Step 2: Inject It Using @Environment

Let’s make a key for our theme:

private struct ThemeKey: EnvironmentKey {
    static let defaultValue = ThemeManager.light
}

extension EnvironmentValues {
    var theme: AppTheme {
        get { self[ThemeKey.self] }
        set { self[ThemeKey.self] = newValue }
    }
}

Now you can inject and use your theme in any view:

struct ThemedText: View {
    @Environment(\.theme) var theme
    
    var body: some View {
        Text("Styled by theme")
            .font(theme.font)
            .foregroundColor(theme.primaryColor)
            .padding()
            .background(theme.backgroundColor)
    }
}

🔁 Step 3: Toggle Themes Dynamically

Let’s wrap your current theme in a view model:

class ThemeViewModel: ObservableObject {
    @Published var currentTheme = ThemeManager.light

    func toggle() {
        currentTheme = (currentTheme.primaryColor == ThemeManager.light.primaryColor) ? ThemeManager.dark : ThemeManager.light
    }
}

Inject it into your app environment:

@main
struct MyApp: App {
    @StateObject private var themeVM = ThemeViewModel()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.theme, themeVM.currentTheme)
        }
    }
}

Hook up a toggle button and you’ve got runtime theming 🎉

🧪 Test, Reuse, and Expand

Once you have the pattern in place, you can:

Pair this with MVVM, like we discussed in this article, and your app becomes scalable, testable, and consistent across screens.

**MVVM in SwiftUI Explained: Build Scalable & Testable Apps with Clean Architecture** _Let’s unravel the mystery of MVVM and learn how to structure your SwiftUI apps with clean, testable architecture…_medium.comhttps://medium.com/swift-pal/mvvm-in-swiftui-explained-build-scalable-testable-apps-with-clean-architecture-2b36443ebfca

✍️ Pro Tip

Instead of hardcoding themes in the app, you can also fetch theme configs from remote sources or user settings, enabling dynamic branding, feature flags, and more.

Now your SwiftUI app doesn’t just work and look good — it adapts to the user’s style, your brand, or the mood of the day. 🧠🎨

✨ Bonus Styling Tips to Polish Your SwiftUI App

At this point, you’ve got fonts, colors, dark mode, and themes dialed in. But let’s sprinkle in some bonus SwiftUI styling tricks that make your app feel less like a prototype and more like a polished product on the App Store 🧽✨

🟣 Rounded Corners and Shadows that Don’t Scream “Default”

SwiftUI makes it very easy to overuse .cornerRadius(10) and call it a day. Let’s level it up:

RoundedRectangle(cornerRadius: 20)
    .fill(Color("CardBackground"))
    .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4)
    .padding()

This adds subtle elevation and makes elements feel touchable — essential for cards, buttons, and modals.

🧠 Pro tip: Avoid full black shadows. Use opacity + blur for natural depth.

🔘 Styling Buttons the SwiftUI Way

Custom buttons used to mean subclassing or using UIButton.appearance. In SwiftUI?

Button("Get Started") {
    print("Tapped!")
}
.buttonStyle(.borderedProminent)
.controlSize(.large)

Or build your own style:

struct PrimaryButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding()
            .background(Color("BrandPrimary"))
            .foregroundColor(.white)
            .cornerRadius(12)
            .scaleEffect(configuration.isPressed ? 0.95 : 1)
    }
}

Apply it like so:

Button("Sign In") {
    // action
}
.buttonStyle(PrimaryButtonStyle())

🎉 Fully customizable, reusable, and animatable.

🔤 Make TextFields Look Good (And Feel Right)

TextFields look kinda… meh out of the box. Fix it up:

TextField("Email", text: $email)
    .padding()
    .background(Color(.secondarySystemBackground))
    .cornerRadius(10)
    .overlay(
        RoundedRectangle(cornerRadius: 10)
            .stroke(Color.gray.opacity(0.3), lineWidth: 1)
    )
    .font(.body)

🔐 Bonus: You can theme this input styling using your AppTheme too!

🧱 Create Reusable ViewModifiers

Don’t repeat the same styling code everywhere. Instead:

struct CardStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color("CardBackground"))
            .cornerRadius(16)
            .shadow(radius: 4)
    }
}

extension View {
    func cardStyle() -> some View {
        self.modifier(CardStyle())
    }
}

Now just use .cardStyle() anywhere 💅

🔍 SwiftUI Gotcha: Modifier Order Matters (Yes, Seriously)

In SwiftUI, the order of view modifiers isn’t just a nice-to-have — it’s a full-blown code law. Apply them in the wrong order, and your layout might go from “beautifully styled” to “why is this button floating in space?” 🪐

Let’s look at how modifier sequencing affects everything from shadows to tap areas.

⚠️ Padding vs. Background

Text("Hello")
    .background(Color.yellow)
    .padding()

🟡 Result:

The yellow background hugs the text tightly, and padding is added outside of that.

Now flip it:

Text("Hello")
    .padding()
    .background(Color.yellow)

💡 Result:

The padding expands the frame first, so now the yellow background wraps the whole padded space, giving you a pill-style appearance. Subtle, but visually very different.

👻 Shadow That Disappears?

Text("Buy Now")
    .shadow(radius: 5)
    .background(Color.green)

❌ This won’t give you a green pill button with a shadow. You’ll get:

✅ Fix it like this:

Text("Buy Now")
    .padding()
    .background(Color.green)
    .cornerRadius(10)
    .shadow(radius: 5)

Now the shadow applies to the rounded green background — as intended.

📱 Tap Area Too Small?

Button(action: {}) {
    Text("Tap Me")
}
.background(Color.blue)

😬 This one’s deceptive. The blue background looks clickable, but only the text is tappable. Why? Because the button’s tap area is just the text view.

✅ Better:

Button(action: {}) {
    Text("Tap Me")
        .padding()
        .background(Color.blue)
        .foregroundColor(.white)
        .cornerRadius(10)
}

Now it’s got proper padding inside the button, and a visually correct tap target.

🧠 Rule of Thumb: The Golden Modifier Sequence

Use this flow to avoid layout headaches:

  1. 🧱 Layout firstpadding, frame, alignment, offset
  2. 🎨 Style nextbackground, cornerRadius, shadow, border
  3. 🤹 Behavior lastonTapGesture, gesture, transition, animation, accessibility

Knowing this one principle can save you hours of head-scratching and Stack Overflow lurking. 🕵️‍♂️

🔄 Wrapping Up: Your SwiftUI Style Game, Upgraded

You made it! 💪 By now, your SwiftUI app is no longer wandering the streets in default SF font and grayscale backgrounds. You’ve learned how to give it style — the kind of style that says, “Yes, this app is on the App Store, thank you very much.”

Here’s a quick recap of what we covered:

Custom Fonts — Bring your brand’s personality with .custom() fonts and support Dynamic Type with ease

Theming Colors — Use Asset Catalogs for light/dark adaptive colors with clean names like CardBackground, not Blue654 😅

Dark Mode — SwiftUI handles most of it for you, but you now know how to preview and tweak like a pro

Custom Theming System — Reusable, scalable, and MVVM-friendly theming using @Environment

Styling Touches — Rounded corners, shadows, button styles, text fields, and ViewModifiers

Modifier Order — That sneaky rule that can make or break your layout… now demystified

With just these tools, you can take a boring, gray prototype and turn it into a vibrant, on-brand, delightful experience — all while keeping your code clean and testable 💅

🎨 Next…

Get ready for some reactive wizardry 🔮

Next in the SwiftUI Mastery Series:

👉 **_SwiftUI with Combine: Real-Time Reactive Apps Explained_**

We’ll bridge the gap between UI and data streams using Combine — so you can respond to real-time updates like a pro (without crying over publishers).

**SwiftUI with Combine: Real-Time Data, Publishers & Subscribers Explained** _This guide will help you master reactive programming in SwiftUI — with real examples, how Combine fits with SwiftUI…_medium.comhttps://medium.com/swift-pal/swiftui-with-combine-real-time-data-publishers-subscribers-explained-c92b653bd196

🧭 Where to Go Next?

Want to keep leveling up your SwiftUI skills? Here are some handpicked reads that pair perfectly with this article:

👉 MVVM in SwiftUI: Explained with Clean Architecture

👉 Mastering State in SwiftUI

👉 SwiftUI Layout Guide (VStack, HStack, Grids)

👉 Forms & Inputs in SwiftUI: Pickers, Toggles, and More

And if you’re building a production-grade app, don’t miss this one:

📦 The Ultimate iOS Article Index

🎉 Enjoyed this article? Your support means the world to me!

💬 Drop a comment below! I love hearing about your experiences and answering questions

🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal

💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal

Happy coding! 🚀

● The newsletter

New articles, straight to your inbox.

No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.

Keep reading