SwiftChartをSwiftUIで使う

SwiftUI

入力されたデータのグラフ表示は必須ですよね。

グラフ表示のライブラリとしてSwiftChartというライブラリがあるのですが、それをSwiftUI化するまでの手順のメモです。

LineViewを使いたいので、LineView特化型です。
# と思って作って行ったのですが、結果的に特にLineView限定になっていません。

CocoaPodでインストール

SwiftChartは、まだSwift Package Managerに対応していないので、CocoaPodを使ってインストールします。

  • プロジェクトのディレクトリで、% pod init
  • 作成されたPodfileに、 pod 'SwiftChart' を追加
  • プロジェクトのディレクトリで、% pod install or update
  • .xcworkspaceを使って、Xcodeを起動

まず、ここで、コンパイルがおかしくなっていないか確認します。

XCode11.4では、1箇所、Deprecatedと言われたコードがあったので、言われるままに修正しました。

UIViewRepresentableでWrapする

まずは、以下のようなコードで、静的なデータを表示することができるかを確認しました。

struct LineGraphView: UIViewRepresentable {
    func makeUIView(context: Context) -> Chart {
        let chart = Chart(frame: CGRect.zero)
        let series = ChartSeries([0, 6.5, 2, 8, 4.1, 7, -3.1, 10, 8])
        chart.add(series)
        return chart
    }
    func updateUIView(_ uiView: Chart, context: Context) {
        print("update")
    }
}

struct LineGraphView_Previews: PreviewProvider {
    static var previews: some View {
        LineGraphView()
    }
}

きちんと別のSwiftUI要素と矛盾しないことを確認しました。

ただ、このままだと、データも内部で生成してますし、タッチされたときの動作も何もないです。
ということで、すこしづつできることを増やしていきます。

# 今回は、データの動的な変更でグラフが変形されることは目指しません。タッチしたときにデータの詳細を表示できることがゴールです。

ChartSeriesを外部から渡す

ChartSeriesを外から渡すようにしようとも考えたのですが、外部からChartSeries(表示対象とするデータ)を取得するClosureを渡すことにしようかと。

struct LineGraphView: UIViewRepresentable {
    var dataFetch:() -> [ChartSeries]

    func makeUIView(context: Context) -> Chart {
        let datas = self.dataFetch()
        let chart = Chart(frame: CGRect.zero)
        chart.add(datas)

        return chart
    }
    
    func updateUIView(_ uiView: Chart, context: Context) {
        print("update")
    }
    

}

struct LineGraphView_Previews: PreviewProvider {
    static let chartSeries1 = ChartSeries([0, 6.5, 2, 8, 4.1, 7, -3.1, 10, 8])
    static let chartSeries2 = ChartSeries([0, 6.5, 2, 8, 4.1, 7, -3.1, 10, 8].reversed())
    static func dataFetch() -> [ChartSeries] {
        return [ChartSeries([0, 6.5, 2, 8, 4.1, 7, -3.1, 10, 8]),
                ChartSeries([0, 6.5, 2, 8, 4.1, 7, -3.1, 10, 8].reversed())]
    }
    static var previews: some View {
        LineGraphView(dataFetch: dataFetch)
    }
}

こうすることで、アプリのデータ構造、SwiftChartでのデータ構造のどちらも理解しなければいけない箇所をClosure中に閉じ込めることができます。
つまり、アプリ本体は、SwiftChartが使うChartSeriesに対してindependentになり、LineGraphViewもどのようにデータを取得するかについてindependentにできます。

タッチした箇所のデータ詳細の表示

調べてみるとChartDelegateなるプロトコルがタッチ操作への対応するためのプロトコルでした。

UIViewRepresentableでは、Coordinatorを作るのが定石ですので、以下のように追加しました。

struct LineGraphView: UIViewRepresentable {
    var dataFetch:() -> [ChartSeries]

    func makeUIView(context: Context) -> Chart {
        let datas = self.dataFetch()
        let chart = Chart(frame: CGRect.zero)
        chart.add(datas)
        
        chart.delegate = context.coordinator

        return chart
    }
    
    func updateUIView(_ uiView: Chart, context: Context) {
        // no need to update because no one will change data
    }
    
    
    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }
    
    class Coordinator:NSObject, ChartDelegate {
        func didTouchChart(_ chart: Chart, indexes: [Int?], x: Double, left: CGFloat) {
            print("Touched")
        }
        
        func didFinishTouchingChart(_ chart: Chart) {
            print("touch didFinish")
        }
        
        func didEndTouchingChart(_ chart: Chart) {
            print("touch didEnd")
        }
    }
}

Chartでは、タッチ中には、didTouchChartが呼ばれ続け、タッチがおわるとdidEndTouchingChartが呼ばれるようです。

didTouchChartの引数

xは、グラフ中のX値、leftは、表示領域中のX値です。(詳細は、SwiftChartのソースを参照のこと)
Indexesは、最初の値は、入力とした何番目のSeriesか、2番目の値は、そのSeriesの中の何番目かです。
これらの情報を使って、”chart.valueForSeries(seriesIndex, atIndex: dataIndex)"とするとChart本体から、値を取得することができます。

この情報を使って、頂点付近をタッチされた時に、その値を数値として表示することができます。

Chartは、その他いろいろカスタマイズ可能

上記の方法で、上位からデータを渡してそのデータを表示するところまではできるようになります。
SwiftChartはグラフの色や軸についてもカスタマイズできるので、それぞれのデータを上記のようなclosureで渡すもよし、@ObservableObjectで渡すのもありです。
いずれにしても、UIKit向けに作られたコンポーネントが簡単にSwiftUIと共存することができます。

注意
上記では、UIViewを継承しているものを再利用をしたので、UIViewRepresentableを使用していますが、UIViewControllerを継承しているものを再利用するならば、UIViewControllerRepresentableを使うことになります。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です