Core Plot

これも今すぐどうこうという話ではないのだが、Core Plot という iOS/MacOS のグラフ・チャートライブラリを見つけたのでメモ。

公式リポジトリはこちら

arm Mac で使いたければ、release-2.4 のブランチを使う。

git clone -b release-2.4  git@github.com:core-plot/core-plot core-plot24

で落として、framework フォルダ内の .xocdeproj を Xcode で起動。

シェルスクリプトに不具合があるようだが、所定のファイルはできているようなので、この状態で本体自体をビルド。

CorePlot.framework が生成されるので、これを使いたいプロジェクトに取り込んで使用する。

この記事のサンプルを MacOS でビルド。

できてますね。
ソースコードは若干修正して以下のようになる。

#import <Cocoa/Cocoa.h>
#import <CorePlot/CorePlot.h>

@interface ViewController : NSViewController<CPTPieChartDataSource,CPTPieChartDelegate>

@property (readwrite, nonatomic) NSMutableArray *pieChartData;

@end

#import "ViewController.h"

@implementation ViewController

@synthesize pieChartData;

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // ホスティングビューを生成します。
    CPTGraphHostingView *hostingView = [[CPTGraphHostingView alloc]
                                        initWithFrame:CGRectMake(0, 0, 320, 320)];
    
    // グラフを生成します。
    CPTXYGraph *graph = [[CPTXYGraph alloc] initWithFrame:hostingView.bounds];
    hostingView.hostedGraph = graph;
    
    // 今回は円グラフなので、グラフの軸は使用しません。
    graph.axisSet = nil;
    
    // 円グラフのインスタンスを生成します。
    CPTPieChart *pieChart = [[CPTPieChart alloc] init];
    
    // 円グラフの半径を設定します。
    pieChart.pieRadius = 80.0;
    
    // データソースを設定します。
    pieChart.dataSource = self;
    
    // デリゲートを設定します。
    pieChart.delegate = self;
    
    // グラフに円グラフを追加します。
    [graph addPlot:pieChart];
    
    // グラフに表示するデータを生成します。
    self.pieChartData = [NSMutableArray arrayWithObjects:
                         [NSNumber numberWithDouble:40.0],
                         [NSNumber numberWithDouble:30.0],
                         [NSNumber numberWithDouble:20.0],
                         [NSNumber numberWithDouble:10.0],
                         nil];
    
    // 画面にホスティングビューを追加します。
    [self.view addSubview:hostingView];
}




// グラフに使用するデータの数を返すように実装します。
-(NSUInteger)numberOfRecordsForPlot:(CPTPlot *)plot
{
    return [self.pieChartData count];
}

// グラフに使用するデータの値を返すように実装します。
-(NSNumber *)numberForPlot:(CPTPlot *)plot field:(NSUInteger)fieldEnum recordIndex:(NSUInteger)index
{
    return [self.pieChartData objectAtIndex:index];
}
@end

です。

 

CoreData と NSOutlineView

CoreData と NSTableView に関して、いくつか書いたが、これらは練習。

NSTableView では entity 同士が relationship で繋がれたような階層的なデータは扱えないので、NSOutlineView というものが用意されている。

NSTableView の時と同様、Cell Based と View Based があり、挙動が違うようなんだが、イマイチ理解してない。
どちらが作成しやすいかといえば Cell Based 。

なお、デフォは View Based になっているようなので、NSOutlineView を貼り付けたら、右インスペクタを操作して Cell Based に設定しておく。

NSOutlineView

いろいろ調べたんだが、芯食った解説(特に Objective-C & MacOS 環境)に巡り合わなかったので、いきなり CoreDtata との連携を考えるのではなく NSOutlineView 単独で適当なデータを表示させる方向でいく。ここは慎重に。

で、作成。

クラス Person と NSMutableArray<Person *> *people を用意して *people を表示させたい。

階層がある場合には、セルクリックで展開できる。

まずは、ポイントを解説。

階層があるので、セルを表示するのに row と column では役不足。
そこで item という概念が登場する。
実態としては、この場合は *people を反映したなにかなんだが、多分、最初はなんのことかわからないと思う。

先に進もう。

NSOutlineDatasource プロトコルを適用した NSOutlineView を管理するクラスを作成する。
プロトコルを満たすために最低4つのメソッドを実装する必要がある。
なお、データを表示するだけなら、NSOutlineViewDatasource プロトコルを満たすだけでいい。delegate までは必要ない

4つのメソッドは

-(NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item;

-(BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item;

-(id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item;

-(id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item;

で、例えば、最初の outlineView: numberOfChildrenOfItem: は

-(NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item{
    //return !item ? [self.people count] :[[item children] count];
    if(item){
        return [[item children] count];
    }else{
        return [self.people count];
    }
}

となる。

DataSource と item

まだ、公式ドキュメントで確認などはしていないのだが、動作から考えるに

・DataSource としてリンクさせたクラスのコレクションがテーブルで表示させる範囲

・コレクションに含まれるクラスが item

なのかなと推測しているし、例外は経験したことがない。

上の場合で言えば、NSMutableArray<Person *> *people 自体は要素ではないから、item ではない。

推測する、とぼかしたのは、コレクションすべてを試したわけではないから。
今のところ、取り扱いが簡単な NSMutableArray で試してうまく表示できたが、他の、例えば NSDictionary などは一切試していない。というか試したが、見事にコケた。書き方が悪いのか、それとも、仕様上そうなってないのかは不明。
チャレンジされた方がいたら、教えて欲しい。

また、「コレクションに含まれるクラスが item」とは書いたが、他のクラスを Person の子要素に含んでいても問題なく動く。

異なるクラスを 親-子-孫 の関係にした場合

「他のクラスを Person の子要素に含んでいても問題なく動く」とは書いたが、Person の子要素として Child クラスを設定し、さらにその子要素として GChild を設定した場合は、ダメっぽい。

        NSString *className = NSStringFromClass([item class]);
        NSLog(@"ClassName is: %@", className);

を適当な位置に入れたが、返されるのは Person のみ。
しかし、NSStringFromClass([item class]) という書き方は便利ですね。item のところにクラスがよくわからないインスタンスに置き換えれば、そのクラス名を返してくれます。

おそらく DataSorce =  NSMutableArray<Person *> *people としたため、item のクラスとしては Person 以外取りようがないからだと思う。

しかし、DataSorce =  NSMutableArray<NSObject *> *object と一般的にすると item は任意のクラスを取りうる。しかし、このやり方で3階層以上を実装するのは頭こんがらがりそう。

まあ、大雑把に表示はできたんですが。→自己解決。別記事にします。

NSOutlineView と CoreData を結びつける

結びつけること自体は、それほど難しくないと思う。

ただ、 One To Many の関係にある relationship の Many の側はことごとく NSSet になってしまうので、いまいち使いづらい。
いい方法ないだろうか?(→ NSSet 公式ページ

クラスを生成した「後に」属性値を追加する

普通にできるっぽい。
.xcdatamodeled に attr を追加。該当するクラスに @property …. を追加する。

これができないと、ちょっとした変更をするたびに、クラスを生成する必要があるので、当然か。

実用的には、クラスと relationship をとりあえず決めておいて、細部は後で編集する、という使い方でいいかもしれない。

 

(続く)


気が早いかもしれないが、CoreData でデータも作成しておく。

いつものように Person – Child という親子関係の Entity を作成し、永続化させると sqlite には以下のように保存される。


対応するコードは以下の通り。


    Child *child = [NSEntityDescription insertNewObjectForEntityForName:@"Child"
                                                    inManagedObjectContext:context];
    child.name = @"taro";
    
    Child *child2 = [NSEntityDescription insertNewObjectForEntityForName:@"Child"
                                                   inManagedObjectContext:context];
    child2.name = @"kana";
     
    Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
                                                    inManagedObjectContext:context];
     
    person.name = @"akiba";
     
 
    NSSet *childs = [NSSet setWithObjects:child, child2, nil];
     //[person addChildrenObject:child];
    [person addChildren:childs];

Child のインスタンスを一度に複数登録する場合には、NSSet を使うので、そのように変更した。

 


アップル公式の参考情報

Xcode で NSOutlineView を使った際に右インスペクタに表示される参照情報。
気になったのでまとめた。

Delegate and Data Sources

NSOutlineViewDataSource

Navigating Hierarchical Data Using Outline and Split Views
サンプル(↓)もあるのだが、立派すぎて初学者には参考にならないかな。

akiba

CoreData と NSTableView

CoreData に関する記事はいくつか書いてきたんだが、実用的には NSTableView とリンクして使いたい。

なのだが、cocoa & Objective-C 環境で作成されたサンプルがないんだな、これが(笑)。

UITableView になってしまうが iOS & Swift なら結構あるんですけどね。
まあ、ないと iPhone アプリでデータのテーブル表示できないでしょうから、当然でしょうけど。

NSTableView 単独になってしまうけど、かろうじて見つけたのが、これ

おそるおそる現環境(MacOS Ventura, Xcode 14)でビルド。

おお、修正なしで一発動作。よしよし。

解説記事も充実している(態度豹変)。

 

このサンプルをベースに CoreData のデータと NSTableView のセルを結びつける。

結果的には、まあまあできた。

ポイントは、NSTableDataSource プロトコルと NSTableViewDelegate プロトコルに必須の二つのメソッドを実装するところでしょうか。

こういうとき「動く」サンプルは便利だと思うのだが、サンプルを少々訂正するだけでいいし、だから、その意味を掴みやすい。

ワイはこんな風に実装した。


// NSTableViewDataSource Protocol Method

- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
    //return self.numbers.count;
    return self.personname.count;
}

// NSTableViewDelegate Protocol Method

-(NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
    NSString *identifier = tableColumn.identifier;
    NSTableCellView *cell = [tableView makeViewWithIdentifier:identifier owner:self];
    
    if ([identifier isEqualToString:@"personname"]) {
        //cell.textField.stringValue = [self.numbers objectAtIndex:row];
        cell.textField.stringValue = [self.personname objectAtIndex:row];
    } else {
        cell.textField.stringValue = [self.personage objectAtIndex:row];
    }
    
    return cell;
}

元コードではセル値の供給元になっている numbers は NSArray として定義されていたが、これだと可変値は扱いにくい。データを追加・削除しやすいように NSMutableArray に変更した。

これはなかなか勉強になった。

最終的には、sqlite のデータが書き変わったら、テーブルのセル値も追従して変えてくれるようにしたいんだが、現状だとちょっと難しいかな。(よくわからない人は「NSTableView update」あたりで検索かけましょう。ただ、これ、オブジェクト間通信を使えばいいような? 設計的に美しくないし、可読性落ちるけどさ→やはり通知でできましたとさ)

 

 

cmake で framework を生成する

この前の記事で、cmake のプロジェクトで cocoa の framework を作成するオプションを見つけたので試してみる。

framework の生成

必ずしも info.plist は必須ではないようだ。
サンプルのうち、以下のように info.plist と CODE_SIGN の部分を潰しても framework は生成される。

CMakeLists.txt
cmake_minimum_required(VERSION 3.13)
project(TestFramework C)
add_libraryFr(TestFamework SHARED
 TestFramework.h
 TestFramework.c)

set_target_properties(TestFramework PROPERTIES
  FRAMEWORK TRUE
  FRAMEWORK_VERSION C
  MACOSX_FRAMEWORK_IDENTIFIER com.akiba.TestFramework
  #MACOSX_FRAMEWORK_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist
  # "current version" in semantic format in Mach-O binary file
  #VERSION 16.4.0
  # "compatibility version" in semantic format in Mach-O binary file
  #SOVERSION 1.0.0
  PUBLIC_HEADER TestFramework.h
  #XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Developer ID Application: Taro Akiba(123xxx789)"
)

この状態で

cmake -S . -B build

としても Makefile は生成される。もちろん make で TestFramework.framework も生成される。生成するだけならば

FRAMEWORK TRUE

とするだけでいいようだ。

生成された framework を使ってみる

とりあえずお試しなので TestFramework.h と TestFramework.c は以下のような簡単なものにした。

TestFramework.h
#ifndef TESTFRAMEWORK_H
#define TESTFRAMEWORK_H

int twice(int);

#endif
TestFramework.c
#include <stdio.h>
#include "TestFramework.h"

int twice(int n) {
    return 2*n;
}

これで、cmake を走らせると確かに TestFramework.framework はできている。

次に Xcode で別のプロジェクトを作成して TestFramework.framework を取り込む。

main.m から以下のように使う。

#import <Foundation/Foundation.h>
#import <TestFramework/TestFramework.h>

int main(int argc, const char * argv[]) {
    
    NSLog(@"%2d",twice(3));
    
    return 0;
}

走らせてみると・・

予想通り 6 と表示してくれました。

成功、成功。

rpath の設定

なお、設定にもよるのだが、こうして作成されたコマンドラインプログラムを(Xcode 上ではなくて)ターミナルから実行すると

Library not loaded: @rpath

というエラーが出る時がある。
このエラーは、コンパイル時の @rpath の設定が実際に生成されたプログラムが参照すべきライブラリ(今回は TestFramework)の path と一致していない時に発生する。

その場合は、Xcode の Runpath Search Paths あたりの設定を見直してください。

ここ、指定しておかないと framework の所定の置き場所しか見に行かないようです。

正しく設定できていれば、ターミナル上からも

と Xcode 環境で走らせた時と同様の結果になります。