在 SwiftUI 中組合矩形來創建條形圖是比較容易的。SwiftUI 是一個很好的平臺,用于創建視圖和快速重構獨立的子視圖。在 SwiftUI 中構建條形圖需要做一些工作,隨著使用數據來試用條形圖,可以確定更多的定制化。
?前言
條形圖以矩形條的形式呈現數據的類別,其寬度和高度與它們表示的值成比例。本文將展示如何創建一個垂直條形圖,其中矩形的高度將代表每個類別的值。
開始圖表布局
SwiftUI 對探索不同布局和預覽實時視圖結果是很友好的。很容易將部分內容提取到子視圖中,以便每個部分都很小且易于維護。從將包含 BarChartView? 以及可能的其他文本或數據的視圖開始。這個 BarChartView 包含一個標題和一個圖表區,它們由文本和圓角矩形表示。
struct ChartView1: View {
var body: some View {
VStack {
Text("Sample Bar Chart")
.font(.title)
BarChartView(
title: "the chart title")
.frame(width: 300, height: 300, alignment: .center)
Spacer()
}
}
}
struct BarChartView: View {
var title: String
var body: some View {
GeometryReader { gr in
let headHeight = gr.size.height * 0.10
VStack {
ChartHeaderView(title: title, height: headHeight)
ChartAreaView()
}
}
}
}
struct ChartHeaderView: View {
var title: String
var height: CGFloat
var body: some View {
Text(title)
.frame(height: height)
}
}
struct ChartAreaView: View {
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 5.0)
.fill(Color(#colorLiteral(red: 0.8906477705, green: 0.9005050659, blue: 0.8208766097, alpha: 1)))
}
}
}

圖表區添加條形圖
定義一些簡單的數據類別,例如一周內每天的步數。以下列表數據被作為主視圖的項目數據,每一條數據包含一個對(名稱,值)。在真正的 app 里,這里的數據應該通過 ViewModel 從 model 里取數據。
每日步數數據
Day
| Steps
|
Mon
| 898
|
Tue
| 670
|
Wed
| 725
|
Thu
| 439
|
Fri
| 1232
|
Sat
| 771
|
Sun
| 365
|
struct DataItem: Identifiable {
let name: String
let value: Double
let id = UUID()
}
struct ChartView2: View {
let chartData: [DataItem] = [
DataItem(name: "Mon", value: 898),
DataItem(name: "Tue", value: 670),
DataItem(name: "Wed", value: 725),
DataItem(name: "Thu", value: 439),
DataItem(name: "Fri", value: 1232),
DataItem(name: "Sat", value: 771),
DataItem(name: "Sun", value: 365)
]
var body: some View {
VStack {
Text("Sample Bar Chart")
.font(.title)
BarChartView(
title: "Daily step count", data: chartData)
.frame(width: 350, height: 500, alignment: .center)
Spacer()
}
}
}
更新 BarChartView? 使數據可以作為參數傳遞到 ChartAreaView
struct BarChartView: View {
var title: String
var data: [DataItem]
var body: some View {
GeometryReader { gr in
let headHeight = gr.size.height * 0.10
VStack {
ChartHeaderView(title: title, height: headHeight)
ChartAreaView(data: data)
}
}
}
}
更新后的 BarChartView? 需要一個 DataItem? 的列表。GeometryReader 被用來確定條形圖的可用高度。數據中的最大值得到后并傳遞給每個 BarView?。主圖表區域保持原來的圓角矩形,并以水平堆疊的方式疊加一系列條形,每個 DataItem 一個。
struct ChartAreaView: View {
var data: [DataItem]
var body: some View {
GeometryReader { gr in
let fullBarHeight = gr.size.height * 0.90
let maxValue = data.map { $0.value }.max()!
ZStack {
RoundedRectangle(cornerRadius: 5.0)
.fill(Color(#colorLiteral(red: 0.8906477705, green: 0.9005050659, blue: 0.8208766097, alpha: 1)))
VStack {
HStack(spacing:0) {
ForEach(data) { item in
BarView(
name: item.name,
value: item.value,
maxValue: maxValue,
fullBarHeight: Double(fullBarHeight))
}
}
.padding(4)
}
}
}
}
}
為 BarView 創建一個新的試圖,該視圖為每條數據創建一個條形圖。它需要每一條數據的名稱和值以及最大值和可用的條形高度。每個條形圖都表示為圓角矩形,條形高度相對于最大條形高度設置。條形的顏色設置為純藍色。
struct BarView: View {
var name: String
var value: Double
var maxValue: Double
var fullBarHeight: Double
var body: some View {
let barHeight = (Double(fullBarHeight) / maxValue) * value
VStack {
Spacer()
ZStack {
VStack {
Spacer()
RoundedRectangle(cornerRadius:5.0)
.fill(Color.blue)
.frame(height: CGFloat(barHeight), alignment: .trailing)
}
VStack {
Spacer()
Text("\(value, specifier: "%.0F")")
.font(.footnote)
.foregroundColor(.white)
.fontWeight(.bold)
}
}
Text(name)
}
.padding(.horizontal, 4)
}
}

屏幕旋轉
條形圖在使用樣本數據時看起來不錯。圖表會調整到適合它所處的容器視圖之中。同樣的圖表可以放到任何沒有其他視圖的新試圖上,當設備旋轉時,圖標將會充滿空間并調整大小。
struct ChartView3: View {
var body: some View {
VStack() {
BarChartView(
title: "Daily step count", data: chartData)
Spacer()
}
.padding()
}
}

手機旋轉時顯示的圖表
真實數據的條形圖
給條形圖使用真實世界的數據。聯合國兒童基金會數據集中五歲以下兒童死亡率最高的十個國家。
五歲以下兒童死亡率:
指從出生到五歲之間死亡的概率,每1000名活產嬰兒
2019年特定國家五歲以下兒童死亡率估計數
ISO Code
| Country Name
| 2019
|
NGA
| Nigeria
| 117.2
|
SOM
| Somalia
| 116.9
|
TCD
| Chad
| 113.7
|
CAF
| Central African Republic
| 110.0
|
SLE
| Sierra Leone
| 109.2
|
GIN
| Guinea
| 98.8
|
SSD
| South Sudan
| 96.2
|
MLI
| Mali
| 94.0
|
BEN
| Benin
| 90.2
|
BFA
| Burkina Faso
| 87.5
|
LSO
| Lesotho
| 86.4
|
可以看出,國家名稱比示例數據中一周中的幾天使用多個數據名稱要長的多。數據使用國家名稱在條形圖中繪制。
struct ChartView4: View {
let chartData: [DataItem] = [
DataItem(name: "Nigeria", value: 117.2),
DataItem(name: "Somalia", value: 116.9),
DataItem(name: "Chad", value: 113.7),
DataItem(name: "Central African Republic", value: 110.0),
DataItem(name: "Sierra Leone", value: 109.2),
DataItem(name: "Guinea", value: 98.8),
DataItem(name: "South Sudan", value: 96.2),
DataItem(name: "Mali", value: 94.0),
DataItem(name: "Benin", value: 90.2),
DataItem(name: "Burkina Faso", value: 87.5)
]
var body: some View {
VStack() {
BarChartView(
title: "Under Five Mortality Rates in 2019", data: chartData)
.frame(width: 350, height: 500, alignment: .center)
Text("Under-five mortality rate:")
Text("is the probability of dying between birth and exactly 5 years of age, expressed per 1,000 live births.")
Spacer()
}
.padding()
}
}
這里對 BarView 做出了一些改動。條形圖上的值使用疊加視圖修改移到了條形圖的頂部。這個值是偏移的,所以文本不會離條形圖的頂部太近。數據名稱的字體大小和字重也可以被設置。向國家名稱那樣較長的文本,顯示出條形圖下面的文本將條形圖推到了線外。文本視圖的寬度被限制在條形圖寬度的范圍內,而且條形圖的標簽文本會被截斷,條形圖的文本視圖也被限制在條形寬度的范圍內,并且文本可以被隱藏起來。
struct BarView: View {
var name: String
var value: Double
var maxValue: Double
var fullBarHeight: Double
var body: some View {
GeometryReader { gr in
let barHeight = (Double(fullBarHeight) / maxValue) * value
let textWidth = gr.size.width * 0.80
VStack {
Spacer()
RoundedRectangle(cornerRadius:5.0)
.fill(Color.blue)
.frame(height: CGFloat(barHeight), alignment: .trailing)
.overlay(
Text("\(value, specifier: "%.0F")")
.font(.footnote)
.foregroundColor(.white)
.fontWeight(.bold)
.frame(width: textWidth)
.offset(y:10)
,
alignment: .top
)
Text(name)
.font(.system(size: 11))
.fontWeight(.semibold)
.lineLimit(1)
.frame(width: textWidth)
}
.padding(.horizontal, 4)
}
}
}

所有的國家名稱都被截斷了,所以將數據更代為使用國家碼而不是國家名稱。圖標被設置為固定大小,視圖被嵌入到 ScrollView 中,以便在設備旋轉時滾動。
struct ChartView5: View {
let chartData: [DataItem] = [
DataItem(name: "NGA", value: 117.2),
DataItem(name: "SOM", value: 116.9),
DataItem(name: "TCD", value: 113.7),
DataItem(name: "CAF", value: 110.0),
DataItem(name: "SLE", value: 109.2),
DataItem(name: "GIN", value: 98.8),
DataItem(name: "SSD", value: 96.2),
DataItem(name: "MLI", value: 94.0),
DataItem(name: "BEN", value: 90.2),
DataItem(name: "BFA", value: 87.5)
]
var body: some View {
ScrollView {
VStack() {
BarChartView(
title: "Countries with the highest Under Five Mortality Rates in 2019", data: chartData)
.frame(width: 350, height: 500, alignment: .center)
Spacer().frame(height:20)
VStack() {
Text("Under-five mortality rate:")
.font(.system(.title2, design:.rounded))
.fontWeight(.bold)
Text("is the probability of dying between birth and exactly 5 years of age, expressed per 1,000 live births.")
.font(.body)
}
.frame(width: 300, height: 130)
.background(Color(#colorLiteral(red: 0.8906477705, green: 0.9005050659, blue: 0.8208766097, alpha: 1)))
.cornerRadius(10)
Spacer()
}
.padding()
}
}
}

結語
在 SwiftUI 中組合矩形來創建條形圖是比較容易的。SwiftUI 是一個很好的平臺,用于創建視圖和快速重構獨立的子視圖。在 SwiftUI 中構建條形圖需要做一些工作,隨著使用數據來試用條形圖,可以確定更多的定制化。使用 GeometryReader 可以創建適應更多可用環境的條形圖。在這篇文章中,我們創建了一個簡單的條形圖,有數值,下面有標簽,還有圖表的標題,下一步就是分離出 x 軸和 y 軸。