概要
iOS16以上から、一部のグラフを簡単に作成できるフレームワークであるChartsが使用できるようになり、さらにiOS17以上からは、円グラフも対応できるようになりデータの視認性を良くできるようになりました!
そんなChartsを使用してみたいとかねてから考えていたので、今回は「エンジニアの構成比率を表示する」という題材でデータを可視化するためにサンプルアプリを実装してみました!最低限グラフを表示できるよう雑に解説していきます!
以下のサンプル動画を作成してみたのでぜひ確認してみてください!
説明すること
- ChartsのChart構造体について
- SectorMark(iOS17以上)、BarMark、PointMark、LineMarkについて
- Chartsを使用する上で、考慮する点について
説明しないこと
- 各データに対する効果的なグラフの種類について
- チャートの細かいモディファイアの使い分けについて
- アクションによるグラフの表示を変化させる方法について
環境
macOS 14.1.2(23B92)
Xcode Version 15.0.1 (15A507)
iOS(simulater) 17.0.1
Chartsについて
Chartsとは、どのAppleのプラットフォームでも使用できる、チャートやグラフを作成することのできるApple純正のフレームワークです。自前のデータを視認性の良いチャートやグラフにすることを目的にしているフレームワークです!
純正のChartsができるまでにサードパーティ製のMPAndroidChart Documentationという外部のフレームワーク等も存在していたそうです。(筆者は使用したことがないので説明は割愛させていただきます。)
使用方法としては、Chart
という構造体内に、円グラフを表示する構造体や棒グラフや折れ線グラフを表示するための構造体を定義して、画面にグラフを表示します。
Chartsには、以下のような初期化の状態を定義されているので、Identifirbleに準拠したデータクラスもしくは、配列等を使用して要素をグラフに落とし込んでいきます!
使用していた感覚としては、ForEach
に似ていると感じました。
public struct Chart<Content> : View where Content : ChartContent {
public init(@ChartContentBuilder content: () -> Content)
public init<Data, C>(_ data: Data, @ChartContentBuilder content: @escaping (Data.Element) -> C) where Content == ForEach<Data, Data.Element.ID, C>, Data : RandomAccessCollection, C : ChartContent, Data.Element : Identifiable
public init<Data, ID, C>(_ data: Data, id: KeyPath<Data.Element, ID>, @ChartContentBuilder content: @escaping (Data.Element) -> C) where Content == ForEach<Data, ID, C>, Data : RandomAccessCollection, ID : Hashable, C : ChartContent
public var actualBody: some View { get }
@MainActor public var body: some View { get }
public typealias Body = some View
}
SectorMark
SectorMarkでは、円グラフを作成することができて以下のように引数にangle: .value("count", $0.numberOfEnginners)
を持たせて、第一引数は、表示する値の単位、第二引数には実際に値を入れることで円グラフを生成できます!
angle
以外にも、innerRadius
やouterRadius
、angularInset
等で円グラフの見せ方を変更できます!
サンプルコード
import SwiftUI
import Charts
struct CircleGraphView: View {
let geometry: GeometryProxy
@Binding var engineerModelList: [NumberOfEngineersModel]
var body: some View {
Chart(engineerModelList) {
SectorMark(
angle: .value("count", $0.numberOfEnginners)
)
.foregroundStyle(by: .value("engineerType", $0.engineerType.rawValue))
}
.frame(width: geometry.size.width * 0.8, height: geometry.size.width * 0.8)
// 円グラフの下に表示されている項目を非表示にすることができる
// .chartLegend(.hidden)
}
}
BarMark
BarMarkは、棒グラフを生成するために使用できるChartsのパーツです。BarMarkをすようするときは、BarMark(x: .value(), y: .value())
のような形でx軸とy軸を設定して、その値をChartパーツから取得して画面を生成しています。
他には、xStart
やyStart
、xEnd
、yEnd
という引数を初期化時に選べるのでこちらも初期化するタイミングで変更できます。
.annotation
等を使用し、平均値の横線や縦線を表示したりしてより見やすいグラフにできるようです。
サンプルコード
import SwiftUI
import Charts
struct BarMarkGraphView: View {
let geometry: GeometryProxy
@Binding var engineerModelList: [NumberOfEngineersModel]
var body: some View {
Chart(engineerModelList) {
BarMark(x: .value("EnginnerType", $0.engineerType.rawValue),
y: .value("NumberOfEnginners", $0.numberOfEnginners))
}
.frame(width: geometry.size.width * 0.8, height: geometry.size.width * 0.8)
}
}
PointMark
PointMarkとは、与えられた値に点を生成してくれます。こちらもChart内でPointMark
を定義して、ただデータを入れれば完成します。
分布図を実装でどのように使用するかは理解しきれていませんが、次に紹介するLineMarkと一緒に使用すれば、折れ線グラフの強調として使用できます。
サンプルコード
import SwiftUI
import Charts
struct PointMarkGraphView: View {
let geometry: GeometryProxy
@Binding var engineerModelList: [NumberOfEngineersModel]
var body: some View {
Chart(engineerModelList) {
PointMark(x: .value("EnginnerType", $0.engineerType.rawValue),
y: .value("NumberOfEnginners", $0.numberOfEnginners))
}
.frame(width: geometry.size.width * 0.8, height: geometry.size.width * 0.8)
}
}
LineMark
今回最後の、LineMarkは折れ線グラフを生成するために使用します。
Chart内でLineMark
を追加して、x軸とy軸を設定するとこちらも折れ線グラフが完成するので、すぐ実装が可能です。
サンプルコード
import SwiftUI
import Charts
struct LineMarkGraphView: View {
let geometry: GeometryProxy
@Binding var engineerModelList: [NumberOfEngineersModel]
var body: some View {
Chart(engineerModelList) {
LineMark(x: .value("EnginnerType", $0.engineerType.rawValue),
y: .value("NumberOfEnginners", $0.numberOfEnginners))
}
.frame(width: geometry.size.width * 0.8, height: geometry.size.width * 0.8)
}
}
今回実装したCharts以外の全コード
簡単なチャートを表示する実装をしたので、コピペで動くであろうソースコードを貼っておきます!(おそらく動くはずです...)
Model
サンプルコード
import Foundation
/// エンジニア職種とエンジニア以外の人数を管理するためのモデル
struct NumberOfEngineersModel: Identifiable {
/// 代表的なエンジニアの職種を定義しているenumクラスで、基本的に
/// クラスやストラクト等の外から直接使用することはないため、NumberOfEngineersModel内管理
enum EngineerType: String, CaseIterable {
case iOS
case Android
case Backend
case Frontend
case Infra
case QA
case Others
}
let id = UUID()
let engineerType: EngineerType
let numberOfEnginners: Int
init(engineerType: EngineerType = EngineerType.Others, numberOfEnginners: Int = 0) {
self.engineerType = engineerType
self.numberOfEnginners = numberOfEnginners
}
/// グラフ生成のための初期化時に各職種の人数をランダムにセットするための処理で配列で当モデルを返却
static func setRandamNumberOfEngineersList() -> [Self] {
EngineerType.allCases.map { NumberOfEngineersModel(engineerType: $0, numberOfEnginners: Int.random(in: 0...20)) }
}
}
ViewModel
サンプルコード
import Foundation
import Observation
@Observable
final class ContentViewModel {
/// チャートの種類を管理するための列挙型
/// ContentViewとContentViewModelのみで使用するため、クラス内に定義
enum ChartType: CaseIterable {
case circle, barMark, pointMark, lineMark
var titleName: String {
return switch self {
case .circle: "Circle🟢"
case .barMark: "BarMark📊"
case .pointMark: "PointMark✊"
case .lineMark: "LineMark📈"
}
}
}
var engineerModelList: [NumberOfEngineersModel] = [NumberOfEngineersModel()]
var chartType: ChartType = .circle
init() {
setUpMockData()
}
func tappedRefreshedMockData() {
setUpMockData()
}
func tappedOptionBottomBarButton(with chartType: ChartType) {
self.chartType = chartType
}
}
// MARK: - Private
private extension ContentViewModel {
func setUpMockData() {
engineerModelList = NumberOfEngineersModel.setRandamNumberOfEngineersList()
}
}
View
サンプルコード
import SwiftUI
import Charts
struct ContentView: View {
@State private var viewModel = ContentViewModel()
var body: some View {
NavigationStack {
GeometryReader { geometry in
VStack {
Spacer()
HStack {
Spacer()
switch viewModel.chartType {
case .circle:
// 円グラフを表示するための子ビュー(iOS17以上)
CircleGraphView(geometry: geometry, engineerModelList: $viewModel.engineerModelList)
case .barMark:
// 棒グラフを表示するための子ビュー
BarMarkGraphView(geometry: geometry, engineerModelList: $viewModel.engineerModelList)
case .pointMark:
// 点(分布?)グラフを表示するための子ビュー
PointMarkGraphView(geometry: geometry, engineerModelList: $viewModel.engineerModelList)
case .lineMark:
// 折れ線グラフを表示するための子ビュー
LineMarkGraphView(geometry: geometry, engineerModelList: $viewModel.engineerModelList)
}
Spacer()
}
Spacer()
}
}
.toolbar {
ToolbarItem(placement: .bottomBar) {
HStack {
ForEach(0..<ContentViewModel.ChartType.allCases.count, id: \.self) { index in
Button {
let chartType = ContentViewModel.ChartType.allCases[index]
viewModel.tappedOptionBottomBarButton(with: chartType)
} label: {
Text(ContentViewModel.ChartType.allCases[index].titleName)
.font(.system(size: 15))
}
}
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
viewModel.tappedRefreshedMockData()
} label: {
Text("Refresh Data")
}
}
}
.navigationTitle(viewModel.chartType.titleName)
}
}
}
さいごに
時間の関係上かなり駆け足になってしまい、深い内容までは踏み込めていないですが、Charts
を使用してすぐにUIを作成できました。
簡単なデータをただ表示するだけであれば、すぐに実装できてしまいますが、グラフを選択した時の挙動や細かい仕様を実装する場合は、モディファイアを駆使して実装を調節していく必要があります。
また、今回実装していて思ったのは、データの持ち方によってグラフ生成の柔軟性は変わってくるということです。Apple純正のスクリーンタイムアプリのような1日の中でいくつかのカテゴリーの何かを行った時間値をカテゴリーで色分けしてグラフを表示する
ような場合のデータ構造の実装に悩みました!
上記の実装ができるようにサンプルアプリで実装を試してみたいと思います!
あと何気にiOS17以上のObservationを使用していますが、プロパティラッパー(@Published)を毎度書かなくていいのめっちゃ楽ですね!
参考記事