この記事では、SwiftUIでのCharts_iOSを使ったグラフの実装方法を紹介します。
実装例としてCoreDataを利用し、Y軸に温度、X軸に日付を線グラフで表示する方法を解説します。
◆動作検証環境
・XCode:12.3
・SwiftUI:2.0
・iOS:14.0
・Life Cycle:SwiftUI App
・Charts:4.0.1
Chartsのインストール
Chartsのインストールの方法や、実装できるグラフの使用例等はこちらのサイトで確認できます。
今回は、CocoaPodsを利用してChartsのインストールを行います。
CocoaPodsのインストール
まずは下記のコマンドを実行してCocoaPodsをインストールします。
※すでにインストール済みの場合は、こちらの設定は必要ありません。
1 2 3 | sudo gem install cocoapods |
インストールが終わったら、次のコマンドで初期化を行います。
1 2 3 | pod setup |
XcodeプロジェクトへCocoaPodsを導入する
導入の作業の前に、Xcodeのプロジェクトが開かれているか確認します。開いている場合はプロジェクトを閉じます。
プロジェクトディレクトリへの移動
1 2 3 | cd xcode_project |
CocoaPodsファイルの作成
1 2 3 | pod init |
Podfileの編集(Chartsの導入)
さきほどのpod init を行ったディレクトリにPodfile が作成されるので、内容を下記のように編集します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | # Uncomment the next line to define a global platform for your project # platform :ios, '9.0' target 'YourApp' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for YourApp pod 'Charts' #追加 end |
編集後に下記コマンドでKeyboardObserving のインストールを行います。
1 2 3 | pod install |
インストール後の更新作業は下記コマンドで行います。
1 2 3 | pod update |
対象のモジュールのインストール後からは、ProjectName.xcworkspace で作業、編集を行います(ProjectName.xcodeproj ではない。)
Charts_iOSを使ってデータの無いグラフの表示
プロジェクトにCharts の導入ができましたので、まずはデータの無い状態でグラフ機能の表示を行います。
LineChart.swiftファイルを作成し、下記のように編集します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import SwiftUI import Charts struct LineChart : UIViewRepresentable { typealias UIViewType = LineChartView func makeUIView(context: Context) -> LineChartView { let lineChartView = LineChartView() return lineChartView } func updateUIView(_ uiView: LineChartView, context: Context) { } } |
この状態でこのstruct を表示すると、
1 2 3 | No chart data available |
と表示されます。
データを追加してCharts_iOSの線グラフを表示する
データを利用する線グラフの表示
まずは、グラフで表示するデータを作成します。
今回は、表示の確認のために以下の簡単なデータとします。
X軸 | Y軸 |
1 | 10.0 |
2 | 20.0 |
3 | 30.0 |
4 | 40.0 |
5 | 50.0 |
LineChartのStruct内に定義します。
1 2 3 4 5 6 7 8 9 | let yValue:[ChartDataEntry] = [ ChartDataEntry(x: 1, y: 10.0), ChartDataEntry(x: 2, y: 20.0), ChartDataEntry(x: 3, y: 30.0), ChartDataEntry(x: 4, y: 40.0), ChartDataEntry(x: 5, y: 20.0) ] |
次に、LineChartView に表示するデータをLineChartData 型で渡すメソッドを作ります。
1 2 3 4 5 6 7 8 | func setData() -> LineChartData{ let set = LineChartDataSet(entries: yValue, label: "My data") let data = LineChartData(dataSet: set) return data } |
そして、makeUIView メソッド内で、lineChartView にデータを追加します。
1 2 3 4 5 6 7 8 9 | func makeUIView(context: Context) -> LineChartView { let lineChartView = LineChartView() lineChartView.data = setData() return lineChartView } |
これでLineChartのコード全文は以下のようになります。
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 | struct LineChart : UIViewRepresentable { typealias UIViewType = LineChartView func makeUIView(context: Context) -> LineChartView { let lineChartView = LineChartView() lineChartView.data = setData() return lineChartView } func updateUIView(_ uiView: LineChartView, context: Context) { } let yValue:[ChartDataEntry] = [ ChartDataEntry(x: 1, y: 10.0), ChartDataEntry(x: 2, y: 20.0), ChartDataEntry(x: 3, y: 30.0), ChartDataEntry(x: 4, y: 40.0), ChartDataEntry(x: 5, y: 20.0) ] func setData() -> LineChartData{ let set = LineChartDataSet(entries: yValue, label: "My data") let data = LineChartData(dataSet: set) return data } } |
この状態で、サイズを指定し表示すると以下のようなグラフを確認できるはずです。
1 2 3 4 | LineChart() .frame(height: 400) |
グラフ表示のカスタマイズ
いくつか表示の方法をカスタマイズします。
makeUIView とsetData を以下のように編集し、グラフ表示をカスタマイズします。
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 | func makeUIView(context: Context) -> LineChartView { let lineChartView = LineChartView() lineChartView.data = setData() lineChartView.backgroundColor = .lightGray //バックグラウンドカラーの変更 lineChartView.data!.setValueFont(.systemFont(ofSize: 20, weight: .light)) //データのフォントサイズとウエイトの変更 lineChartView.data!.setValueTextColor(.white) lineChartView.data!.setDrawValues(true) //データの値表示(falseに設定すると非表示) lineChartView.rightAxis.enabled = false //右側のX軸非表示 lineChartView.animate(xAxisDuration: 2.5) //表示の際のアニメーション効果(この場合はX軸方法で2.5秒) //Y軸表示の設定 let yAxis = lineChartView.leftAxis // lineChartView.leftAxisを変数で定義 yAxis.labelFont = .boldSystemFont(ofSize: 12) //Y軸単位のフォントサイズ yAxis.setLabelCount(5, force: true) //Y軸の表示罫線数(falseにすると指定無し) yAxis.labelTextColor = .white //Y軸単位のテキストカラー yAxis.axisLineColor = .white //Y軸単位の軸のカラー yAxis.labelPosition = .outsideChart //Y軸単位のポジション(.insideChartにすると内側で表示) //X軸表示の設定 let xAxis = lineChartView.xAxis // lineChartView.xAxisを変数で定義 xAxis.labelPosition = .bottom //X軸単位のポジション(下部に表示) xAxis.labelFont = .boldSystemFont(ofSize: 12) xAxis.setLabelCount(5, force: true) xAxis.labelTextColor = .white xAxis.axisLineColor = .white return lineChartView } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | func setData() -> LineChartData{ let set = LineChartDataSet(entries: yValue, label: "My data") let data = LineChartData(dataSet: set) set.mode = .cubicBezier //線表示を曲線で表示 set.drawCirclesEnabled = false //各データを丸記号表示を非表示 set.lineWidth = 1.5 //線の太さの指定 set.setColor(NSUIColor.red) //線の色の指定 set.fill = Fill(color: .white) //塗りつぶし色の指定 set.fillAlpha = 0.5 //塗りつぶし色の不透明度指定 set.drawFilledEnabled = true //値の塗りつぶし表示 return data } |
この状態でグラフを表示すると、以下のようになります。
他にも、カスタマイズの方法がありますが、ここでは代表的な方法のみ紹介しています。
Date型の値をY軸の表示に対応させる
現在はグラフに表示させる値は、ChartDataEntry クラスのインスタンスを利用していますが、利用できるパラメータは、X軸、Y軸ともにDouble 型となります。
この仕様に対応し、X軸に日付のデータを表示できるように、コードを編集します。
getDataPointsメソッドの追加
X軸 | Y軸 |
1 | 10.0 |
2 | 20.0 |
3 | 30.0 |
4 | 40.0 |
5 | 50.0 |
現在利用しているデータは上記の内容です。
Y軸は上記のまま利用し、X軸の値を日付での表示に対応させます。
そのために、X軸、Y軸を1セットとし、このセットの数をX軸に値になるようにgetDataPoints メソッドを以下のように作成します。
1 2 3 4 5 6 7 8 9 10 | func getDataPoints(accuracy: [ChartDataEntry]) -> [ChartDataEntry] { var dataPoints: [ChartDataEntry] = [] for count in (0..<accuracy.count) { dataPoints.append(ChartDataEntry(x: Double(count), y: accuracy[count].y)) } return dataPoints } |
これで、X軸の値は引数で受け取る[ChartDataEntry] 型配列の要素数、Y軸はChartDataEntry のy の値とする[ChartDataEntry] を返すようになります。
このメソッド作成に伴い、setData メソッドでdataPoint を定義する際にさきほどのメソッドを利用するように編集します。
1 2 3 4 5 6 7 8 | func setData() -> LineChartData{ let dataPoint = getDataPoints(accuracy: yValue) let set = LineChartDataSet(entries: dataPoint, label: "My data") let data = LineChartData(dataSet: set) |
この状態でグラフを表示すると、X軸が配列の要素数(この記事のデータの場合は0〜4に5個)になり、以下のようになります。
次に、makeUIView メソッド内を編集します。
xAxis.granularity = 1.0 とする事で、X軸の要素数の単位が1となり、日付の表示に対応できるようになります。1 2 3 4 5 6 7 8 9 10 | let xAxis = lineChartView.xAxis // lineChartView.xAxisを変数で定義 xAxis.labelPosition = .bottom //X軸単位のポジション(下部に表示) xAxis.labelFont = .boldSystemFont(ofSize: 12) xAxis.setLabelCount(5, force: false) xAxis.labelTextColor = .white xAxis.axisLineColor = .white xAxis.granularity = 1.0 //X軸の表示単位を1.0ごとにする |
DateValueFormatterクラスの作成
グラフの素データの要素数を日付に変換するために、IAxisValueFormatter クラスを継承するクラスを作成し、以下のように編集します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class DateValueFormatter: NSObject, IAxisValueFormatter { let dateFormatter = DateFormatter() var startDate:Date init(startDate:Date) { self.startDate = startDate } public func stringForValue(_ value: Double, axis: AxisBase?) -> String { let modifiedDate = Calendar.current.date(byAdding: .day, value: Int(value), to: startDate )! dateFormatter.dateFormat = "M/d" return dateFormatter.string(from: modifiedDate) } } |
パラメータで受け取るDate型の日付を最初の日として、データの要素分表示するようになります。
dateFormatter.dateFormat = "M/d" と指定する事でグラフで日付が表示される際は1/1 となります。
あとは、makeUIView のメソッド内でxAxis にさきほどのクラスでフォーマットします。
インスタンスを作る時に、最初に表示する日付けをDate型で指定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | //X軸表示の設定 let xAxis = lineChartView.xAxis // lineChartView.xAxisを変数で定義 xAxis.labelPosition = .bottom //X軸単位のポジション(下部に表示) xAxis.labelFont = .boldSystemFont(ofSize: 12) xAxis.setLabelCount(5, force: false) xAxis.labelTextColor = .white xAxis.axisLineColor = .white xAxis.granularity = 1.0 //X軸の表示単位を1.0ごとにする let formatter = DateValueFormatter(startDate: Date()) xAxis.valueFormatter = formatter |
この状態でグラフを表示すると、以下のようになりX軸を日付で表示する事ができます。
CoreDataの値をCharts_iOSの線グラフで表示する
最後に、CoreDataの値を利用してCharts_iOSの線グラフで表示するできるように編集します。
今回はCoreDataの値は、日付と温度と2種類としました。
CoreDataを使ったデータベース処理の方法は過去の記事を参考にしてください。
この記事では、SwiftUIでのCoreData の基本的な実装方法【CRUD(作成、更新、削除)】を紹介しています。簡単なタスク管理App(タスク内容とスケジュールの登録)を作成しながら解説します。 ◆動[…]
この記事で紹介するCoreDataの処理を含めたファイルとコードは以下のとおりです。
ChatsTestApp.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import SwiftUI @main struct ChatsTestApp: App { let persistenceController = PersistenceController.shared var body: some Scene { WindowGroup { ContentView() .environment(\.managedObjectContext, persistenceController.container.viewContext) } } } |
ContentView.swift
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 76 77 78 79 80 81 82 83 84 | import SwiftUI import CoreData struct ContentView: View { @StateObject var viewModel = ViewModel() @FetchRequest(entity: Temp.entity(), sortDescriptors: [NSSortDescriptor(key: "date", ascending: true)],animation: .spring()) var results : FetchedResults<Temp> @Environment(\.managedObjectContext) var context var body: some View { ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom), content: { VStack(spacing:0){ HStack{ Text("Data") .font(.largeTitle) .fontWeight(.heavy) .foregroundColor(.black) Spacer(minLength: 0) } .padding() .padding(.top,UIApplication.shared.windows.first?.safeAreaInsets.top) .background(Color.white) if results.isEmpty{ Spacer() Text("No Data") .font(.title) .foregroundColor(.primary) .fontWeight(.heavy) Spacer() }else{ LineChart() ScrollView(.vertical,showsIndicators: false, content:{ LazyVStack(alignment: .leading, spacing: 20){ ForEach(results){data in VStack(alignment: .leading, spacing: 5, content: { Text(String(data.temp )) .font(.title) .fontWeight(.bold) Text(data.date ?? Date(),style: .date) .fontWeight(.bold) Divider() }) .foregroundColor(.primary) .contextMenu{ Button(action: { viewModel.EditItem(item: data) }, label: { Text("Edit") }) Button(action: { context.delete(data) try! context.save() }, label: { Text("Delete") }) } } } .padding() }) } } Button(action: {viewModel.isNewData.toggle()}, label: { Image(systemName: "plus") .font(.largeTitle) .foregroundColor(.white) .padding(20) .background(Color.green) .clipShape(Circle()) }) .padding() }) .sheet(isPresented: $viewModel.isNewData, content: { Sheet(viewModel: viewModel) }) } } |
ViewModel.swift
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 | import SwiftUI import CoreData class ViewModel : ObservableObject{ @Published var temp:Double = 5 @Published var date = Date() @Published var isNewData = false @Published var updateItem : Temp! func writeData(context : NSManagedObjectContext ){ if updateItem != nil{ updateItem.date = date updateItem.temp = temp try! context.save() updateItem = nil isNewData.toggle() temp = 0.0 date = Date() return } let newTask = Temp(context: context) newTask.date = date newTask.temp = temp do{ try context.save() isNewData.toggle() temp = 0.0 date = Date() } catch{ print(error.localizedDescription) } } func EditItem(item:Temp){ updateItem = item date = item.date! temp = item.temp isNewData.toggle() } } |
Sheet.swift
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 | import SwiftUI struct Sheet: View { @ObservedObject var viewModel : ViewModel @Environment(\.managedObjectContext) var context var body: some View { VStack{ HStack{ Text("\(viewModel.updateItem == nil ? "Add New" : "Update") Data") .font(.title) .fontWeight(.heavy) .foregroundColor(.primary) } .padding() TextField("", value: $viewModel.temp, formatter: NumberFormatter()) .padding() Divider() .padding(.horizontal) HStack{ Text("When") .font(.title) .fontWeight(.bold) .foregroundColor(.primary) } .padding() DatePicker("", selection:$viewModel.date, displayedComponents:.date)//日付も使用する場合は”displayedComponents:.date”をなくす .labelsHidden() Button(action: {viewModel.writeData(context: context)}, label: { Label(title:{Text(viewModel.updateItem == nil ? "Add" : "Update") .font(.title) .foregroundColor(.white) .fontWeight(.bold) }, icon: {Image(systemName: "plus") .font(.title) .foregroundColor(.white) }) .padding(.vertical) .frame(width:UIScreen.main.bounds.width - 30) .background(Color.orange) .cornerRadius(50) }) .padding() } .background(Color.primary.opacity(0.06).ignoresSafeArea(.all, edges: .bottom)) } } |
Persistence.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import CoreData struct PersistenceController { static let shared = PersistenceController() let container: NSPersistentContainer init(inMemory: Bool = false) { container = NSPersistentContainer(name: "ChatsTest") if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") } container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) } } |
4日分の温度のデータをCareDataに追加し、ContentViewで以下のよう表示されます。
今回は、このデータを先程作成したグラフで表示するように編集を行います。
getDataPointsの編集
温度の値の配列のデータでグラフ表示に対応できるように、getDataPointsを編集します。
1 2 3 4 5 6 7 8 9 10 | func getDataPoints(accuracy: [Double]) -> [ChartDataEntry] { var dataPoints: [ChartDataEntry] = [] for count in (0..<accuracy.count) { dataPoints.append(ChartDataEntry(x: Double(count), y: accuracy[count])) } return dataPoints } |
LineChartの編集
LineChartメソッドを以下のように編集し、CoreDataの温度の値を利用し、Double型の配列を作ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | struct LineChart : UIViewRepresentable { var tempData:FetchedResults<Temp> var temps:[Double] = [] init(tempData:FetchedResults<Temp>) { self.tempData = tempData for count in (0..<self.tempData.count){ temps.append(self.tempData[count].temp) } } typealias UIViewType = LineChartView ... |
setDataの編集
setDataも対応させます。
1 2 3 4 5 | func setData() -> LineChartData{ let dataPoint = getDataPoints(accuracy: temps) |
makeUIViewの編集
setDataのグラフの始まりの日付の指定も、CoreDataのデータを利用するように編集します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | ... let xAxis = lineChartView.xAxis // lineChartView.xAxisを変数で定義 xAxis.labelPosition = .bottom //X軸単位のポジション(下部に表示) xAxis.labelFont = .boldSystemFont(ofSize: 12) xAxis.setLabelCount(5, force: false) xAxis.labelTextColor = .white xAxis.axisLineColor = .white xAxis.granularity = 1.0 //X軸の表示単位を1.0ごとにする let formatter = DateValueFormatter(startDate: tempData[0].date!) xAxis.valueFormatter = formatter ... |
この状態でグラフを表示すると、下記のようにCoreDataの内容にあった表示となります。
日付が連続しない場合の対応
現在のコードの場合、最初の日付からその後全ての日付でデータがある場合であれば、素のデータどおりに表示されますが、温度のデータが無い日もある場合、要素数を素に日付を表示するので素のデータとは違う表示となってしまいます。
現在のCoreData内のデータは3月1日から4日まで、連続でありますが、これを7日まで(データの無い日は0で表示)グラフで表示するようにします。
この表示の方法を修正するようにコードを編集します。
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 | struct LineChart : UIViewRepresentable { var tempData:FetchedResults<Temp> var temps:[Double] = [] init(tempData:FetchedResults<Temp>) { self.tempData = tempData let startDate = self.tempData[self.tempData.startIndex].date let today = Date() let calendar = Calendar(identifier: .gregorian) let startDateDC = Calendar.current.dateComponents([.year, .month, .day, .hour], from: startDate!) let todayDC = Calendar.current.dateComponents([.year, .month, .day, .hour], from: today) let startDateYMD = calendar.date(from: DateComponents(year: startDateDC.year, month: startDateDC.month, day: startDateDC.day, hour: startDateDC.hour!)) let todayYMD = calendar.date(from: DateComponents(year: todayDC.year, month: todayDC.month, day: todayDC.day,hour: todayDC.hour!)) let progressDays = calendar.dateComponents([.day], from: startDateYMD!, to: todayYMD!).day var counter = 0 for count in (0..<progressDays!){ let shotDate = self.tempData[counter].date! let shotDateDC = Calendar.current.dateComponents([.year, .month, .day,], from: shotDate) let compareDate = Calendar.current.date(byAdding: .day, value: count, to: self.tempData[0].date!)! let compareDateDC = Calendar.current.dateComponents([.year, .month, .day,], from: compareDate) if (shotDateDC.year! == compareDateDC.year!) && (shotDateDC.month! == compareDateDC.month!) && (shotDateDC.day! == compareDateDC.day!) { temps.append(self.tempData[counter].temp) if counter < self.tempData.count - 1{ counter += 1 } }else{ temps.append(0.0) } } } typealias UIViewType = LineChartView ... |
上記の編集されているコード内で利用されているDate型やCalendarクラスを利用する日付の計算、比較の方法の詳細はこちらの記事を参考にしてください
この記事では、SwiftUIでのDate型やCalendarクラスを利用する日付の計算、比較の方法を紹介しています。 日付、時刻の比較 例としてDate() で取得できる日付(実行時に今日の現時刻[…]
この状態でグラフを表示すると以下のようになります。
グラフの線が0を下回る場所でも表示されているので、setDataの表示モードを変更します。
1 2 3 4 | // set.mode = .cubicBezier 線表示を曲線で表示 set.mode = .horizontalBezier |
この変更で表示が以下のようになります。
以上、SwiftUIでのCharts_iOSを使ったグラフの実装例としてCoreDataを利用し、Y軸に温度、X軸に日付を線グラフで表示する方法を解説しました。