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) などとする。
若干クセはあるのだが、普通にクラス変数っぽく使える。あるじゃん。

また、objective-c 自体も文法が(主に 2010 年代に)モダンに改変されており、@property や @synthesize なども使い方が微妙に変わっている。

この図はこちらの記事からお借りしてきました。わかりやすいですね、感謝!

 

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

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

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

@interface ViewController : NSViewController<NSTableViewDataSource>

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


このまま続けてもいいのだが、まとまり悪くなるのでここでいったん切ろう。

ワイもそうだが、IT 業界にいる限り Objective-C だけいじっていればいいというものではない。
というか実際に多い案件は、別の言語だったりする。

だから、この記事は「ちょっと objective-C 思い出すときに読み返す」ときに役にたつ内容にしておく。

その他

@property 表記

アクセサー(ゲッターやセッターのこと)を定義できる。慣れれば便利なのだろうが、Java あたり読み書きしているとすぐにこの規約を忘れる。

この記事読んでね。

クラスエクステンションの () 表記

忘れやすいのがクラスエクステンションの時に使われる () 。

詳しくはこの記事あたり参照。

なんでヘッダーで定義された宣言を .m ファイルでもう一回やる必要があるのか?疑問に思ったら読むといいと思う。

Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
// 名前
@property (nonatomic) NSString *name;
// 名前と年齢をログに出力する
- (void)displayProfile;
@end
Person.m
#import <Foundation/Foundation.h>
@interface Person ()
// 年齢
@property (nonatomic) NSInteger age;
// 名前をログに出力する
- (void)displayName;
// 年齢をログに出力する
- (void)displayAge;
@end
@implementation Person
……
@end

要するに Obj-C には Public だの Private がないため、ヘッダーファイルではなく実体ファイルの方でメソッド・メンバー変数を定義することで外部クラスからのアクセス制限を行っているという理屈です。

Person.h
@interface Person: NSObject
@end

Person.m
@interface Person()
@end

@implementation Person
@end

という構造が見えてますか?

強調して書くとこういう仕組み。

強調して、と書いたがプロジェクトを作成するとき AppDelegate は () をつけて生成してくれますね。

 

 

 

 

 

 

カテゴリの () 表記

ここが紛らわしいと思うのだが、@interface () はクラスエクステンションだけでなくカテゴリでも使われる

ただ、こちらはファイル名自体が ClassA+ClassB.h のようになっているので、それで判断できるかな。

カテゴリーとクラスエクステンション』参照。

 

 

NSUserDefaults

サンプル

AppDelegate.m
@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; // 取得
    [ud setObject:@"hoge" forKey:@"KEY_S"];
    [ud synchronize];
}

 

参考:https://glassonion.hatenablog.com/entry/20110920/1316473990

保存場所

Non SandBox: /Users/ユーザ名/Library/Preferences

SandBox: /Users/ユーザ名/Library/Containers/アプリ名/Data/Library/Preferences/

なのだそうだが、デバッグ時にそれらしいファイルはなかったな???

→registerDefaults メソッドはファイルに書き出しません!

しかし、standard と shared の違いがわからんなあ。

→shared は文字どおり「共有」ということらしい。
何と共有するかは、この記事によれば、同じグループ間で、ということらしいのだが、使ったことはない。

 

Objective-C 諸々

objective-c や cocoa の割と独自仕様っぽいところ。

delegate

見かける割にいまいち理解できていなかったデリゲート。

「MTKView のデリゲートがレンダラー」と気がつき、ああそういうことかと納得。

大抵の場合、ViewController が MTKView を呼び出す前に

_view.delegate = _renderer;

などとしているはずだ。

メタルの表示用ビューとして MTKView は存在しているが、各アプリでのこのビューの使い方は様々だろうから、そこら辺は自分で DrawInMTKView を実装してね、プロトコルは準備しておくから・・・ということなんだと思う。

わかりにくくなるのは、プロトコルが明示されているわけではないので、その仕組みに気が付きにくいからじゃないでしょうか。

本来であれば以下のような記述が必要なようだ。

委譲する側(のヘッダ)

@protocol HogeDelegate

@property (weak, nonatomic) id delegate;

@protpcol HogeDelegate <NSObject>
-(void)FugaMethod;
@end

以下のように明示的にぶん投げるときもある。

委譲する側(の .m ファイル)

[self.delegate FugaMethod] //こうやって使うようだ

ぶん投げられる側はプロトコル準拠を明示しておく。

委譲される側(のヘッダ)

@interface Fuga: NSObject <HogeDelegate>

ところで、これが面白いのは、ある種の情報の通知として利用できる点。
上の例なら、Hoge の情報を Fuga で受けられる。
ViewerController に受け持たせてしまい、あるオブジェクトでの情報をビューに反映させるといった使い方ができる。

参考:https://dev.classmethod.jp/articles/ios-delegate/

マルチスレッド処理

NSTread を使う方法、dispatch_async – queue の系を使う方法などいくつかあるようだ。

(続く)

KVO

https://qiita.com/mitsu9/items/e738c41a8d87cc71ec3e

KVOとはキー値監視(Key-Value Observing)のことを指します。
アプリ開発をかじったことのある人ならMVCという言葉を聞いたことがあると思いますが、KVOはModelの変更をControllerやViewに通知し、反映させるための仕組みです。
他の通知システムとしてはdelegateやNSNotificationを利用した方法などがあります。

なるほど。

サンプルとして MCV を意識したメモアプリあり。

インスタンス変数の宣言

Objective-Cでは、インスタンス変数をどこに宣言するのが正しいのか? 』が興味深かった。

やはり、みなさん疑問に思っていたんですね。

 

 

C での配列の取り扱い

C での配列の取り扱い

C で配列使う時は注意が必要!

と、このブログでも以前に書いたが、やってもうた…

まあ、実際には未遂だったんだが。

 

しかし、エグいのは構造体のメンバーに vector_float3 を代入するような場合で、こう書かないとエラーが出る。

structObj[0].position=(vector_float3){0.5, -0.5, -0.5};

単に {0.5, -0.5, -0.5} と書いたのではダメのようだ。

配列の要素が数個なら後で代入してもそれほど手間ではないが、10 以上になったら宣言と同時に初期化しておかないと実用的ではないように思う。

また、構造体をグローバルで使いたい場合は、諦めた方が賢明かもしれない。

メソッドの中で static const で定義し、ポインタ使ってメソッド間で受け渡すしかないような気がする。

Metal で複数のオブジェクトを描画する

見出しのことをやろうとすると難しくないか?

というのが最近の問題意識。

二つのポリゴンというような意味ではなく、一つはポリゴン、もう一つは 3D Texture というように描画原理が異なるもののような場合。

大抵のサンプルが一つの描画原理でしか取り扱っていないからなあ。

どうしたものやら。

 

 

Mac アプリとメニュ

Mac アプリでメニュの実装をする場合、一番簡単なのは AppDelegate に書いてしまうこと。

https://qiita.com/imk2o/items/436839153530dbcb7ed1

やり方は上記記事を参考にしてほしい。

デフォルトメニューの First Responder の担当オブジェクトは AppDelegate になっているのだから、First Responder に testaction という responder を登録し、AppDelegate に以下の実装をしておけば、実行してくれる。

- (IBAction)testaction:(id)sender
{
    NSLog(@"test action");
}
AppDelegate.m

図がわかりやすい。

 

NSOutlineViewDelegate プロトコル

NSOutlineView 関連のプロトコルでは、DataSource の方は存在感があるが Delegate の影は薄い。

まあ、DataSource の方はないと表示すらできないわけだが…。

試しに

@interface OutlineviewManager : NSObject<NSOutlineViewDataSource>

@interface OutlineviewManager : NSObject<NSOutlineViewDataSource, NSOutlineViewDelegate>

としてもコンパイル自体はできる。

行選択時の機能

公式ページを見るに、行選択をした時のメソッドを供給しているようです。

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

のメソッドは、公式の説明だと item が選択できるかどうかを定義するもののように紹介されていますが、おそらく機能も提供できます。
OutlineView に Person クラス(のインスタンス)を表示させているとき

- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item
{
Person *p = item;
NSLog(@"%@", p.name);

return YES;
}

を実装しておくと name プロパティの値(文字列)を表示してくれます。