想要在圖表上加手勢,不是一件特別容易的事情,主要原因在於圖表上顯示的各種圖形,因為效率關係,其實已渲染成一張圖,所以手勢是加在整個圖表上,而不是加在每筆資料畫出來的個別圖上。所以我們想要點選圖表上的某個部分,讓該部分換個顏色或加上特別標記,就變的不是那麼容易。WWDC22的影片中有特別針對這部分給出範例,但說實在不是很容易懂,而且若座標軸的資料是 String 型態時,處理方式又跟給出的範例不一樣。
這裡以長條圖為例,並特別將 x 軸資料設定為 String 型態,來看看如何透過手勢來跟圖表互動。雖然程式碼略多,但處理邏輯是先取得手指在長條圖上的座標,然後取得長條圖上每個長條的左側座標與右側座標,接著判斷手指的座標落在哪一個長條範圍內,當確定手指落在哪一個長條後就重新渲染圖形一次,讓該長條改顏色,或是加點別的東西。
儲存資料的結構如下。
struct Product: Identifiable {
var id = UUID()
var color = Color.blue
var name: String
var count: Int
}
資料內容如下。這裡在變數前面加上 @State,主要目的是為了之後的程式碼可以直接修改每個元素中屬性 color 值。
struct BarChart: View {
@State var products: [Product] = [
.init(name: "A", count: 7),
.init(name: "B", count: 10),
.init(name: "C", count: 12),
.init(name: "D", count: 15),
.init(name: "E", count: 20),
.init(name: "F", count: 23),
.init(name: "G", count: 18),
.init(name: "H", count: 17),
.init(name: "I", count: 15),
.init(name: "J", count: 11),
.init(name: "K", count: 9)
]
先把圖畫出來。
var body: some View {
Chart {
ForEach(products) { item in
BarMark(
x: .value("Name", item.name),
y: .value("Count", item.count)
)
.foregroundStyle(item.color)
}
}
}
接下來要加上手勢,但實際上手勢無法直接加在圖表上,所以必須先在圖表上頭疊一個圖層,圖層中畫上一個透明矩形,矩形大小與位置都跟圖表一模一樣,手勢是加在這個矩形上。然後將矩形放到 GeometryReader 中,其目的就是為了要取得手勢觸發時手指點選在圖表上的座標位置。矩形的 contentShape 修飾器是為了讓手勢可以作用,若沒有這個修飾器,即便加了手勢也不會有反應。
Chart {
...
}
.chartOverlay { chartProxy in
GeometryReader { geoProxy in
Rectangle().fill(.clear).contentShape(Rectangle())
}
}
接下來要在 Rectangle 上增加拖放手勢。變數 xPosition 取得手指在圖表上實際繪圖的位置,也就是扣除了 x、y 座標軸上的標記以及邊線大小…等跟實際圖形無關的部分。接下來的 for 迴圈會計算長條圖上每個長條的左側座標與右側座標,這個就是 positionRange(atX:) 函數的功能。知道每個長條的範圍後,接著檢查手指的位置落在哪一個長條範圍內,確定後改變負責該長條資料的 color 屬性,並將該資料所在的陣列索引值記錄到 coloredIndex 變數中,稍後說明這個變數的宣告方式。
Chart {
...
}
.chartOverlay { chartProxy in
GeometryReader { geoProxy in
Rectangle().fill(.clear).contentShape(Rectangle())
.gesture(DragGesture()
.onChanged { value in
let xPosition = value.location.x - geoProxy[chartProxy.plotAreaFrame].origin.x
// 復原顏色
if let coloredIndex {
products[coloredIndex].color = .blue
}
// 檢查每一個長條的座標是否符合手指的座標,若符合則設定顏色
for index in products.indices {
if let range = chartProxy.positionRange(forX: products[index].name) {
if range.contains(xPosition) {
products[index].color = .orange
coloredIndex = index
break
}
}
}
}
.onEnded { _ in
if let coloredIndex {
products[coloredIndex].color = .blue
}
}
)
}
}
最後宣告 coloredIndex 變數,如下。
@State var coloredIndex: Int? = nil
到此大功告成,執行看看。