Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
218: Customizing Chart Marks and Axes
When you first start playing with Swift Charts, it's tempting to just throw some data at a BarMark or LineMark and let the framework handle the rest. For a quick prototype, that's fine. But if you're building something that people actually have to use to make decisions, the defaults usually aren't enough. I've seen too many developers ship charts that are technically accurate but visually ambiguous because they relied on the "naive" way of letting the system decide the axis and the colors.
Let's look at a real scenario: a daily water intake tracker. You want to show the user how much they've drunk over the last week compared to a goal of 2,500ml. The naive approach is to just plot the values and let Swift Charts auto-scale the Y-axis. It looks like this:
Chart(waterData) { data in
BarMark(
x: .value("Day", data.day),
y: .value("Amount", data.amount)
)
}
The Problem with "Automatic" Everything
At first glance, this works. But here's where it breaks: if the user drinks 2,400ml every day, the Y-axis might cap exactly at 2,400. The user has no visual reference for their 2,500ml goal. They're staring at a chart where the bars hit the top, but they're still technically failing their goal. It's a classic case of the data being correct, but the communication being wrong.
To fix this, we need to stop treating the axis as a black box. Instead of letting the framework guess, we should explicitly define the scale and customize the marks. By using chartYScale(domain:), we can force the chart to always show up to 3,000ml, ensuring the 2,500ml goal is always visible within the frame. But just fixing the scale isn't enough; the user shouldn't have to squint at the axis to know if they hit their target.
Injecting Meaning into the Marks
The second mistake I see often is using a single, static color for all marks. In our water tracker, a blue bar is just a blue bar. It doesn't tell the user "Success" or "Try Harder." The better way is to tie the foregroundStyle directly to the data logic. I prefer doing this inline within the mark definition to keep the relationship between the value and the visual cue explicit.
Chart(waterData) { data in
BarMark(
x: .value("Day", data.day),
y: .value("Amount", data.amount)
)
.foregroundStyle(data.amount >= 2500 ? .green : .blue)
}
Now we're actually communicating. But we can go further. If you want to be a real pro about it, you don't just want a green bar; you want a clear, labeled line marking that goal. This is where RuleMark comes in. Adding a RuleMark at the 2,500 value creates a visual "finish line" that anchors the entire chart.
Fine-Tuning the Axis for Humans
Finally, let's talk about the axis labels. The default labels are often too verbose or too sparse. If you're dealing with milliliters, you don't want the axis to say "2000.0" or "2k". You want it to feel like a health app. Using AxisMarks allows us to intercept the rendering process. I usually recommend using the value closure to format the numbers into something readable, like adding "ml" as a suffix only to the top-most label to reduce visual clutter.
.chartYAxis {
AxisMarks(values: .automatic) { value in
AxisGridLine()
AxisTick()
AxisValueLabel {
if let amount = value.as(Double.self) {
Text("\(Int(amount))ml")
}
}
}
}
By moving from the naive "plot and pray" method to this intentional approach, you've changed the chart from a simple data visualization into a functional tool. You've controlled the scale to provide context, used color to provide instant feedback, and cleaned up the axes to remove friction for the user.
📋 Practical Task
Exercise: The Performance Threshold Dashboard
You are building a server monitoring tool. You have a dataset of "Response Times" (in milliseconds) for the last 10 requests. The critical threshold for a "Slow Response" is 500ms.
- Create a
ChartusingBarMarkto display the response times. - Custom Scaling: Ensure the Y-axis always extends to 1000ms, regardless of the highest data point.
- Conditional Styling: Set the bar color to
.redif the response time is 500ms or higher, and.grayif it is below. - The Goal Line: Add a
RuleMarkat the 500ms mark with a label that says "Critical Threshold". - Axis Customization: Customize the Y-axis labels so they append "ms" to the values.
There are no comments for now.