Objective-C 再入門

普通に iPhone アプリを使うなら swift でいいんだろうが、既存の C/C++ ライブラリを組み込んで使うような場合などは、Objective-C も検討せねばならないだろう。

特にレガシーなプロジェクトなぞ、本体が Objective-C で書かれているので、swift に移植するにせよ、ある程度の Objective-C の理解は必須だろう。

なぜ Objective-C はとっつきにくいのか?

ところで、Objective-C のクセの強さは、万人の知るところだが、これは歴史的背景を考えると理解しやすいかもしれない。

C にオブジェクト指向の思想を取り込むために、大雑把に二つのアプローチがあったようだ。

一つは言語自体の設計を変えてしまう方法。これが現在の主流で、 C++ の源流となっている。

もう一つは、C のコンパイラー自体には大きな変更を加えず、文法的な表記に特殊仕様を入れ、パース時にこれを解釈して、オブジェクトを取り扱えるようにする方法。Objective-C は、どちらかといえば、こちらだろう。

だから、C++ 感覚で取り組もうとすると訳のわからないことになってしまう。

また、Objective-C がとっつきにくいのは、ビギナー向けのほとんどのサンプルが Mac や iPhone の GUI アプリだという点。これだと、Objective-C の勉強というよりも cocoa などの勉強になってしまい、Objective-C の言語の本質みたいなものが掴みにくい。

サンプル

というわけで、GUI 一切なしのコマンドラインプログラムを試しに書いてみた。


#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <stdio.h>
// Person クラスの宣言
@interface Person : NSObject {
    NSString *name;
}
- (void)setName :(NSString *)Name;
- (void)printName;
@end
// Person クラスの実装
@implementation Person : NSObject
- (void)setName :(NSString *)Name {
    name = Name;
}
- (void)printName {
    printf("My name is %s \n", [name UTF8String]);
}
@end
// 実行プログラム
int main(void) {
    id person;
    person = [[Person alloc] init];//Person *person = new Person(); などとはしない
    [person setName:@"akiba"];
    [person printName];
    return 0;
}

これを Xcode 上で実行するとこんな感じになる。

Person クラスを実体化して、フィールド変数の name をセットした後、ゲッター経由でこの値を取得・表示するというよくあるプログラムだ。

GUI がないから、ワンファイルにすっきりと収まっているが、(Java や C++ に慣れた目から見ると)違和感を感じるところがちょいちょいある。

まず、どうということはないクラス Person でも NSObject を継承している点。またクラスを定義するときに@ディレクティブを多用している。

Objective-C の「C のコンパイラー自体には大きな変更を加えず、文法的な表記に特殊仕様を入れ、パース時にこれを解釈して、オブジェクトを取り扱えるようにする」という歴史的経緯に由来する特徴がよく出ているのではないかと思う。

ところで、最も違和感のあるのはこの箇所だろう。

    person = [[Person alloc] init];

オブジェクトに対してメッセージを送ることで機能を発現させているわけだが、これも歴史的な背景があり、 SmallTalk の影響らしい。

ただし、id 型は必ずしも使う必要はなく、

Person *person = [[Person alloc] init];

としても動きます。

また、Objective-C は C の拡張なので、C の関数も当然使える。実際、printf は C と同じように動作している。

その他、注意しておきたいところ

フィールド変数(インスタンス変数)

name のセッターは setName になってますが、フィールド変数の先頭文字を「大文字」にするのはお約束のようです。
ここら辺は Java と一緒でしょうか。

UTF8String がわからん、という声も聞きますが、name 自体は NSString (へのポインタ)です。標準 C の String に変換する必要があるので、ここで変換をかけているわけです。

なお、インスタンス変数の宣言の仕方などについては、この質疑応答が凄まじくよくまとまっています。

m と mm の違い

Objective-C のファイルは .m .mm の二つがあり得ます。
この記事がわかりやすいんですが、会員限定かな。
簡単にいえば、.m が objective-C ファイル本来の拡張子、.mm の方は C++ のファイルを取り込む際に使用する拡張子です。

継承とカテゴリ

オブジェクト指向の特徴の一つは継承だが、Objective-C はここら辺は素直だ。

よくわからんのは「カテゴリクラス」。

Person(hogehoge)

とあった場合、このクラスは元の Person のカテゴリクラスです。

元の場所とは違った場所でメソッドが追加できます。

情報の古い(アップデートされてない)記事が多い

Obj-C ユーザーが少ない理由に、記事がそもそも少ないし、あっても古い(アップデートされてない)というのも一因になっていると思う。

例えば、よく「Obj-C ではクラス変数という概念がない」などと紹介されているが、2016 以降(OS 10.12 〜)はある

公式アナウンスはこちら

使い方は別記事で説明するかもしれないが、@property(class) などとする。
若干クセはあるのだが、普通にクラス変数っぽく使える。あるじゃん。

多重継承の禁止とプロトコル

継承が出てきたので、書いておくと Obj-C では多重継承、つまり複数のクラスを親クラスにすることができないらしい。

多重継承の代わりにプロトコルで代用しなさいよ、というのがアップルの教え。
サンプルコードなどで

@interface ViewController : NSViewController<NSTableViewDataSource>

という書き方をよく見かけるが、<> 内がプロトコル。

babour

お久しぶりです。

所用でかなり放置。

ところで久々のファッションネタ。

冬のアウターは2年ほど前からモッズコート系にしていたのだが、気軽に着れるものではない。

悩みながら物色していたのだが、結局、babour (日本ではバブアーと呼ばれている)インターナショナルのパチモノ・・レプリカを購入した。

バブアー本物はメンテが面倒そうというのと現時点(2023 秋冬)では日本公式ではインターナショナルを取り扱っていないというのがその理由。

パチモノなら、オイル抜き・リプルーフも失敗を恐れずにできる(はず)。

なお、参考になった動画。

 

addsubview するときの view のお手軽な作成方法

文法ラウンドやメソッド暗記モードを経て、cocoa でアプリっぽいものを作成する機会が多くなったが、全然知られていない方法を一つ。

みんな大好き NSView だが、子供の view を作成して親 view に addsubview したいというときがしばしばある。

しかし、具体的な方法論がほとんど知られていない(笑)。

ワイは試行錯誤(といってもさほど時間はかからなかったが)の結果、以下のようにしている。

Xcode の + で UI パーツのライブラリを呼び出すダイアログを呼び出して、custom view をチョイス。

ここがキモだと思うのだが、これを親ビューが所属する Controller Scene に置く

すると上図のように Xcode 上でもしっかりと操作可能な形で配置される。

iOS 関係はわからんが、MacOS でこんな図は見たことがない。

追加した view を ChildView、元々あった view を ParentView とすると、ViewController の viewdidload あたりに

    _childview = [[ChildView alloc] initWithFrame:_parentview.frame];
    [_parentview addSubview:_childview];

としておくと、しっかりこのビューが生成される。

かなり便利な方法だと思うんだが、全然知られてないよね。

NSView の再描画

ところで MacOS の描画関係は Metal から入ったワイは、NSView は自動で再描画してくれるものだと思い込んでいたが、違うらしい。setNeedsDisplay メソッドで明示する。

 

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

です。

 

NSSplitView

今すぐどうこうというわけではないが、NSSplitView を使えるとアプリのウィンドウの表現が豊かになりそうなので、ちょっと調べる。

ネット上で使えそうなリソースは・・・

How to Use NSSplitView in a macOS app

まあ、良い記事ではあると思うけど、ちょっと応用的でしょうか。

動画もあったが、現代的ではない。

いつもの通り、公式ページもチェック。

 

結局、自分で手を動かしてまあまあ使えるようになった。

上半分は Metal の View で下半分が NSView を Core Graphics で描画したものです。
NSSplitVewDelegate プロトコルを適用するところで、おかしな挙動するんじゃないかと思ってましたが、そんなことはなく。結果オーライということで。

静止画だと伝わらないですが、ディバイダで上下の比率を変えることができます。

今回はちょっと楽して ViewController に NSSplitVewDelegate プロトコルを適用してます。

 

retain, release と -fno-objc-arc

これまで UI パーツの使い方を割と丁寧におさえてきたが、ここら辺で cocoa (最近だと AppKit というのかな?)アプリの作成に慣れるために、個人的に試験的なアプリを作成している。

ARC をオフにするには

その際に「これはいくらなんでも説明不足なんじゃ?」と思ったのが、ARC の使い方。

まず、「現在ではデフォルトで ARC がオンになっている」みたいなことは書かれているが、では、どうやって ARC をオフにしていいのか?そうすることのメリットは?みたいな説明はほとんどなされていない。

いやあ、これだと、スニペットみたいなコードは書けても、アプリを作成する際に色々と不都合が起こると思う。

その説明はいったんおいて、実用的なことを先に書くと、ARC = オンになっている Obj-C プロジェクトである部分だけをオフにしたいならば、TARGETS -> Build Phase -> Compile Sources でオフにしたいファイルの Compiler Flags に -fno-objc-arc というオプションを与える

これで、このファイルに含まれるクラスの ARC は無効になる。
あるオブジェクトを保持したければ retain (メソッド)を、破棄したければ release (メソッド)を明示する。

上の例では AppDelegate にこのオプションを与えたが、多分、これはかなり実用的な例。
ワイが試験的に作成しているアプリはまだ構造はさほど複雑ではないので、WindowsController は AppDelegate に集約している。
この時、困ったのは、あるウィンドウを閉じた時、(ARC をオンにしていると)そのウィンドウを管理しているコントローラーの存在が、不安定なように見えたこと。
ウィンドウをクローズするというのは、ウィンドウオブジェクトが破棄されたのか、それとも見えなくなっているだけでどこかにあるのかなどいくつか可能性があると思うが、この時の挙動がはっきりしない。

例えば、あるウィンドウのコントローラー(のポインタ)を wc としたとき

if(wc = nil){
  //処理
}

のようなコードを書いても、思ったような処理ができなかった。

だが、ARC をそこだけオフにしてみたところ、今の所狙ったようで動作している。

問題は使い分け

ARC に関してある程度把握はできたが、だからと言って、以降、すべて ARC=オフ にしてコーディングするかといえば、そんなことはないと思う。

まだ慣れていないせいもあると思うが、生成したすべてのオブジェクトで参照カウンタを気にするのは大変負担のように思える。

具体的な使い分けに関しては(この記事以外、ほとんど和文の資料はないと思うが)

retain と release の関係について

を参照してください。