カテゴリー別アーカイブ: program

プログラム書いてて気づいた事など。

[iOS]Json形式のテキストを読み込んでNSDictionaryにする

このメモはSBJson frameworksに関するもの。
記述時点での正式リリースバージョン3.0.4
参考:http://stig.github.com/json-framework/api/3.0/
ARC適用する場合は3.1α以降でなければ動作しない。
http://ip7.biz/wordpress/?p=1008

導入手順

  1. ターミナルを立ち上げる
  2. gitから入手すべく以下を入力してENTER
    • git clone git://github.com/stig/json-framework.git
  3. DLしたファイルを解凍し、解凍したファイル下のClassフォルダ内のファイルを自分のプロジェクトフォルダにD&Dしてコピーする。
    • もしビルドが通らないのであれば、、Build Phases のCompile SourcesにSBJon関連全ファイル(.m,.h)を追加することでBuildを通す事が出来る。
    • この時のエラーメッセージ
      _OBJC_CLASS_$_SBJsonParser", referenced from:
  4. Json形式のファイルを扱うクラス(.m)内で以下を記述
    • #import 
  5. JSON形式のファイルを扱うクラスヘッダ内で以下を記述
    • @class SBJsonStreamParser;
    • @class SBJsonStreamParserAdapter;

テキストファイルから読み込むんでデータ化

テキストファイルから読み込む

        //read file
        NSString* fileName  = [NSString stringWithFormat:@"dummyData"];
        NSString* path      = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Documents"];
        path = [path stringByAppendingPathComponent:fileName];
        NSData* data = [NSData dataWithContentsOfFile:path];

読めてなかったら強制終了

if(data == nil) abort();

データを文字列にエンコード

        //Data encode to NSString.
        NSString* dataStr = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"data = %@",dataStr);

SBJsonでパースする

        //SBJson parse to NSDictionary.
        SBJsonParser* sbjsonparser =[[SBJsonParser alloc]init];
        NSError* error;
        error = nil;
        NSDictionary* dic = [sbjsonparser objectWithString:dataStr error:&error];
        NSLog(@"JSON dictionary=%@", [dic description]);

テスト

読み込んだテキストファイルの中身

{
"buttonArray":[
    {
      "word":"キウイ",
      "imgPath":"greenButtons_0000s_0003.png"
    },
    {
      "word":"うみがめ",
      "imgPath":"redButtons_0000s_0003.png"
    }
    ]
}

動作確認した際に出た文字列

CoreData[39477:fb03] JSON dictionary={
    buttonArray =     (
                {
            imgPath = "babylogButtons_0000s_0003.png";
            word = "U30adU30a6U30a4";
        },
                {
            imgPath = "babylogButtons_0000s_0003.png";
            word = "U3044U3061U3054";
        }
    );
}

JavaScript本格入門 ~モダンスタイルによる基礎からAjax・jQueryまで

[ObjC]ファイルの読み書きとNSHomeDirectory()と[[NSBundle mainBundle]resourcePath]

テキストファイルの読み込みでどうしようもないところでハマったので戒めを込めてちゃんとまとめておく。

やろうとしたこと

テキストファイルを読み込む
テキストファイルはコンパイル時には存在している
起動と同時に読み込んでそれを保持する。

ハマったところ

App内がファインダから参照できずにAppと同じディレクトリに置いてあるDocumentsフォルダが自前で用意したものと混同。
参照しても容易したファイルが存在せず、起動時も落ちる。
原因は[[NSBundle mainBundle]resourcePaht]で取得するべきパスをNSHomeDirectory()で取得していた事。
だって読み込み用のサンプルにはNSHomeDirectory()ベースで記述されてるんだもの…。

参考にしたソース

ボタンを押すとボタンのタグに応じて書込み/読み込みが実行される。

//ボタンクリック時に呼ぶ
- (IBAction)clickButton:(UIButton*)sender {
    if (sender.tag==0 {
        NSData* data=[self str2data:_textField.text];
        [self data2file:data fileName:@"test.txt"];
        NSLog(@"data = %@",data);
    } else if (sender.tag==1 {
        NSData* data=[self file2data:@"test.txt"];
        [_textField setText:[self data2str:data]];
        NSLog(@"data = %@",data);
    }
}


//バイト配列の書き込み
- (BOOL)data2file:(NSData*)data fileName:(NSString*)fileName {
    NSString* path=[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"];
    path=[path stringByAppendingPathComponent:fileName];
    NSLog(@"path : %@",path);
    return [data writeToFile:path atomically:YES];
}

//バイト配列の読み込み
- (NSData*)file2data:(NSString*)fileName {
    NSString* path=[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"];
    path=[path stringByAppendingPathComponent:fileName];
    NSLog(@"path : %@",path);
    return [NSData dataWithContentsOfFile:path];
}

コードの解説

-(IBAction)clickButton:(UIButton*)sender

ボタンが押された際にボタンに付属したタグに応じて処理を分岐させている。

- (BOOL)data2file:(NSData*)data fileName:(NSString*)fileName

分岐した先での処理。保存するパスをNSHomeDirectory()でルートを取得。
その後Documentsディレクトリ下を保存場所にして文字列化。
さらにファイル名を追加。ファイル名は引数で渡されている文字列を使用。
最後に保存時の結果を返す。

- (NSData*)file2data:(NSString*)fileName

書込み同様に読み込み先をDocuments下のディレクトリに指定。
ファイル名も指定。
最後に読み込んだデータを返す。

改変

データを書込みしてから読み込み、という流れであればNSHomeDirectory()ベースで良い。
なぜなら保存用ディレクトリであるDocumentsフォルダ内にファイルを置いているし、読み書きすべきところはそこだから。

やりたかったのは書き込みをする前からファイルが存在していて、それを読み込んでほしかった。
よって以下のように内容を修正

//バイト配列の読み込み
- (NSData*)file2data:(NSString*)fileName {
    NSString* path=[[[NSBundle mainBundle]resourcePath] stringByAppendingPathComponent:@"Documents"];
    path=[path stringByAppendingPathComponent:fileName];
    NSLog(@"path : %@",path);
    return [NSData dataWithContentsOfFile:path];
}

[[NSBundle mainBundle]resourcePath]をベースとしたディレクトリ内のファイルを読むように記述を変更。
App/Documents/data.txtというファイルが合った場合は

        NSData *data;
        NSString* fileName = [NSString stringWithFormat:@"data.txt"];
        NSString* path;
        path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Documents"];
        path = [path stringByAppendingPathComponent:fileName];
        data = [NSData dataWithContentsOfFile:path];

という記述で(NSData)data内にデータが格納される。

以降の処理は

        if(data == nil) abort();

これを挟んでから処理を記述した。
ファイルの読み込みに失敗するとその場でアプリが落ちるのでバグに気づく。

NSHomeDirectory()と[[NSBundle mainBundle] resourcePath]

前者のNSHomeDirectoryは.appが置いてあるディレクトリを指す関数で、アプリが自分で保存したファイル等を読み書きする際に使用する。
一方後者のディレクトリは.app内をRootとしたディレクトリで一つ階層が下になる。

プロジェクトファイルのTARGETSから対象を選択し、BuildPhaseから確認できるCopy Bundle Resourcesの項目が後者で取得したパスから見えるファイル群。
なのでここにあるファイルは前述のソースで言うところのNSHomeDirectory()を後者のメソッドに置き換える事で参照が可能になる。
NSHomeDirectory()
写真はiPhoneシミュレータの中のディレクトリ。
NSBundle (ry で指定された場所はAppより下になる。
NSHomeDirectory()で取得できるパスはAppやDocuments等がある一つ上の階層で直接ファインダーで確認できる。

参考

この本にファイル読み書きのサンプルが載っていたおかげで助かった。
iPhone/iPad/iPod touchプログラミングバイブル―iOS 5/Xcode 4対応 (smart phone programming bible)

UIScrollViewを使ってちゃんとスクロールさせる手順

達成した事があったにもかかわらずちょっとハマってしまったのできちんとメモって二度轍しないようにするための記事。

UIScrollViewを扱うにあたって最低限しておかなければ成らない事

  • 3重の入れ子構造にする
    • Window
      • UIScrollView
        • contentView
          • 実際に見せたいView群

やる事

  1. UIScrollViewを生成
  2. ScrollViewの上に乗せるコンテンツのViewを生成
  3. ScrollViewにaddSubViewでコンテンツのViewを乗せる。
  4. ScrollViewに必要なビューを乗せたいだけ沢山乗せる(載せる)。
  5. ScrollViewの可動域を指定する。
  6. 現在のViewにはUIScrollViewを上書きする。

サンプルコード

– (void)loadView
{
[super loadView];

UIScrollView* scv = [[UIScrollView alloc]initWithFrame:CGRectMake(0, 0, 320, 520)];     //1
scv.scrollEnabled = YES;//4のいちぶ
scv.backgroundColor = [UIColor redColor];

UIView* mainView = [[UIView alloc]initWithFrame:CGRectMake(0,0,320, 7000)];//2
mainView.backgroundColor = [UIColor yellowColor];
//整形用の値
float margineX = 5.0;
float margineY = 5.0;
float buttonWith = 310.0;
float buttonHeight = 70.0;
float margine = 5.0;
float titleHeight = 200.0;

UILabel* titleImage = [[UILabel alloc]initWithFrame:CGRectMake(0,0,320,titleHeight)];
titleImage.backgroundColor = [UIColor blueColor];
titleImage.text = [[NSString alloc]initWithString:@”Title”];
titleImage.textColor = [UIColor whiteColor];
titleImage.font = [[UIFont alloc]fontWithSize:32];
[mainView addSubview:titleImage];//3,4

margine += titleHeight + margineY;

UIButton* btnCamera = [self makeButton:CGRectMake(margineX, margine,buttonWith,buttonHeight) text:@”Camera” tag:BTN_CAMERA];
[mainView addSubview:btnCamera];//3,4

margine += buttonHeight + margineY;

UIButton* btnPhoto = [self makeButton:CGRectMake(margineX,margine,buttonWith,buttonHeight) text:@”Photo” tag:BTN_READ];
[mainView addSubview:btnPhoto];//3,4

margine += buttonHeight + margineY;

UIButton* btnSaved = [self makeButton:CGRectMake(margineX,margine,buttonWith,buttonHeight) text:@”saved” tag:BTN_WRITE];
[mainView addSubview:btnSaved];//3,4

margine += buttonHeight + margineY;

UIButton* btnGraph = [self makeButton:CGRectMake(margineX,margine,buttonWith,buttonHeight) text:@”Graph” tag:BTN_WRITE];
[mainView addSubview:btnGraph];//3,4

margine += buttonHeight + margineY;

[scv setContentSize:CGSizeMake(320.0, 500.0)];//5
[scv addSubview:mainView];
self.view = scv;//6

}

margineの部分の処理はBlockにした方が美しいよね…。

参考

図入りで説明されてて分かりやすいし、ズームのさせ方まで載ってるので勉強になった。
テン*シー*シー:http://ameblo.jp/xcc/entry-10322378932.html

【Unity】簡易AnimationViewer

簡易アニメーションビューワを作成してみたのでサンプルコードとその解説を残す。

MotionVIewer Screen Shot 0024-03-26 at 17.20.10

やること

  • 新しいシーンを作成
  • モーション付きのモデルをインスタンシング
  • CharacterControllerコンポーネントを追加
  • スクリプト追加

新しいシーンを作成

Command + Shift + N で新しいシーンを作成する。

シーンビューにはカメラしか写ってない。

MotionVIewer Screen Shot 0024-03-26 at 15.29.16

モーション付きのモデルをインスタンシング

アセットストア等からデータを入手できるので入手しておく。

これをプロジェクトビューからシーンビューかHierarchyにD&Dでインスタンシングする。

MotionVIewer Screen Shot 0024-03-26 at 17.33.01

インスタンシングされたオブジェクトを選択し、Inspectorに表示されたTransformの座標を全てゼロにしておく。

この時点でモノによってはカメラに納まっている。

CharacterControllerコンポーネントを追加

Component->Physics->Character Controllerを選択し、先にインスタンシングしたオブジェクトにコンポーネントを追加する。

Inspector の AnimationsにあるElementを見てアニメーションがちゃんと存在するを確認しておく。

MotionVIewer Screen Shot 0024-03-26 at 15.42.29

スクリプトの追加

下記のスクリプトをコピペして追加する。

#pragma strict
var roteSpeed:float = 3.5;

private var side : float = 1.0;
private var animationSpeed:float;
private var animationCount:uint;
private var animationList:Array;
function Start () {
     print("animationGetCount:" + animation.GetClipCount());
     print(animation.clip.name);
     animationCount = animation.GetClipCount();
     print(gameObject.animation);
     animationList = GetAnimationList();
}

function Update () {
     if( -0.9 < transform.rotation.y && transform.rotation.y < 0.9 ){
     }else{
          side *= -1;
     }
     transform.rotation.y += roteSpeed/180 * side;

}

function OnGUI (){
     var sw : int = Screen.width;
     var sh : int = Screen.height;
     var margin : int = 10;
     //ghool position
     var ghoolRoteYLabel:Rect = Rect(0,margin + 0,sw,sh/10);
     GUI.Label(ghoolRoteYLabel,"Rote Y:"+ transform.rotation.y);

     margin += 10;

     //slider
     var playSpeedRect:Rect = Rect(0,margin + 30,sw,sh/10);
     animationSpeed = GUI.HorizontalSlider(playSpeedRect,animationSpeed,0.0,5.0);
     for (var state : AnimationState in animation)
     {
          state.speed = animationSpeed;
     }
     GUI.Label(playSpeedRect,"playSpeed :"+ animationSpeed);

     margin += 100;

     //Buttons
     var buttonSpace:int = 40;
     var rectWidth:int = 100;
     var rectHeight:int = 40;
     var max:int = 10;
     var rects:Array = new Array();
     var i:int = 0;

     for (var name : String in animationList)
     {
          var rect:Rect = Rect(15,margin + 20*i + buttonSpace*i, rectWidth,rectHeight);
          if(GUI.Button(rect,animationList[i].ToString())){
               animation.CrossFade(animationList[i],0.01);
          }
          i++;
     }
}

private function GetAnimationList():Array
{
     var tmpArray = new Array();
     for (var state : AnimationState in gameObject.animation)
     {
          tmpArray.Add(state.name);
     }
     return tmpArray;
}

これを先のインスタンスしたオブジェクトにD&Dするとコンポーネントとして追加される。
再生ボタンで実行を行うと保持しているモーション一覧がボタン化されて列記される。
それらをクリックするとモーションが変更される。
ループ指定がOffだとモーションは一度再生された後に最終フレームで停止する。
スライダは再生速度を変動させる。ループ再生しているモーションを確認する際は便利。

MotionVIewer Screen Shot 0024-03-26 at 17.54.11

スクリプトの解説

Startメソッドで各種初期化。

var roteSpeed:float = 3.5;

private var side : float = 1.0;
private var animationSpeed:float;
private var animationCount:uint;
private var animationList:Array;
function Start () {
     print("animationGetCount:" + animation.GetClipCount());
     print(animation.clip.name);
     animationCount = animation.GetClipCount();
     print(gameObject.animation);
     animationList = GetAnimationList();
}

自身の保持するClip(アニメーション1個の単位)の数を取得。
同時に自作の関数でClipの名前を配列で取得。

UpdateメソッドではキャラのY軸上の回転を行わせている。

function Update () {
     if( -0.9 < transform.rotation.y && transform.rotation.y < 0.9 ){
     }else{
          side *= -1;
     }
     transform.rotation.y += roteSpeed/180 * side;
}

OnGUIメソッドではUI表示。

function OnGUI (){
     var sw : int = Screen.width;
     var sh : int = Screen.height;
     var margin : int = 10;
     //ghool position
     var ghoolRoteYLabel:Rect = Rect(0,margin + 0,sw,sh/10);
     GUI.Label(ghoolRoteYLabel,"Rote Y:"+ transform.rotation.y);

     margin += 10;

     //slider
     var playSpeedRect:Rect = Rect(0,margin + 30,sw,sh/10);
     animationSpeed = GUI.HorizontalSlider(playSpeedRect,animationSpeed,0.0,5.0);
     for (var state : AnimationState in animation)
     {
          state.speed = animationSpeed;
     }
     GUI.Label(playSpeedRect,"playSpeed :"+ animationSpeed);

     margin += 100;

     //Buttons
     var buttonSpace:int = 40;
     var rectWidth:int = 100;
     var rectHeight:int = 40;
     var max:int = 10;
     var rects:Array = new Array();
     var i:int = 0;

     for (var name : String in animationList)
     {
          var rect:Rect = Rect(15,margin + 20*i + buttonSpace*i, rectWidth,rectHeight);
          if(GUI.Button(rect,animationList[i].ToString())){
               animation.CrossFade(animationList[i],0.01);
          }
          i++;
     }
}

スライダはアニメーションの再生速度の変更を担う。
変更時に保持するアニメーション全ての再生速度を同時に変更する。
その後、自動でアニメーション数に応じてボタンを生成。
ボタン押下時の効果はクロスフェードさせてアニメーションをスイッチする、という内容。
アニメーションの名前が分からなくてもElement#で拾えてたと思ってサンプル探したけど出て来ず。
仕方なくAnswersで検索したら発見。コピペ。

GetAnimationListメソッド

private function GetAnimationList():Array
{
     var tmpArray = new Array();
     for (var state : AnimationState in gameObject.animation)
     {
          tmpArray.Add(state.name);
     }
     return tmpArray;
}

for inで、gameObjectが保持するAnimationState形式のデータを全て取得する。
取得したAnimationState各種のClipのnameプロパティ(任意で指定可能な文字列)を取得して配列に格納している。
ループが完了したら配列は返される。

参考

Xcode4.2デバッグ環境構築とgdbのチートシート

main内で止まるとバグを追いにくくてハマるのでできるだけ追いやすい様にと色々調べてみた。

  1. どんなデバッグ方法があるのか知る
  2. デバッグ向きの環境に作り替える
  3. gdbを触れる様にしとく

どんなデバッグ方法があるのか知る

NSLogだけじゃない、こんな方法あんな方法、まず知っておく。

デバッグ向き環境に作り替える

Xcode左上のプロジェクトを選択して環境

12_02_01-02

 

3つの環境変数を追加できたらOKボタン。

12_02_01-02

gdbを触れる様にしておく

gdbはデバッグ時に使えるコマンド。
gdb_window

Xcodeの最下部のコンソールに入力して様々な情報を得る事ができる。

 

詳しくはApple のdebugging with gdbを参照。

チートシートも見つけたのでよかったらどうぞ。
http://darkdust.net/files/GDB%20Cheat%20Sheet.pdf

 

ObjCの継承に関する疑問とselfの暴走とか

詳細Objective-C2.0第3版を読み進めてます。

詳解 Objective-C 2.0 第3版
詳解 Objective-C 2.0 第3版
posted with amazlet at 12.01.26
荻原 剛志
ソフトバンククリエイティブ
売り上げランキング: 7869

その中の継承に関する項目にて。
A->B->Cという継承構造でsuperをつかうとCからAのメソッドを呼び出せるよ!みたいな事が書かれてました。

文献上の例だとAからCの3つですが、そもそもクラスは全てNSObjectから継承してるはずなので、Superいっぱつで親飛び越して先祖を参照する、っていっきにNSObjectを参照することになるの?どこまで上位を参照しにいくんだ?とか疑問が。

結論から言うと、継承を続けている際に、直近のOverrideされたメソッドを使用する、という挙動でした。
それ以外にも色々勘違い解消できました。

Screen Shot 0024-01-26 at 12.47.21-2

試してみた

実際に、以下の様にサンプルを作ってコンソールに経過を吐かせて動作を確認します。

A.h

#import 

@interface A : NSObject{
}

- (void)method1;
- (void)method2;
- (void)method3;

@end

A.m

#import "A.h"

@implementation A
@synthesize One = One_;
@synthesize Two = Two_;
@synthesize Three = Three_;

-(void)method1{
  NSLog(@"A::method1");
  [self method2];
  [self method3];
}
-(void)method2{
  NSLog(@"A::method2");
}
-(void)method3{
  NSLog(@"A::method3");
}
@end

B.h

#import "A.h"

@interface B : A
-(void)method1;
@end

B.m

#import "B.h"

@implementation B

//*
-(void)method1{
  NSLog(@"B::method1");
}
// */
@end

C.h

#import "B.h"

@interface C : B

-(void)method2;
@end

C.m

#import "C.h"

@implementation C

-(void)method2{
  NSLog(@"C::method2");
  NSLog(@"next line call super method 1");
  [super method1];
}
@end

AppDelegate.h

#import 

@interface AppDelegate : UIResponder 

@property (strong, nonatomic) UIWindow *window;

@end

AppDelegate.m

“- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions”に以下を追加で。

  A* a = [[[A alloc]init]autorelease];
  B* b = [[[B alloc]init]autorelease];
  C* c = [[[C alloc]init]autorelease];

  NSLog(@"a method");
  [a method1];
  [a method2];
  [a method3];
  NSLog(@"b method");
  [b method1];
  [b method2];
  [b method3];
  NSLog(@"c method");
  ;//←WPのバグで余計なのでてるので注意
  ;//←WPのバグで余計なのでてるので注意
  ;//←WPのバグで余計なのでてるので注意
  NSLog(@"end");

実行するとコンソールには以下の様に。

2012-01-26 12:51:36.090 classTest[51229:f803] a method
2012-01-26 12:51:36.092 classTest[51229:f803] A::method1
2012-01-26 12:51:36.092 classTest[51229:f803] A::method2
2012-01-26 12:51:36.093 classTest[51229:f803] A::method3
2012-01-26 12:51:36.094 classTest[51229:f803] A::method2
2012-01-26 12:51:36.094 classTest[51229:f803] A::method3
2012-01-26 12:51:36.095 classTest[51229:f803] b method
2012-01-26 12:51:36.107 classTest[51229:f803] B::method1
2012-01-26 12:51:36.109 classTest[51229:f803] A::method2
2012-01-26 12:51:36.110 classTest[51229:f803] A::method3
2012-01-26 12:51:36.111 classTest[51229:f803] c method
2012-01-26 12:51:36.113 classTest[51229:f803] B::method1
2012-01-26 12:51:36.114 classTest[51229:f803] C::method2
2012-01-26 12:51:36.114 classTest[51229:f803] next line call super method 1
2012-01-26 12:51:36.117 classTest[51229:f803] B::method1
2012-01-26 12:51:36.117 classTest[51229:f803] A::method3
2012-01-26 12:51:36.118 classTest[51229:f803] end
2012-01-26 12:51:36.119 classTest[51229:f803] Applications

ここで注目。Cクラスが送ったメッセージ[super method1]はどこに届いたか?
答え:Bクラスのオブジェクト。

つまり、superで指定した先は直近のクラスでした。

じゃ、もっと上のクラスのを参照するには?

とりあえずBクラスでOverrideしたmethod1をコメントアウトします。

B.h

#import "A.h"

@interface B : A
//-(void)method1;
@end

B.m

#import "B.h"

@implementation B
/*
-(void)method1{
  NSLog(@"B::method1");
}
// */
@end

もういっかい立ち上げる…とコンソールが激しいことに!!

2012-01-26 12:55:10.106 classTest[51311:f803] a method
2012-01-26 12:55:10.109 classTest[51311:f803] A::method1
2012-01-26 12:55:10.110 classTest[51311:f803] A::method2
2012-01-26 12:55:10.110 classTest[51311:f803] A::method3
2012-01-26 12:55:10.111 classTest[51311:f803] A::method2
2012-01-26 12:55:10.111 classTest[51311:f803] A::method3
2012-01-26 12:55:10.112 classTest[51311:f803] b method
2012-01-26 12:55:10.112 classTest[51311:f803] A::method1
2012-01-26 12:55:10.113 classTest[51311:f803] A::method2
2012-01-26 12:55:10.113 classTest[51311:f803] A::method3
2012-01-26 12:55:10.114 classTest[51311:f803] A::method2
2012-01-26 12:55:10.114 classTest[51311:f803] A::method3
2012-01-26 12:55:10.115 classTest[51311:f803] c method
2012-01-26 12:55:10.115 classTest[51311:f803] A::method1
2012-01-26 12:55:10.116 classTest[51311:f803] C::method2
2012-01-26 12:55:10.117 classTest[51311:f803] next line call super method 1
2012-01-26 12:55:10.117 classTest[51311:f803] A::method1
2012-01-26 12:55:10.118 classTest[51311:f803] C::method2
2012-01-26 12:55:10.118 classTest[51311:f803] next line call super method 1
2012-01-26 12:55:10.154 classTest[51311:f803] A::method1
2012-01-26 12:55:10.155 classTest[51311:f803] C::method2
2012-01-26 12:55:10.155 classTest[51311:f803] next line call super method 1
2012-01-26 12:55:10.156 classTest[51311:f803] A::method1
2012-01-26 12:55:10.162 classTest[51311:f803] C::method2
2012-01-26 12:55:10.163 classTest[51311:f803] next line call super method 1
2012-01-26 12:55:10.163 classTest[51311:f803] A::method1
2012-01-26 12:55:10.164 classTest[51311:f803] C::method2
2012-01-26 12:55:10.164 classTest[51311:f803] next line call super method 1

途中から再帰呼び出し状態に。

これはCクラスのmethod2で呼び出した [super method1];はAクラスのmethod1がselfのmethod2を呼び出している。

-(void)method1{
  NSLog(@"A::method1");
  [self method2];
  [self method3];
}

つまりCクラスのmethod2を呼び出し、再度Aクラスのmethod1が呼ばれ、という再帰状態に。

あれ?selfってthisみたいなものじゃなかったの?

とどのつまり

継承に関しては直近の先祖の中でOverrideされたものを適用する、という事がわかりました。
superはOverrideされたか否かで参照先が変動するということです。

そして、自身が犯した勘違いは、selfはthisと同じ様に、自身を参照するのかと思っていた点。
実際は、呼び出したオブジェクトのメソッドをコールするのでした。

この挙動を実現するにあたって、存在しないメソッドをコールした際に動作が停止しないで無視される言語仕様にした理由がわかりました。

文献に対する理解力不足がおもわぬ勘違い解消に発展した変なお話。

GoogleAppを使ってリマインダーを作ってみた。

作った動機は以下の記事を読んで。
http://readingmonkey.blog45.fc2.com/blog-entry-556.html

効率良く記憶/勉強するためには特定の法則間隔でリマインドするのが効率良いらしい。
でもそのリマインドの間隔ではOmnifocusではやれない。

うまいことやれないかなーと考えてみたらGoogleカレンダー上に通知イベントを生成するスクリプトを書けばいいじゃんと思い至る。

んでちょいと調べた結果、可能だということが分かったので早速作成。
作業2時間。久々にJavascript触った割りにちゃんとできました。

要件整理

  • ボタンを押すと前述の前述のURLの間隔単位でGoogleCalendarにイベントを生成してくれる。
  • イベント名は任意で。
  • リマインドしてくれる回数もしていさせて欲しい。
  • 可能であれば今何回目なのかも知らせて欲しい。
  • リマインダーなんだから通知して欲しい。
  • 詳細もメモできたらいいな。

とする。

作り方

  1. google docsに新しいスプレッドシートを作成する
  2. スクリプトエディタを開いて後述のソースをコピペする
  3. ボタンを設置してfunctionを関連づけさせとく
  4. google calendarに新しいカレンダーを作り、ソース上に記述されてるカレンダー名と同じ名前を付けておく
  5. 4のカレンダーの通知の項目にメールとかポップアップとかでリマインドさせるようにしとく
  6. 完成!

1:google docsに新しいスプレッドシートを作成する

割愛

2:スクリプトエディタを開いて後述のソースをコピペする

スプレッドシートを開き、メニューバーからツール->スクリプトエディタでエディタを開き、
後述のソースをコピペします。

3:ボタンを設置してfunctionを関連づけさせとく

makeRemindEventOnCal change inspector
メニューバーから↑の手順で図形描画を選択し、”図形描画を挿入”画面に移行し、
makeRemindEventOnCal create rect
矩形を配置します。
makeRemindEventOnCal draw rect
矩形が用意できたら右上の”保存して閉じる”ボタンを押してスプレッドシートに戻ります。
makeRemindEventOnCal Add Script
スプレッドシートに描画された図形にマウスオーバーすると”図形描画”の文字が。ここのプルダウンからスクリプトを関連づけさせます。
makeRemindEventOnCal set script
記述する関数名はソースの2つ目の関数名を記述します。
“makeRemindEventOnCal change inspector”

4:google calendarに新しいカレンダーを作り、ソース上に記述されてるカレンダー名と同じ名前を付けておく

自分のアカウントのグーグルカレンダーを開いて、新しいカレンダーを追加します。カレンダーに名前を付けれるのでソースで指定してある”記憶強化リマインダ”に。

名前を変えるのであればソース側のカレンダー名も偏向すること。

5:4のカレンダーの通知の項目にメールとかポップアップとかでリマインドさせるようにしとく

makeRemindEventOnCal alert setting
カレンダーの通知の項目を開いて、メールやポップアップに適当な時間を記述して通知してくれるようにしときます。これを設定しないとただのカレンダー止まりなのでちゃんと能動的に動いてくれる様にします。

6:完成!

ここまでやれば完成。動作は以下の通りです。
ReminderOnCalendar write eventName
イベント名を記述。
カレンダー上に表記される文字なので短く分かりやすいものに。
ReminderOnCalendar write loop count
最初の1ヶ月は間隔が長くなる不定期なので、固定でリマインドするようにしてあります。
その後の安定的(31日毎)にリマインドする回数を指定します。
ReminderOnCalendar write description
カレンダーに記載する詳細を入力する欄。思い出すべき内容を出来るだけ細かく書くか、何をやるかだけ書くか、見出し以外の事を書いときます。
ReminderOnCalendar run script end
スクリプトが完了するとこれが表示されます。

ではカレンダーを見てみます…。

makeRemindEventOnCal result1
おお!ちゃんと書かれてる!
makeRemindEventOnCal result2
詳細もご覧の通り記述されます。

ソース

ソースは以下の通り。

/*
カレンダーにリマインダーの理論間隔でイベントを設置するスクリプト
ver. 0.1 2012/1/3: OHGAKI Kunihiro
ver. 0.2 2012/1/4: OHGAKI Kunihiro  : コメントを書き直し。
*/

/*
makeCalEvent
カレンダーにイベントを書き込む関数
cal as Calendar Object
カレンダーオブジェクト。何度も取得するのもアレなので一度取得したら保持させるようにしたので引数で渡してます。
グローバル変数にしとけよって話だ…。
GoogleAppってグローバル変数ってつかえるのかね

eventName as String.
カレンダーに記述するイベント名を文字列で取得。

targetDate as Date
実際に記述する日時を日付で指定。

count as Integer
リマインドした回数を詳細とかイベント名に記述する。

descript as String
カレンダーのイベントの詳細部分に記述する文字列。

*/

function makeCalEvent(cal,eventName, targetDate,count,descript){
  var cname = eventName;
  if (cname != "" & cname != "cancel"){
    targetDate.setHours(0);
    targetDate.setMinutes(0);
    targetDate.setSeconds(0);
//    Browser.msgBox(descript);
    cal.createEvent(
      eventName+"@"+count+"回目",
      targetDate,
      targetDate,
      {
        description:count + "回目のリマインド" + descript,
        sendInvites:false
      }
    );
  }
}

/*
メインの関数な。スプレッドシートから呼び出すのはこれ。
makeRemindEventOnCal カレンダーにリマインダーイベントをセットするスクリプト。
リマインドの間隔は以下のURLの記事を元に作成してます。
http://readingmonkey.blog45.fc2.com/blog-entry-556.html
*/
function makeRemindEventOnCal(){
    var cal = CalendarApp.openByName("記憶強化リマインダ"); //←リマインダー専用のカレンダーを予め作成。名前は任意で。

  //カレンダーに表示するイベント名を入力する。
  var eventName = Browser.inputBox("イベント名を入力してください");
  if(!eventName)return;
  //リマインドする回数を入力。
  var count = Browser.inputBox("安定期後にリマインドしたい回数を数値で入力", "整数でよろしく", null);
  var description = Browser.inputBox("詳細を記述してください。未記入でもOK");
  var remindArray = [1,2,4,7,15,21];
  //軌道に乗るまでの1ヶ月はこの間隔で広げて行く。
  var remindMonth = 31;
  //軌道にのったら一月以上あけてはならないらしい。
  var beforStationaryPhaseCount = remindArray.length;
  for(var i = 0; i < beforStationaryPhaseCount; i++){
    var day = new Date;
    var target = remindArray.shift();
    day.setTime(day.getTime() + target*24*3600*1000);//指定の日にリマインドタイミングをセット。
    makeCalEvent(cal,eventName,day,i+1,description);
  }
  var afterDay = new Date;
  if(count != 0){
    for(var i = 0; i < count; i++ ){
      afterDay.setTime(afterDay.getTime() + remindMonth*24*3600*1000);//31日後にセット。
      makeCalEvent(cal,eventName,afterDay,i + beforStationaryPhaseCount +1,description);
    }
  }
      //完了通知
  Browser.msgBox("イベントのセットが完了しました。");
}

ハマったところ

カレンダーのイベントを生成する.createEvent()の引数に詳細を文字列で入力するんだけども、この引数がAdvanced Argumentsであり、省略可能引数オブジェクトのプロパティで、最初に発見したサンプルはバージョン違いなのか記述方法が違っていた。エラーが出ていたので本家サイトを探すに至り、難を逃れた。今後は最初から本家を見るように心がける。日本語サイトに逃げがちなのを戒めますわよ。

あと、タイポで反映されてなかったのに気づかずに5分くらい悩んだ。相変わらずエラー出ないのでこういうバグ発見はマジ困る。ブラウザ上のエディタではなくてローカルのちゃんとしたエディタで作業すべきなんだろうな。※英語記述のタイポを戒めてくれるエディタを教えて下さい!英語力低い僕には(ry

参考にしたサイト

【UINavigationController】ボタンを押すことで画面遷移する構造の基本構成

画面遷移する際の概念がやっと理解できたので、構成を残しておく。
サンプルは以下の通り。

処理順のイメージ

  1. AppDelegateがWindowを生成
  2. AppDelegateがTopViewControllerを生成してWindowに表示
  3. TopViewControllerがViewに載ったらイベント付きのボタンを2つ生成
  4. ボタンが押されたらViewController1を生成してWindowのViewに掲示。
  5. ViewController1がViewに載ったらイベント付きのボタンを1つ生成
  6. ボタンが押されたらTopViewControllerを生成してViewに乗せる。

気をつけなければ成らないのは以下の点

  • このサンプルではUINavigationControllerを利用しているので、遷移経路が全て保持されており、経路上の画面自体も保持され続ける。
  • よって、メモリオーバー必至。
  • メモリオーバー時用の対処はUIKit詳細リファレンスに載ってるのでそれを参照。

サンプル

AppDelegate.h

#import 

@interface AppDelegate:NSObject {
  UIWindow* window_;
  UIViewController* rootController_;
}

@property (strong, nonatomic) UIWindow *window;

@end

AppDelegate.m

#import "AppDelegate.h"
#import "TopViewController.h"

@implementation AppDelegate
@synthesize window = _window;

- (void)applicationDidFinishLaunching:(UIApplication *)application{
  // window を自分で作成
  CGRect bounds = [[UIScreenmainScreen]bounds];
  window_ = [[UIWindowalloc] initWithFrame:bounds];
  TopViewController* top = [[[TopViewController alloc]init]autorelease];
  rootController_ = [[UINavigationController alloc] initWithRootViewController:top];
  [window_addSubview:rootController_.view];
  [window_makeKeyAndVisible];
}

- (void)dealloc{
  [rootController_release];
  [window_release];
  [superdealloc];
}

@end

TopViewController.h

#import 
@interface TopViewController : UIViewController
@end

TopViewController.m

#import "TopViewController.h"

@implementation TopViewController

- (UIButton*)makeButton:(CGRect)rect text:(NSString*)text tag:(NSInteger)tag{
  UIButton* button = [UIButtonbuttonWithType:UIButtonTypeRoundedRect];
  [button setFrame:rect];
  [button setTag:tag];
  [button setTitle:text forState:UIControlStateNormal];
  [button addTarget:selfaction:@selector(clickButton:) forControlEvents:UIControlEventTouchUpInside];
  return button;
}

- (void)viewDidLoad{
  NSLog(@"top view did load");
  [superinit];
  UIButton* milkButton = [self makeButton:CGRectMake(10, 20, 300, 100) text:@"milk"tag:0];
  [self.viewaddSubview:milkButton];
  UIButton* soltButton = [self makeButton:CGRectMake(10, 120, 300, 100) text:@"solt"tag:1];
  [self.viewaddSubview:soltButton];
  NSLog(@"top view did load end");
}

- (IBAction)clickButton:(UIButton*)sender{
  NSLog(@"click Milk: tag is : %d",sender.tag);
  /*
   画面遷移の処理を行う。
   1:viewControllerを生成する。
   2:その画面に遷移。
   */
  //UIViewControllerを生成
  Class class = NSClassFromString(@"ViewController1");
  id viewController = [[[class alloc] init ]autorelease];
  [self.navigationController pushViewController:viewController animated:YES];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
@end

ViewController1.h

#import 
@interface ViewController1 : UIViewController
@end

ViewController1.m

#import "ViewController1.h"

@implementation ViewController1

- (void)viewDidLoad{
  [super viewDidLoad];

  //Hello,world!ラベルを追加
  //背景は白、文字は黒で。
  UILabel* label = [[[UILabel alloc] initWithFrame:self.view.bounds] autorelease];
  label.text = @"Hello, world";
  label.textAlignment = UITextAlignmentCenter;
  label.backgroundColor = [UIColor whiteColor];
  label.textColor = [UIColor blackColor];
  label.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  [self.view addSubview:label];

  //これをタップしたら画面遷移する
  UIButton* button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
  [button setTitle:@"画面遷移" forState:UIControlStateNormal];
  [button sizeToFit];
  CGPoint newPoint = self.view.center;
  newPoint.y += 50;
  button.center = newPoint;
  button.autoresizingMask =
    UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
  [button addTarget:self
             action:@selector(buttonDidPush:)
   forControlEvents:UIControlEventTouchUpInside];
  [self.viewaddSubview:button];
}

-(id)init{
  if((self = [superinit])){
    self.title=@"Hello";
  }

  returnself;
}


- (void)buttonDidPush:(UIButton*)sender{
  NSLog(@"Pushed!!");
  id TopViewController = [[[TopViewController alloc] init ]autorelease];
  [self.navigationController pushViewController:TopViewController animated:YES];
}

@end

おまけ

ちなみにViewController1のbuttonDidPush:senderメソッドを以下の様に書き換えると、トップに戻る様になるので遷移経路を保持せずにメモリを節約できる。

- (void)buttonDidPush:(UIButton*)sender{
  NSLog(@"Pushed!! tag:%d",sender.tag);
  [self.navigationController popToRootViewControllerAnimated:YES];
//  id viewController = [[[TopViewController alloc] init ]autorelease];
//  [self.navigationController pushViewController:viewController animated:YES];
}

iOSプログラミング逆引きリファレンス108 ~知りたいことがすぐわかるiPhoneプログラミングテクニック~

iPhoneプログラミングUIKit詳解リファレンス

iOSデバッグ&最適化技法 for iPad/iPhone
詳解 Objective-C 2.0 第3版

Objective-cにおけるプロパティとはなんだ?

プロパティとインスタンス変数の違いが良くわからなかったので調べてみた。

インスタンス変数

インスタンス変数はそのクラスから生成されたインスタンスオブジェクトが生きている限りは保持されるインスタンス固有の変数。

プロパティはそのインスタンス変数にアクセス指定子を付けて宣言することで、インスタンス自身ではないオブジェクトからもアクセスできるようになる。

インスタンス変数にはアクセス指定子を付けてアクセスを制御できる。

  • @private
  • @protected
  • @public //デフォルト
  • 例:@private uint age;

->でインスタンス変数にアクセスできる。
ただしアクセス指定子をつけても警告のみでアクセスできてしまうし、ビルドも通る…。ェ…。

Objective-Cにおけるプロパティはどんな機能を持っているのか?

* アクセサメソッドが自動的に作られる

Objective-Cのプロパティを実装するにはどうしたらいいか?

.hファイル側で

  • @property(retain) NSString* propertyName;
#import

@interface PropertyCheck : NSObject

@property (retain) NSString* name;

@end

.mファイル側で

  • 通常はこう記述する。
    • @synthesize propertyName;
  • インスタンス変数名とプロパティ名が違う場合は以下の様にする。(※インスタンス変数が”_propertyName”の場合)
    • @synthesize propertyName = _propertyName;
#import "PropertyCheck.h"

@implementation PropertyCheck{
//  NSString* name;
}

- (id)init{
  self = [super init];
  self.name = @"default";
  return self;
}

@synthesize name;

@end

ただし!

  • @dynamic propertyName で宣言するとアクセサメソッドは生成されない。
  • @dynamic propertyName で宣言するとコンパイル時にチェックされない。エラー、ワーニング等がでない。

生成されたアクセサメソッドの構文はドット表記で行うが良い。

  PropertyCheck* propChck = [[[PropertyCheck alloc]init]autorelease];
  NSLog(@"prop.name = %@",propChck.name);
  propChck.name = @"iPhone3GS";
  NSString* name = propChck.name;
  NSLog(@"name      = %@",name);
  NSLog(@"prop.name = %@",propChck.name);
  propChck.name = @"iPhone4S";
  name = propChck.name;
  NSLog(@"name      = %@",name);

これでログ側には

 prop.name = default
 name      = iPhone3GS
 prop.name = iPhone3GS
 name      = iPhone4S

と表示されるはず。

ドット演算子の利点

通常の表記だとコンパイル時にチェックされないので実行するまでメソッド名が正しいかとか、存在するか等がわからない。
しかしドット演算子を使用すると使用可否がコンパイル時にチェックされる。

アクセサメソッドに任意の名前を付ける

アクセサメソッド名を自作するには以下の様に表記を変更する。
@property (getter=nowHealthPoint, setter=damagedHealthPoint:) NSInt enabled;
getterは引数無しだがsetterは引数ありなので必ずセミコロンを記述しておく。
同じ変数にアクセスするのでgetterの返り値の型とsetterの引数の型は一致する。この例ではNSIntにしてある。

プロパティの属性

  • readwrite
    • 読み書き可だよ。
  • readonly
    • 読み込みのみだよ。setterが生成されなくなる。
  • retain
    • 新規参照。参照数をインクリメント。
    • ガービッジコレクションを有効にしてるとretainは無効になる。
    • ガービッジコレクションを無効にしている場合、手動でreleaseする必要がある。
  • assign
    • まま渡される。
    • ガービッジコレクションが無効だと警告の上、これが適用される。
  • copy
    • set時にコピーが渡される。
    • ガービッジコレクションを無効にしている場合、手動でreleaseする必要がある。

プロパティとインスタンス変数

インスタンス変数として宣言するならプロパティとして定義した方が良いという結論に。
歴史ある言語って色々便利な分だけ分かりにくいね…。等など。

参考

iOSプログラミング逆引きリファレンス108 ~知りたいことがすぐわかるiPhoneプログラミングテクニック~

iPhoneプログラミングUIKit詳解リファレンス

iOSデバッグ&最適化技法 for iPad/iPhone
詳解 Objective-C 2.0 第3版

パブリッククラスとコンクリートクラスとクラスクラスタと

コンクリートクラスとかクラスクラスタとか聞き慣れない単語がでてきたので、漠然と理解するよりもちゃんと解説されてるものを読んで理解しておこうと思い調べたのでメモ。

コンクリートクラスって?

コンクリートクラスとは、”有名どころのクラス”のサブクラスで、”有名どころのクラス”を使用しておけば、格納するデータに応じて最適なクラスを自動的に割り当てるObjective-cの機能で呼び出されたクラス。

クラスクラスタって?

クラスクラスタとは、”有名どころのクラス”とそのサブクラス全体を含めたクラス群のこと。

具体的にどういうものかをソースを書いて理解

  NSString* str = [[NSString alloc]initWithString:@"/path/path/"];
  NSLog(@"str = %@",[str class]);

コンソールには…

2011-12-12 13:41:24.295 playground[13697:f803] str class = __NSCFConstantString

だとさ。

つまり、ユーザが宣言したクラスではなく、そのクラスを継承したサブクラスが自動で割り当てられてて、

ってあれ?パス用のクラスクラスタが割り当てられると思っていたのだけれども?

…。

そうそう、これをさっきのコードに追加してみると…

  NSString* path = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"];
  path = [path stringByAppendingPathComponent:[NSStringstringWithFormat:@"%d.txt",str]];
  NSLog(@"path class = %@",[path class]);

こんなレスポンス

2011-12-12 13:41:24.295 playground[13697:f803] str class = __NSCFConstantString
2011-12-12 13:41:24.296 playground[13697:f803] path class = NSPathStore2

となる。

両方ともNSStringで宣言してるのにClassが違う。
ここで実際に表示されたクラス群がコンクリートクラス。
親になってるスーパークラスはNSStringはパブリッククラスと呼ばれる。
なのでNSStringクラスで使用可能なクラスメソッド/インスタンスメソッドは継承してるコンクリートクラスでも使える。

stringByAppendingFormat:メソッドを実行するとNSPathStore2クラスに変換されたりするのかな?という思いつき。
※stringByAppendingFormat:メソッドがNSPathStore2クラスに付属するメソッドなのかどうかは調べてません。

最後にこれを追加してみる。

  str = [str stringByAppendingFormat:@"file.txt"];
  NSLog(@"str = %@",str);
  NSLog(@"str class = %@",[str class]);

コンソールには

2011-12-12 13:41:24.295 playground[13697:f803] str class = __NSCFConstantString
2011-12-12 13:41:24.296 playground[13697:f803] path class = NSPathStore2
2011-12-12 13:52:22.455 playground[13873:f803] str = /path/path/file.txt
2011-12-12 13:52:22.456 playground[13873:f803] str class = __NSCFString

最初にチェックしたstrのクラスは”__NSCFConstantString”だったのが”__NSCFString”になってる。
データをstringByAppendingFormat:メソッドによって生成したオブジェクトなので格納されてるデータが変わったのかな?と仮説を立ててみた。

色々試した結果、どうやら格納されるデータをどのように生成したかによって割り当てられるクラスが変動するみたい。

  NSString* str2 = [@"test" stringByAppendingPathComponent:@"/path/path/"];
  NSLog(@"str class = %@",[str2 class]);

このコードだと、最初に宣言されたstr2は NSStringのstringByAppendingPathComponent:メソッドによって生成されているので、クラスはPathStore2な!と言われて割り当てられた感じ。

2011-12-12 14:12:31.654 playground[14265:f803] str2 class = NSPathStore2

宣言はNSStringで行っているのでメモリサイズもそれに準じたフィールドが宛てられていると推測。なのにクラスが変わっててもいいの?とかとか疑問は湧く、が追い過ぎ注意。
間違っていたら改めるとして、生成時に副次的に生じる情報を元に判別している、とだけ解釈するまでにここでは留めておこう。

図にしてみる

話を戻してコンクリートクラスとパブリッククラスを図にするとこんな感じ。

クラスクラスタ

まとめ

プログラマが宣言するクラスがパブリッククラス。
実際に割り当てられるクラスがコンクリートクラス。
それらを全部合わせてクラスクラスタ。

また一つ、賢くなってしまった…。

参考