iOS16、Xcode14から標準グラフ描画ライブラリにChartsが追加されました。ここではまだ情報が少ないChartsを使って、技術プログラムでは必須の散布図や折れ線グラフを作成する方法を紹介します。SwiftUI初心者の筆者が調べながら書いているので、同じく初心者の人に参考になると思います。
こんにちは。wat(@watlablog)です。ここではSwiftUI標準のChartsを使ってグラフを作ってみます!
SwiftUI標準Chartsの概要と本記事の目標
標準のCharts
この記事で扱うのはSwiftUI標準で使えるChartsです。標準のChartsについては以下のリンク先を参考にしてください。
https://developer.apple.com/jp/xcode/swiftui/
SwiftUIのChartsについて、公式ページはこちらです。基本は1次情報であるこのページを参照しておきましょう。
https://developer.apple.com/documentation/charts
ちなみに標準のChartsが2022年9月に発表されるまではGitHubのページからダウンロードしてグラフを描画する方法があったようです。筆者は2023年からSwiftに入門したのですが、Web検索だけだとどちらのChartsについて書いてある記事なのかよくわからない時がありましたのでご注意ください。
https://github.com/danielgindi/Charts
目標:散布図と折れ線グラフをつくる
「SwiftUI Charts」で検索するとどうやら棒グラフの描き方ばかり出てきます。おそらくiOSアプリとしてはファイル容量や日々のデータ可視化等、棒グラフに適したデータにグラフを用いるニーズが多いためであると考えられます。
さらに、サンプルの多くはデータを事前に用意しておいてそれを表示させるという内容が多いようです。
WATLABブログは主にPythonで科学技術プログラミングを学ぶための記事を書いてきましたが、その知見をiOSアプリにしてみたいと思ったことがSwiftを学んでいる理由です。
そのためSwiftでも科学技術でよく使う散布図や折れ線グラフを作ってみたいと思います。
また技術計算ではあらかじめデータを用意するのではなく、数式で得られたデータをプロットしたいことが多いです。そのためこの記事では関数で計算された値をプロットする方法を習得することを目標とします。
動作環境と注意点
この記事では以下の環境で動作を確認しています。
Mac | OS | macOS Ventura 13.2.1 |
---|---|---|
CPU | 1.4[GHz] | |
メモリ | 8[GB] | |
Xcode | Version | 14.3(14E222b) |
Swift | Version | 5.8(swiftlang-5.8.0.124.2 clang-1403.0.22.11.100) |
iPhone SE2 | OS | iOS 16.3.1 |
Xcode関連のバグ?Chartsで発生したエラーと解決例
ここで1つ注意点があります。筆者は最初にXcode14.3をインストールして使っていましたが、Chartsの公式サンプルコードを書いても以下のエラーが出て動作しませんでした(Chart{}の部分に)。
「Cannot call value of non-function type 'module<Chart>」
色々模索した結果、最終的にはXcodeのバージョンを一度14.1まで落として解決しました。Xcodeは複数のバージョンを共存させてインストールできます。
YouTubeやブログ記事で普通に使っている人がいたのでおかしいなと思いましたが、それらのコンテンツの作成日がXcode14.3になる前だったので試してみました!
そして一度解決したら上に記載している14.3でもエラーが出なくなりました。そのため現在は14.3でプログラミングをしています。
おそらく以下のエラーと同じ種類だと思うのですが、正直いまだに原因はわかりません。
Swift UI AsyncImage error - Cannot call value of non-function type 'module'
I am learning SwiftUI, from scratch and trying to show a Image from a URL, I am using the built in AsyncImage method, yet I get this error with no context. Cannot call value of non-function type 'module', Googling didn't help and I am following the exact same code line-by-line from a article I am following.
https://stackoverflow.com/questions/68770717/swift-ui-asyncimage-error-cannot-call-value-of-non-function-type-moduleasync
SwiftUIのChartsで散布図を作成するコード
まずは1点のみをプロットする
段階的にコードの書き方を確認しながら進めましょう。まずは以下の方針で1点だけポイントを表示させる散布図を作成します。
- ボタンクリックで散布図の値が更新される
Swiftで書くのは毎回GUIアプリです。今回もイベントと連動するということを確認します。Buttonウィジェットを追加し、イベントが書けるようにしておきます。ボタンイベントは「SwiftでiOSデバイスのマイクを使って録音機能を追加する方法」で書き方を紹介していますので参考にしてください。 - データ変数を更新できるように宣言する
ボタンイベントと関連し、散布図に渡すデータ変数(x値とy値)は@Stateを使って更新を監視できるように宣言します。@Stateについては「Swift入門:最低限覚えておく基礎文法の備忘録」に詳細を書きましたので、まだ読んでいない人は是非参考にしてください。 - 関数を使ってデータの受け渡しを行う
技術プログラミングでよく使う関数を介したデータのやり取りを想定します。関数はfuncで書きます。関数funcの書き方も「Swift入門:最低限覚えておく基礎文法の備忘録」に記載していますのでこちらも参考にしてください。
以下のコードをそのままXcodeのContentView.swiftに貼り付けることで、上記方針の通りに1点散布図プログラムが動きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import SwiftUI import Charts struct ContentView: View { // 散布図に使う点データを宣言 @State private var x0: Float = 0.0 @State private var y0: Float = 0.0 func wavePoint(px0: Float, py0: Float) -> (px: Float, py: Float){ // 点座標を計算する関数 let px = px0 + 1 let py = py0 + 1 return (px, py) } var body: some View { // メインUI VStack { Chart{ // 散布図プロット部分 PointMark(x: .value("x", x0), y: .value("y", y0) ) } Button("Button") { // ボタンクリックで散布図の値を更新 let result = wavePoint(px0: x0, py0: y0) x0 = result.px y0 = result.py } } .padding(.all, 10) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { // プレビュー ContentView() } } |
こちらが実行結果です。Buttonをクリックする度にxとyが+1されて斜めに移動します。イベント処理と関数処理、値の更新処理がうまくいっていることを確認しました。グラフエリアのグリッドや文字は自動でフォーマットが決まっています。フォーマットを自由にいじるのは別途記事を書こうと思います。
複数点をプロットしてみる
ボタンイベントなしのシンプル構成
1点はイメージしやすかったので次は複数点のプロットに挑戦します。基本的なコードは以下のサンプルを参考にしました。
https://developer.apple.com/documentation/charts/creating-a-chart-using-swift-charts
Pythonに慣れた筆者からすると、SwiftUIとChartを使った複数点の散布図プロットは少々面倒でした。そのためまずは公式のサンプルコードを参考に、ボタンイベントなしで作成します。
以下のコードはstructでx値とy値をそれぞれxValueとyValueで定義するPointsData構造体を作成し、dataで実際の値を定数として与えています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import SwiftUI import Charts struct PointsData: Identifiable { // 点群データの構造体 var xValue: Float var yValue: Float var id = UUID() } // データを定義 var data: [PointsData] = [ .init(xValue: 0, yValue: 0), .init(xValue: 1, yValue: 1), .init(xValue: 2, yValue: 2), ] struct ContentView: View { var body: some View { Chart { // データ構造からx, y値を取得して散布図プロット ForEach(data) { shape in PointMark( x: .value("x", shape.xValue), y: .value("y", shape.yValue) ) } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { // プレビュー ContentView() } } |
このコードを実行するとこちらのように3点がプロットされます。これはまだまだサンプル通りなので序の口です。
ボタンイベントありの場合(関数演算付き)
要領を得たので次はボタンで値が更新されるイベントを追加しましょう。これはこれまで学んだことの組み合わせなのであと少し編集するだけです。せっかくなので横軸xに対して縦軸はランダムに値が決まるように.random()を使ってみます。
少し編集項目が多くなるので以下に箇条書きで説明を行った後にコードを示します。
- var dataを動的に指定できるよう初期化する
先ほどは「var data」にPointsDataの型を引き継いで直接データ点数を指定していましたが、どんなデータ長の数値配列が来ても対応できるように初期化の段階では空にしておきます。
そしてContentsView内で@Stateを使って宣言することで状態を監視し、更新が反映できるようにしてみます。 - 関数で配列を作成する
この記事の目標である関数でデータを用意する部分は「func wavePoints」で行っています。.randomを使っていたりしますが、基本は「Swift入門:最低限覚えておく基礎文法の備忘録」の記事でやっていたことと同じ内容です。 - ボタンイベント
Buttonをクリックすると先ほど定義した関数によるx配列(xArray)とy配列(randArray)がタプルで返ってきます。これを分解して散布図用のデータ形式にするわけですが、ここで.appendを使ってみました。.appendを使うことで動的にデータを構築できます。ただし、.appendの前に.removeAllで一度配列をクリアにしておかないとどんどんデータが追加されていってしまいます。
上記の内容に注意してコーディングすることでChart{}内のコードは何も変更することなく複数点のデータ更新やプロットが実装できます。
以下に全コードを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
import SwiftUI import Charts struct PointsData: Identifiable { // 点群データの構造体 var xValue: Float var yValue: Float var id = UUID() } struct ContentView: View { // データを定義 @State private var data: [PointsData] = [] @State private var randArray: [Float] = [] @State private var x: [Float] = [] @State private var y: [Float] = [] func wavePoints(dt: Float, length: Float) -> (xArray: [Float], yArray: [Float]){ // 散布図にプロットするデータを演算する関数 // 時間刻みdtと時間長lengthからx軸を作成 let xArray = Array(stride(from: 0.0, to: length, by: dt)) // ランダム配列をx軸のデータ数分計算 randArray = (0..<xArray.count).map { _ in Float.random(in: 0..<1) } return (xArray, randArray) } var body: some View { // UI VStack{ Chart { // データ構造からx, y値を取得して散布図プロット ForEach(data) { shape in // 散布図をプロット PointMark( x: .value("x", shape.xValue), y: .value("y", shape.yValue) ) } } Button("Button"){ // ボタンイベント // 関数を実行してx, y演算結果を取得 let result = wavePoints(dt: 1, length: 10) x = result.xArray y = result.yArray // プロットデータの全削除 data.removeAll() // プロットデータの追加 for i in 0..<x.count { data.append(.init(xValue: x[i], yValue: y[i])) } } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { // プレビュー ContentView() } } |
以下に実行例を示します。ボタンクリックでランダムに散布図が描かれているので目標達成です。
SwiftUIのChartsで折れ線グラフを作成するコード
次は折れ線グラフを作ってみます。おおげさにタイトルを付けてしまいましたが、先ほどのコードにおいてChart{}内のPointMarkをLineMarkに変更するだけでOKです。
1 2 3 4 5 6 7 8 9 10 11 12 |
Chart { // データ構造からx, y値を取得して散布図プロット ForEach(data) { shape in // 折れ線グラフをプロット LineMark( x: .value("x", shape.xValue), y: .value("y", shape.yValue) ) } } |
こちらが実行結果です。ここまでできればグラフの種類変更も朝飯前のようです。
まとめ
本記事ではSwiftUIに追加されたChartsを使って散布図と折れ線グラフを描くコードを紹介しました。structでデータ構造を決定し、実際のデータを作成する方法として静的、動的の2種類を書いてみました。Swiftを始めたばかりですが、ようやく構造体の使い方がわかってきた気がします。
基本は公式のサンプルコードを参考にすればある程度書けるといっても、Swiftでまともなコードを書いたことがない筆者にとってはエラー1つを解決するのに長い時間がかかってしまいました。
しかし少しずつ検証していくことでやりたいことに近づいているという感覚が持てたので、徐々にスピードアップすることができそうです。
Chartsは2022年後半に標準となったライブラリであり、さすがのChatGPT-4もあまり役に立ちませんでした。新技術に対するキャッチアップに必要な体力はやはり必要ですね…。
そのうちグラフの書式変更もやってみます。
ようやくSwiftUIを使ってグラフを描けるようになりました!
Twitterでも関連情報をつぶやいているので、wat(@watlablog)のフォローお待ちしています!