はじめに

こんにちは!システム開発部のYです。

みなさんは、チャート(グラフ)を作成する際、フレームワークを使用していますか?
有名なものとしては、「DGCharts (旧:iOS Charts)」などが挙げられますが、最近ではWWDC2022でApple純正のSwift Chartsが発表され、これを利用している方も増えているようですね。
筆者自身はSwift Chartsを利用した経験がないですが、今回プロジェクトでチャートが導入されることに伴い、そこまでカスタマイズの必要ないSwift Chartsを採用することにしました。
使用してみたので、基本的な使い方やカスタマイズの仕方等記事としてまとめたいと思います。

環境

  • Swift Version: 5.9.2
  • Xcode 15.2
  • iOS: 17.2
  • macOS: 14.3.1

目次

Swift Chartsについて

Swift ChartsはWWDC 2022で発表され、SwiftUIで簡単にグラフの描画をすることができるフレームワークです。
https://developer.apple.com/documentation/charts



※ iOSは16から利用可能です。

Swift Chartsの使い方

まず、簡単な棒グラフを表示してみます。表示する内容は、各教科の点数を表示する以下とします。

教科点数
国語80
数学90
英語20
理科50
社会30

まず最初にSwift Chartsを使えるようにするため、importを行います。

import Charts

これで使えるようになりました。次にViewの中でChart{ }を囲み、その中で定義していきます。
とりあえず、国語のみを表示するとします。棒グラフを表示するには、BarMarkを使用します。
.valueの第1引数に文字列キーを指定し、第2引数にパラメーターを指定します。

struct ContentView: View {
    var body: some View {
        
        Chart {
            BarMark(
                x: .value("Name", "国語"),
                y: .value("Score", 80)
            )
        }
        .frame(height: 300)
    }
}


続けて他の棒グラフも追加します。

struct ContentView: View {
    var body: some View {
        
        Chart {
            BarMark(
                x: .value("Name", "国語"),
                y: .value("Score", 80)
            )
            BarMark(
                x: .value("Name", "数学"),
                y: .value("Score", 90)
            )
            BarMark(
                x: .value("Name", "英語"),
                y: .value("Score", 20)
            )
            BarMark(
                x: .value("Name", "理科"),
                y: .value("Score", 50)
            )
            BarMark(
                x: .value("Name", "社会"),
                y: .value("Score", 30)
            )
        }
        .frame(height: 300)
    }
}


表示できました。実装完了です!
ではなく、このままだと冗長で見づらいですし9科目表示したいとなった場合修正する必要があります。

より実践的な使い方

表示するデータを構造体として定義します。
ChartsではForEachのようにループ文を使うことができるため構造体は、Identifiableに準拠させます。
一意のidを持たせます(UUID等)。今回はnameとします。

struct Subject: Identifiable {
    let name: String
    let score: Int
    
    var id: String { name }
}

これを用いて先程のグラフに変更を加えると、以下の書き方ができます。
Chartの中にForEachを定義する方法、または直接Chartにデータを渡す方法があります。

struct ContentView: View {
    var body: some View {
               
        // Chart内にForEachを定義する場合
        Chart {
            ForEach(subjects) { data in
                BarMark(
                    x: .value("Name", data.name),
                    y: .value("Score", data.score)
                )
            }
        }
        .frame(height: 300)

        // 直接Chartにデータを渡す場合
        Chart(subjects) { element in
            BarMark(
                x: .value("Name", element.name),
                y: .value("Score", element.score)
            )
        }
        .frame(height: 300)

    }
}

棒グラフを縦から横に変更したい場合、xとyの値を反対にすれば実現できます。

struct ContentView: View {
    var body: some View {
               
        // 直接Chartにデータを渡す場合
        Chart(subjects) { element in
            BarMark(
                // 反対にすると横の棒グラフになる
                x: .value("Score", element.score),
                y: .value("Name", element.name)
            )
        }
        .frame(height: 300)

    }
}

Markの種類

7つのMarkがあります。
SectorMarkはWWDC2023で発表され、簡単にPie Chartが作ることが出来ます。

カスタマイズ

Swift Chartsには様々なカスタマイズオプションがあります。
その中から、よく使いそうなものを紹介します。

・グラフのサイズや背景色、枠線の変更(chartPlotStyle)

chartPlotStyleを使用することで、グラフのサイズや背景色、枠線の変更が出来ます。
下記は、高さを300に青色の枠線を追加しています。

struct ContentView: View {
    var body: some View {
        
        Chart(subjects) { element in
            BarMark(
                x: .value("Name", element.name),
                y: .value("Score", element.score)
            )
        }
        .chartPlotStyle { content in
            content
                .frame(height: 300)
                .background(.blue.opacity(0.2))
                .border(.blue, width: 1)
        }
    }
}

・カテゴリを分ける(foregroundStyle)

構造体にカテゴリを追加し、カテゴリを定義しておきます。
foregroundStyle(by:)を使用して、キーを指定します。

struct Subject: Identifiable {
    let name: String
    let score: Int

    var id: String { name }
    
    var category: String
}

let subjects: [Subject] = [
    .init(name: "現代文A", score: 80, category: "国語"),
    .init(name: "現代文B", score: 90, category: "国語"),
    .init(name: "数学I", score: 20, category: "数学"),
    .init(name: "数学II", score: 50, category: "数学"),
    .init(name: "科学", score: 30, category: "理科")
]

struct ChartView: View {
    
    var body: some View {
        
        Chart(subjects) { element in
            BarMark(
                x: .value("Name", element.name),
                y: .value("Score", element.score)
            )
            .foregroundStyle(by: .value("Category", element.category))
        }
        .chartPlotStyle { content in
            content
                .frame(height: 300)
        }        
    }
}

・カテゴリごとのチャートの色指定(chartForegroundStyleScale)

foregroundStyle(by:)で指定したキーを、chartForegroundStyleScaleで色とセットで指定します。
存在しないキーを渡した場合、クラッシュしてしまうため注意が必要です。

struct ChartView: View {
    
    var body: some View {
        
        Chart(subjects) { element in
            BarMark(
                x: .value("Name", element.name),
                y: .value("Score", element.score)
            )
            .foregroundStyle(by: .value("Category", element.category))
        }
        .chartForegroundStyleScale(["国語": .cyan, "数学": .red, "理科": .yellow])
        .chartPlotStyle { content in
            content
                .frame(height: 300)
        }
    }
}

・目盛りの値の範囲を指定(chartXScale, chartYScale)

目盛りの値の範囲を指定したい場合、X軸ならchartXScaleをY軸ならchartYScaleを使用します。
下記は、Y軸の目盛りの範囲を0から200と変更することが出来ます。

struct ContentView: View {
    var body: some View {
        
        Chart(subjects) { element in
            BarMark(
                x: .value("Name", element.name),
                y: .value("Score", element.score)
            )
        }
        // Y軸の値を0から200とする
        .chartYScale(domain: 0...200)
        .frame(height: 300)
    }
}

・目盛りの表示位置を変更(AxisMarks)

縦軸がデフォルトだと右側に表示されますが左側に変更したい場合、AxisMarksのpositionを設定すれば変更することが出来ます。
下記は、Y軸の表示を左側に変更しています。

struct ContentView: View {
    var body: some View {
        
        Chart(subjects) { element in
            BarMark(
                x: .value("Name", element.name),
                y: .value("Score", element.score)
            )
        }
        .chartYAxis {
            // Y軸の表示を左側に変更
            AxisMarks(position: .leading)
        }
        .frame(height: 300)
    }
}

・目盛りの間隔を調整(AxisMarks)

目盛りの間隔を調整したい場合、AxisMarksのvaluesを設定することで変更することが出来ます。
下記は、Y軸の目盛りを「0, 20, 40, 60, 80, 100」として表示するようにしています。
また.automatic(minimumStride: 増加数, desiredCount: 目盛りの数)でも表示することが出来ます。

struct ContentView: View {
    var body: some View {
        
        Chart(subjects) { element in
            BarMark(
                x: .value("Name", element.name),
                y: .value("Score", element.score)
            )
        }
        .chartYAxis {
            // 目盛りの間隔を調整
            AxisMarks(values: [0, 20, 40, 60, 80, 100])
            
            // 上記と同じ
            // AxisMarks(values: .automatic(minimumStride: 20, desiredCount: 6))
        }
        .frame(height: 300)

    }
}

・目盛りのテキストを編集(AxisValueLabel)

目盛りに表示しているテキストのフォントや、フォントサイズの変更等をする場合、AxisValueLabelを使用することで変更出来ます。
注意点としては、chartXAxis, chartYAxisを呼び出した際に空の状態だとグリッド線や目盛りの線が消えるため、表示したい場合は呼び出す必要があります。

struct ContentView: View {
    var body: some View {
        
        Chart(subjects) { element in
            BarMark(
                x: .value("Name", element.name),
                y: .value("Score", element.score)
            )
        }
        .chartYAxis {
            
            AxisMarks(position: .leading, values: [0, 20, 40, 60, 80, 100]) { value in
                
                AxisValueLabel {
                    if let intValue = value.as(Int.self) {
                     
                        Text("\(intValue)")
                            .font(.system(size: 15, weight: .heavy))
                            .foregroundStyle(.orange)
                    }
                }
                
                // chartXAxis, chartYAxisを呼び出し、空の状態だとグリッド線や目盛りの線が消えてしまうので呼び出す
                AxisGridLine()
                AxisTick()
            }
        }
        .frame(height: 300)
    }
}

・X、Y軸に対してラベルの表示制御(chartXAxisLabel, chartYAxisLabel)

X、Y軸に対してラベルの表示制御をする場合、X軸ならchartXAxisLabelをY軸ならchartYAxisLabelを使用することで設定出来ます。

struct ContentView: View {
    var body: some View {
        
        Chart(subjects) { element in
            BarMark(
                x: .value("Name", element.name),
                y: .value("Score", element.score)
            )
        }
        .chartXAxisLabel(position: .bottom, alignment: .center) {
            Text("X軸テキスト")
                .font(.title)
        }
        .chartYAxisLabel(position: .leading, alignment: .center) {
            Text("Y軸テキスト")
                .font(.title)
        }
        .frame(height: 300)
    }
}

・ガイドの非表示(chartXAxis, chartYAxis)

目盛り自体を表示したくない場合があります。その場合は、X軸ならchartXAxisをY軸ならchartYAxisをhiddenに設定することで変更できます。

struct ContentView: View {
    var body: some View {
        
        Chart(subjects) { element in
            BarMark(
                x: .value("Name", element.name),
                y: .value("Score", element.score)
            )
        }
        // X軸とY軸ともに非表示
        .chartXAxis(.hidden)
        .chartYAxis(.hidden)
        .frame(height: 300)
    }
}

アクティビティモニタのグラフ部分みたいなものを実装してみる

アクティビティモニタの、CPU負荷のグラフ部分を作ってみたいと思います。 
LineMarkとAreaMarkを組み合わせてカスタマイズします。
LineMarkが積み上げ表示ができなく、複雑なコードになってしまいました。

ChartView.swift

struct ChartView: View {
        
    @StateObject private var chartViewModel: ChartViewModel = .init()
    
    var body: some View {
        
        Chart(chartViewModel.chartDatas) { data in
            LineMark(
                x: .value("Name", data.date),
                y: .value("Value", data.category == "ユーザ" ? data.value + data.addValue : data.value)
            )
            .foregroundStyle(by: .value("Category", data.category))
            .lineStyle(StrokeStyle(lineWidth: 1))
            
            AreaMark(
                x: .value("Name", data.date),
                y: .value("Value", data.value)
            )
            .foregroundStyle(by: .value("Category", data.category))
            .opacity(0.3)
        }
        // チャートの色指定
        .chartForegroundStyleScale(["ユーザ": .cyan, "システム": .red])
        // グラフのサイズを変更
        .chartPlotStyle { content in
            content
                .frame(height: 80)
        }
        // ガイドを非表示
        .chartXAxis(.hidden)
        .chartYAxis(.hidden)
        // カテゴリ名を非表示
        .chartLegend(.hidden)
        // 目盛りの値の範囲を指定
        .chartYScale(domain: 0...100)
        .padding(5)
    }
}

ChartViewModel.swift

class ChartViewModel: ObservableObject {
    
    @Published var chartDatas: [ChartData] = {
       var initChartDatas = [ChartData]()
        
        let now = Date()
        
        // データの初期化
        for i in stride(from: 30, through: 1, by: -1) {
            initChartDatas.append(.init(date: now - Double(i) * 1, value: 0, category: "システム"))
            initChartDatas.append(.init(date: now - Double(i) * 1, value: 0, category: "ユーザ"))
        }
        
        return initChartDatas
    }()
    
    init() {
        self.startTimer()
    }
    
    func startTimer() {
        
        Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            guard let self = self else { return }
            
            
            Task { @MainActor in
                
                let now = Date()
                
                // システム
                let systemValue = Double.random(in: 0.0 ..< 50.0)
                let systemChartData = ChartData(date: now, value: systemValue, category: "システム")
                self.chartDatas.append(systemChartData)
                self.chartDatas.removeFirst()
                
                // ユーザ
                let userValue = Double.random(in: 0.0 ..< 50.0)
                let userChartData = ChartData(date: now, value: userValue, addValue: systemValue, category: "ユーザ")
                self.chartDatas.append(userChartData)
                self.chartDatas.removeFirst()
                
                print("self.chartDatas: \(self.chartDatas)")
            }
        }
    }
}

まとめ

Swift Chartsの基本的な使い方やカスタマイズの仕方をまとめました。
簡単にチャートが実装できるため、そこまでカスタマイズをしないのであればSwift Chartsの選択もありだと思います。ただし、iOS 16以降をサポートしている必要があります。
この記事がお役に立てれば幸いです。また、間違っている点などありましたら、教えていただけると幸いです。

参考リンク



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる