案例:创建通用的图表组件库
目标
构建一个可复用的 SwiftUI 图表组件库,支持多种图表类型和高度自定义配置,适用于不同项目需求。
实现步骤
1. 定义基础协议
protocol ChartDataPoint: Identifiable {
var id: UUID { get }
var value: Double { get }
var label: String? { get }
}
protocol ChartStyle {
var colors: [Color] { get }
var lineWidth: CGFloat { get }
var showAxis: Bool { get }
// 其他样式属性...
}
2. 创建通用 ChartContainer 视图
struct ChartContainer<Content: View>: View {
let title: String
let style: ChartStyle
@ViewBuilder let content: () -> Content
var body: some View {
VStack(alignment: .leading) {
Text(title)
.font(.headline)
content()
.chartStyle(style) // 自定义环境值
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color(.systemBackground))
)
}
}
}
3. 实现具体图表组件
可配置的柱状图组件
struct BarChartView<Data: RandomAccessCollection>: View where Data.Element: ChartDataPoint {
let data: Data
let style: BarChartStyle
var body: some View {
GeometryReader { geometry in
HStack(alignment: .bottom, spacing: 2) {
ForEach(data) { item in
VStack {
Rectangle()
.fill(style.color(for: item))
.frame(height: CGFloat(item.value) / maxValue * geometry.size.height)
if style.showLabels {
Text(item.label ?? "")
.font(.caption)
.rotationEffect(.degrees(-45))
}
}
}
}
}
}
private var maxValue: Double {
data.map { $0.value }.max() ?? 1
}
}
4. 使用 ViewBuilder 实现灵活组合
struct CompositeChartView<Primary: View, Secondary: View>: View {
@ViewBuilder let primaryChart: () -> Primary
@ViewBuilder let secondaryChart: () -> Secondary
var body: some View {
VStack {
primaryChart()
secondaryChart()
}
}
}
5. 通过 PreferenceKey 实现智能布局
struct ChartSizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct SizeReportingChart<Content: View>: View {
@ViewBuilder let content: () -> Content
var body: some View {
content()
.background(
GeometryReader { proxy in
Color.clear
.preference(key: ChartSizePreferenceKey.self, value: proxy.size)
}
)
}
}
使用示例
struct FinancialDashboard: View {
let revenueData: [MonthlyData]
let expenseData: [MonthlyData]
var body: some View {
ChartContainer(title: "2023 Financials") {
CompositeChartView {
BarChartView(data: revenueData, style: .revenueStyle)
.frame(height: 200)
} secondaryChart: {
LineChartView(data: expenseData, style: .expenseStyle)
.frame(height: 150)
}
}
}
}
最佳实践
命名规范:
- 使用
Chart后缀命名所有图表组件(如BarChart) - 样式类型使用
Style后缀(如BarChartStyle)
- 使用
文档注释:
/// 通用的柱状图组件 /// - Parameters: /// - data: 遵循 ChartDataPoint 协议的数据集合 /// - style: 柱状图样式配置 /// - Note: 自动根据容器高度计算比例 struct BarChartView: View { ... }版本控制:
- 使用语义化版本(SemVer)管理组件库
- 提供迁移指南应对重大变更
测试覆盖:
class ChartComponentsTests: XCTestCase { func testBarChartRendering() { let testData = [...] let view = BarChartView(data: testData, style: .default) assertSnapshot(of: view, as: .image) } }
扩展建议
- 添加主题系统支持暗黑模式
- 实现图表导出为图片/PDF功能
- 开发交互式预览组件
- 创建Playground实时编辑功能
