How to Make SwiftUI Apps Look Good Without Being a Designer
The practical guide to colors, gradients, and shadows that’ll make your SwiftUI views look professional

Here’s the thing most tutorials won’t tell you .foregroundColor(.blue) is a code smell.
Not always. But most of the time.
SwiftUI ships with semantic colors that adapt to Dark Mode, accessibility settings, and platform conventions automatically. When you hardcode Color.blue or Color(red: 0.2, green: 0.4, blue: 0.8), you're fighting the system instead of working with it.
Semantic colors look like this:
Text("Hello")
.foregroundColor(.primary)
Text("Subtitle")
.foregroundColor(.secondary).primary is black in Light Mode, white in Dark Mode. .secondary is gray, but the right shade of gray for each mode. You don't have to think about it.
Here’s the full list of semantic colors you should know:
.primary- main text.secondary- less important text.tertiary- even less important.accentColor- your app's brand color (set globally)
For backgrounds:
VStack {
Text("Content")
}
.background(Color(.systemBackground))Why the extra Color() wrapper? Because UIColor.systemBackground exists in UIKit and SwiftUI bridges to it. These give you the correct background colors for different contexts - primary backgrounds, secondary backgrounds (like grouped list cells), tertiary backgrounds.
You can also use .background directly as a color in iOS 15+:
.background(.background)Yes that reads weird. But it works.
Now when DO you hardcode colors? Brand colors, accent colors, specific design requirements. Like if your app’s theme is purple:
Button("Sign Up") {
// action
}
.buttonStyle(.borderedProminent)
.tint(.purple)Or define custom colors in an asset catalog and reference them:
Color.brandPurple // from Assets.xcassetsSwiftUI automatically converts asset catalog color names to properties (camelCase, starting with lowercase). So “BrandPurple” becomes .brandPurple. This way you can still define Light/Dark variants in the asset catalog and get automatic adaptation.
Quick tip: use .foregroundStyle() instead of .foregroundColor() in iOS 15+. It's more flexible and works with gradients:
Text("Gradient Text")
.foregroundStyle(
LinearGradient(colors: [.blue, .purple],
startPoint: .leading,
endPoint: .trailing)
)Can’t do that with .foregroundColor().
🌈 Gradient Styles — Linear, Radial, Angular
Gradients are where SwiftUI apps go from “I made this” to “okay this actually looks kinda nice.”
There are three types: linear, radial, and angular (sometimes called conic).
Linear gradients go in a straight line:
Rectangle()
.fill(
LinearGradient(
colors: [.blue, .purple],
startPoint: .top,
endPoint: .bottom
)
)Top to bottom, blue fading to purple. You can use .leading/.trailing for horizontal, or .topLeading/.bottomTrailing for diagonal.
Radial gradients radiate from a center point:
Circle()
.fill(
RadialGradient(
colors: [.yellow, .orange, .red],
center: .center,
startRadius: 20,
endRadius: 100
)
)
Start radius is where the first color begins, end radius is where the last color ends. Great for sun effects, spotlight effects, that sort of thing.
Angular gradients sweep around a circle:
Circle()
.fill(
AngularGradient(
colors: [.red, .yellow, .green, .blue, .purple, .red],
center: .center
)
)
Notice the .red at both ends? That makes it loop smoothly. Without it you get a harsh seam. Angular gradients are perfect for loading spinners, color wheels, or that rainbow effect everyone loves.
Here’s a practical example — a card with a subtle gradient background:
VStack(alignment: .leading, spacing: 8) {
Text("Premium Feature")
.font(.headline)
Text("Unlock advanced analytics")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding()
.frame(maxWidth: .infinity)
.background(
LinearGradient(
colors: [Color.purple.opacity(0.6), Color.blue.opacity(0.4)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.cornerRadius(12)
The .opacity() modifier makes it subtle. Full opacity gradients usually look too aggressive unless you're going for that vaporwave aesthetic.
You can also use more than two colors:
LinearGradient(
colors: [.pink, .purple, .blue, .cyan],
startPoint: .topLeading,
endPoint: .bottomTrailing
)SwiftUI distributes them evenly. If you need precise control over color stops, there’s a gradient initializer that takes Gradient.Stop objects, but honestly? The simple version works 95% of the time.
One gotcha: gradients as view backgrounds vs as fills behave slightly different. With .background(), the gradient sizes to the view. With .fill() on shapes, it sizes to the shape's frame.
✨ Layering Gradients & Overlays
Okay so single gradients are cool but layering them is where things get interesting.
Basic technique: stack gradients with .overlay() or .background() and adjust opacity:
Rectangle()
.fill(
LinearGradient(
colors: [.blue, .purple],
startPoint: .top,
endPoint: .bottom
)
)
.overlay(
LinearGradient(
colors: [.white.opacity(0.3), .clear],
startPoint: .top,
endPoint: .center
)
)This gives you a two-layer gradient — the main blue-to-purple, plus a white shine effect on top. It’s subtle but makes things look way more polished. Check difference 👇

Another trick — use .blendMode() to change how layers combine:
ZStack {
LinearGradient(colors: [.orange, .red],
startPoint: .top,
endPoint: .bottom)
LinearGradient(colors: [.yellow, .clear],
startPoint: .topLeading,
endPoint: .bottomTrailing)
.blendMode(.screen)
}
.frame(height: 200)
.screen blend mode makes the yellow gradient lighten the orange/red instead of just covering it. Other useful blend modes: .multiply (darkens), .overlay (enhances contrast), .softLight (subtle glow).
You can also layer gradients with solid color overlays for interesting effects:
Image("photo")
.resizable()
.aspectRatio(contentMode: .fill)
.overlay(
LinearGradient(
colors: [.clear, .black.opacity(0.7)],
startPoint: .center,
endPoint: .bottom
)
)
.overlay(
VStack {
Spacer()
Text("Photo Caption")
.foregroundColor(.white)
.padding()
}
)This is that Instagram-style gradient overlay that makes text readable over photos. Dark gradient at the bottom, text on top of that. Works every time.
Here’s a card design combining multiple techniques:
VStack(alignment: .leading) {
Text("Featured")
.font(.caption)
.foregroundColor(.white.opacity(0.8))
Text("New Release")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.white)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: 150)
.background(
ZStack {
LinearGradient(colors: [.purple, .blue],
startPoint: .topLeading,
endPoint: .bottomTrailing)
Circle()
.fill(RadialGradient(colors: [.white.opacity(0.2), .clear],
center: .topTrailing,
startRadius: 0,
endRadius: 150))
.offset(x: 50, y: -50)
}
)
.cornerRadius(16)
Main gradient background, radial gradient “light source” in the corner, text on top. Three layers, looks way fancier than the code suggests.
Pro tip: use .compositingGroup() when layering gets weird. Sometimes SwiftUI's rendering order causes transparency issues - wrapping in .compositingGroup() forces it to render as one unit:
VStack {
// complex layered content
}
.compositingGroup()
.shadow(radius: 10)
💎 Shadow Techniques That Don’t Look Ugly
Default SwiftUI shadows are… not great. They’re too sharp, too dark, too obviously computer-generated.
Here’s the basic shadow:
RoundedRectangle(cornerRadius: 12)
.fill(.white)
.shadow(radius: 10)It works but it’s harsh. The trick to good shadows? Layer multiple subtle shadows instead of one strong shadow.
Two shadows — one tight and close, one softer and further. This mimics how real shadows work. The first is the contact shadow, the second is the ambient shadow.
For cards and elevated UI:
.shadow(color: .black.opacity(0.08), radius: 2, x: 0, y: 2)
.shadow(color: .black.opacity(0.12), radius: 8, x: 0, y: 4)Those opacity values matter. .opacity(0.3) is too dark for modern iOS. Stick to 0.05-0.15 range.
Colored shadows for glowing effects:
Text("Glow")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.cyan)
.shadow(color: .cyan.opacity(0.5), radius: 10, x: 0, y: 0)
.shadow(color: .cyan.opacity(0.3), radius: 20, x: 0, y: 0)
Two layers again, same color as the text, high radius for that neon glow. Don’t use x/y offset for glows — you want the shadow centered.
Here’s a realistic button with proper shadows:
Button(action: {}) {
Text("Press Me")
.fontWeight(.semibold)
.foregroundColor(.white)
.padding(.horizontal, 24)
.padding(.vertical, 12)
.background(
LinearGradient(colors: [.blue, .purple],
startPoint: .leading,
endPoint: .trailing)
)
.cornerRadius(10)
}
.shadow(color: .purple.opacity(0.3), radius: 8, x: 0, y: 4)
.shadow(color: .purple.opacity(0.2), radius: 16, x: 0, y: 8)
Colored shadows matching the button color, layered for depth, modest opacity. Looks miles better than .shadow(radius: 5).
One warning: too many shadows tanks performance. If you’re rendering a List with 100 cells and each cell has 3–4 shadows, scrolling will stutter. For lists, stick to one shadow or use dividers instead.
🎭 Dark Mode & Color Adaptivity
Dark Mode isn’t optional anymore. And the lazy way — checking @Environment(\.colorScheme) everywhere - is a pain.
Better approach: use adaptive colors from the start.
SwiftUI’s semantic colors already adapt:
Text("Adapts automatically")
.foregroundColor(.primary) // black in light, white in dark
.padding()
.background(Color(.secondarySystemBackground)) // adaptsNo if colorScheme == .dark needed.
For custom colors, define them in an asset catalog with Light/Dark variants. Then use them like:
Color.cardBackgroundThe asset catalog handles the switching. Way cleaner than code-based checks.
But what if you need code-based adaptation? Use the environment:
@Environment(\.colorScheme) var colorScheme
var adaptiveGradient: LinearGradient {
colorScheme == .dark
? LinearGradient(colors: [.gray, .black],
startPoint: .top,
endPoint: .bottom)
: LinearGradient(colors: [.white, .gray.opacity(0.3)],
startPoint: .top,
endPoint: .bottom)
}Then use adaptiveGradient in your views. It updates automatically when the user switches modes.
Shadows in Dark Mode need different opacity:
.shadow(
color: colorScheme == .dark
? .white.opacity(0.05)
: .black.opacity(0.1),
radius: 10
)Dark Mode shadows should be lighter and subtler. Heavy black shadows on dark backgrounds look weird.
Here’s a card that adapts properly:
struct AdaptiveCard: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
VStack(alignment: .leading) {
Text("Adaptive Card")
.font(.headline)
Text("Changes with Dark Mode")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(12)
.shadow(
color: colorScheme == .dark
? Color.white.opacity(0.03)
: Color.black.opacity(0.1),
radius: 10, x: 0, y: 5
)
}
}Background uses system color, text uses semantic colors, shadow adapts. Everything just works.
One gotcha with gradients — they don’t automatically adapt. You’ll need to define separate gradients for Light/Dark if the same gradient doesn’t work in both modes:
var backgroundGradient: some View {
Group {
if colorScheme == .dark {
LinearGradient(colors: [.black, Color(white: 0.2)],
startPoint: .top,
endPoint: .bottom)
} else {
LinearGradient(colors: [Color(white: 0.95), .white],
startPoint: .top,
endPoint: .bottom)
}
}
}Annoying but necessary for good Dark Mode support.
Testing tip: use Xcode’s Environment Overrides to toggle Dark Mode while previewing. Look for the environment overrides button in the preview canvas. Way faster than changing system settings.
✅ Wrap-up
SwiftUI’s styling system is more powerful than most tutorials show. Semantic colors give you Dark Mode for free. Layered gradients add depth without custom graphics. Multi-layer shadows look professional instead of harsh.
Quick reference:
Colors:
- Use
.primary/.secondaryfor text - Use
Color(.systemBackground)for adaptive backgrounds - Define custom colors in asset catalogs, not code
Gradients:
- Linear for directional fades
- Radial for spotlights and glows
- Angular for circular patterns
- Layer with
.overlay()and.blendMode()for depth
Shadows:
- Layer 2–3 subtle shadows instead of one strong shadow
- Keep opacity between 0.05–0.15 for modern look
- Adjust opacity for Dark Mode
- Use colored shadows for glowing effects
Dark Mode:
- Semantic colors adapt automatically
- Asset catalog colors handle Light/Dark variants
- Gradients and shadows need manual adaptation
- Test with Environment Overrides in Xcode
The real skill is knowing when NOT to add gradients and shadows. Sometimes a flat color with good typography is better than a gradient with three shadows. But when you need visual polish, these techniques will get you 90% of the way there without needing a designer.
📺 Want to see all of this in action with live examples? Check out the Swift Pal YouTube channel at https://youtube.com/@swift-pal for video tutorials on SwiftUI styling and iOS development.
Now go make your app look less like programmer art.
🎉 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! 🚀
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
Why I'm Rebuilding My Blog From Scratch (and Leaving Medium)
After years of publishing on someone else's platform, I'm moving my writing to a home I actually own. Here's the reasoning, and what I'm building instead.
ReadData Is the Model: The Most Ignored Part of AI
A beginner-friendly guide to why data quality beats model hype.
Read