iOS 17 กับการมาของ Charts Framwork ที่ไม่ต้องใช้ผ่าน Cocoapods อีกต่อไป

Sitthiphong Kanhasura
Lotus’s IT
Published in
4 min readDec 28, 2023

อย่างที่ทุกคนรู้กันว่า Charts นั้นสำคัญสำหรับงาน Data visualization ไม่ว่าจะเป็น Bar chart, Pie chart หรือ Donut chart

ซึ่งชาว iOS Developer จะสร้าง Charts แต่จะทีก็ต้องคอยมา Custom Charts เขียนกันเองอย่างการใช้ Path เข้ามาช่วย หรือการใช้ Third Party อย่าง Cocoapods — Charts นี่แหละ แต่ภายในงาน WWDC 2023 ได้มีการปล่อย Pie Chart ออกมาอย่างเป็นทางการให้ชาว iOS เรานำไปเล่นกันอย่างง่าย ๆ

Charts Photo — on Unsplash

ในบทความนี้ เราจะมาลองเล่น Pie Chart และ Donut Chart กัน โดยใช้ SwiftUI และมาเรียนรู้เกี่ยวกับ Interactivity ใน Swift Charts ด้วยเช่นกัน

Prepare data

ก่อนอื่นเรามาเตรียม Data อย่างง่าย ๆ สำหรับ Charts framework กันก่อน โดยการ Create new project ขึ้นมาจากนั้นเพิ่มโค้ดส่วนนี้ลงไป

struct Donut {
let id = UUID().uuidString
let name: String
let count: Int
}

struct ContentView: View {
private var donuts: [Donut] = [
Donut(name: "HONEY DIPPED", count: 12),
Donut(name: "PEANUT", count: 30),
Donut(name: "TRIO", count: 29),
Donut(name: "BLUEBERRY SHELL", count: 7),
Donut(name: "PON DE RING", count: 82),
Donut(name: "TWIST", count: 54)
]

var body: some View { ... }
}

Pie Charts

ในการสร้าง Pie Chart ต้องใช้คำสั่ง SectorMark to represent Pie Chart UI

var body: some View {
NavigationStack {
VStack {
Chart {
ForEach(donuts, id: \.name) { donut in
SectorMark(
angle: .value("Donut", donut.count)
)
.foregroundStyle(by: .value("Type", donut.name))
}
}
.frame(height: 500)
}
.padding()
.navigationTitle("DONUTS")
}
}

ตัว angle parameter ของ SectorMark จะถูกคำนวณและนำมาวาดเป็น pie chartโดยใช้ค่าจาก donut.count

initial chart view

Customizing the Pie Chart

เส้นกั้นระหว่าง Sector

SectorMark มีจำนวน parameters ที่สามารถ custom ของแต่ละ sector ได้ โดยการเพิ่มช่องว่างระหว่าง sectors โดยการใช้ angularInset

SectorMark(
angle: .value("Donut", donut.count),
angularInset: 4
)
add angularInset parameter

กำหนดขนาด Sector ที่ต้องการ

เราสามารถเพิ่มขนาดของแต่ละ sectors โดยใช้ outerRadius parameter.

ตัวอย่างเช่น ถ้าเราต้องไฮไลท์ Trio Donut Sector ให้มีขนาดที่ใหญ่ขึ้น สามารถทำได้โดยการใส่ outerRadius parameter.

SectorMark(
angle: .value("Donut", donut.count),
outerRadius: donut.name == "TRIO" ? 180 : 150,
angularInset: 4
)
add outerRadius parameter

ใส่ Label แต่ให้แต่ละ Sector

การเพิ่ม Label ให้แต่ละ sector เราสามารถใช้ annotation modifier ให้ SectorMark โดยการ set position เป็น .overlay

SectorMark(
angle: .value("Donut", donut.count),
outerRadius: donut.name == "TRIO" ? 180 : 150,
angularInset: 4
)
.foregroundStyle(by: .value("Name", donut.name))
.annotation(position: .overlay) {
Text("\(donut.count)")
.font(.title)
.foregroundStyle(.white)
}
add annotation modifier of SectorMark

Donut Chart

จากด้านบนเราทำการสร้าง Pie Chart กันเสร็จเรียบร้อยแล้ว เราสามารถเปลี่ยนจาก Pie Chart เป็น Donut Chart ได้อย่างง่าย ๆ โดยการใช้ innerRadius parameter ของ SectorMark เพียง 1 บรรทัดก็จะเปลี่ยนจาก Pie Chart เป็น Donut Chart ได้ทันที

SectorMark(
angle: .value("Donut", donut.count),
innerRadius: .ratio(0.6),
outerRadius: donut.name == "TRIO" ? 180 : 150,
angularInset: 4
)
.foregroundStyle(by: .value("Name", donut.name))
.annotation(position: .overlay) {
Text("\(donut.count)")
.font(.title)
.foregroundStyle(.white)
}
add innerRadius parameter

หรือเราสามารถใช้ cornerRadius modifier ให้ Sector เพื่อ round the corners ได้

SectorMark(
angle: .value("Donut", donut.count),
innerRadius: .ratio(0.6),
outerRadius: donut.name == "TRIO" ? 180 : 150,
angularInset: 4
)
.foregroundStyle(by: .value("Name", donut.name))
.cornerRadius(10)
.annotation(position: .overlay) {
Text("\(donut.count)")
.font(.title)
.foregroundStyle(.white)
}
add cornerRadius modifier

สุดท้ายเราสามารถเพิ่ม Chart Background ได้โดยการใช้ chartBackground modifier ให้ Chart

Chart {
...
}
.frame(height: 500)
.chartBackground(content: { _ in
Image(systemName: "moon.dust")
.resizable()
.frame(width: 100, height: 100)
})
add chartBackground modifier to Chart

Interacting with Charts

การเพิ่ม Interaction ให้ทั้ง Pie Chart และ Donut Chart ใน new swiftUI version โดยการใช้ chartAngleSelection modifier ให้ Chart เป็น binding เมื่อ User กดไปที่ Sector และใช้ onChange modifier เพิ่มจับค่าของ chart angle value

@State private var selectedCount: Int?
...

Chart {
...
}
.frame(height: 500)
.chartAngleSelection(value: $selectedCount)
.onChange(of: selectedCount) { (oldValue, newValue) in
if let newValue {
print(newValue)
}
}
add interaction with chart

สังเกต Donut Chart นี้ใช้ค่าของ donut.count มา Plot เป็น Chart ดังนั้นเมื่อกดที่ Sector จะทำการ print newValue ออกมาซึ่งค่าของจุดแรกเริ่ม หรือค่าที่ value = 0 จะอยู่ที่ 00:00 นาฬิกา แล้ววนตามเข็มนาฬิกาไปจนถึง 11:59 ก็คือสุดสิ้นสุด หรือค่าที่ value = ค่าของ donut.count ทั้งหมดมาบวกกัน (value = 214)

newValue when click on Sector

ในการจะหาค่า chart angle value ว่าเป็นของ Sector ไหนนั้นเราจำเป็นต้องเขียน function เพื่อคำนวณหาค่าเอง

private func findSectorSelected(by value: Int) -> String? {
var sum = 0
let donut = donuts.first() { donut in
sum += donut.count
return value <= sum
}
return donut?.name
}

เมื่อได้ function ในการหา Sector name แล้วต่อไปก็ประกาศตัวแปรเพื่อรับค่าที่เราคลิก Sector นั้น ๆ

@State private var selectedSector: String?

เมื่อ Sector ถูก hold ตัว selectedSector ก็จะถูก Highlight ตามที่เราเลือก

.onChange(of: selectedCount) { (_, newValue) in
if let newValue {
selectedSector = findSectorSelected(by: newValue)
} else {
selectedSector = nil
}
}

ส่วน Sector ที่เราไม่ได้เลือกก็จะถูก fade ให้สีจางลงนั่นเอง

SectorMark(
...
)
.opacity(selectedSector == nil ? 1.0 : (selectedSector == donut.name ? 1.0 : 0.3))

เย้! จบแล้วสำหรับบทความ Chart Framwork หวังว่าทุกท่านจะสนุกกับการเรียนรู้เรื่อง Chart กันนะครับ

ถ้าหากผิดพลาดประการใดหรือผมเขียนผิดยังไงต้องอภัยล่วงหน้าด้วยนะครับ ถ้าผู้อ่านหากมีคำแนะนำอย่างใดสามารถ Comment ไว้ได้เลย

“ แล้วพบกันใหม่ในบทความต่อไปเด้อครับโผ้ม “

--

--