2012年4月アーカイブ

こんにちは。開発担当のICTFです。

同じコントロール内で複数のActionSheetを用いる場合、一般的には各ActionSheetにタグを割り振り、ボタンタッチのコールバック内でタグ番号とボタン番号から処理を選択する。といった記述をします。
例えば、ボタンタッチのコールバック処理は次の様なイメージになります。

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex

{

    switch(actonSheet.tag) {

        case tag1:

            switch(clickedButtonAtIndex) {

                case 0:

                    処理①

                    break;

                case 1:

                    処理②

                    break;

                case 3:

                    処理③

                    break;

                default:

                    break;

            }

            break;

        case tag2:

            switch(clickedButtonAtIndex) {

                case 0:

                    処理②

                    break;

                case 1:

                    処理③

                    break;

                case 3:

                    処理④

                    break;

                default:

                    break;

            }

            break;

        default:

            break;

    }

}


たった2種類のアクションシートを作っただけで、こんなにも面倒臭い記述になってしまいます。
それにswitch分やif分をだらだらと書かなければならないのは、なんだかオブジェクト指向になりきれて無い様でどうにも好きになれません。

また、ActionSheetが使い辛くなっている要因の1つとして、アクションの対象となるオブジェクトと紐付けができないという点があります。
例えばリストビューで選択されている1つの項目(オブジェクトA)に対するActionSheetを表示したとします。
アクションとして「削除」を選んだ際に、tag番号は「TAG_DEFAULT」、ボタン番号は「3」というように取得ができるのですが(この2つの値で削除という処理を選択することが可能です)、「オブジェクトAに対する削除」というところまでは判断ができません。
この問題を解決する為に、ActionSheetを表示するタイミングでコントロールのメンバとして宣言されている記憶用変数にオブジェクトAを記憶しておき、ボタンタッチコールバック処理内で記憶しているオブジェクトAを活用する。といった処理が必要になる訳です。
しかし、このような実装もオブジェクト指向としては格好が悪いですよね。できれば1つの機能(ActionSheetの表示からその対象の管理、そのアクションまで)は1つのクラスで実現したいところです。

前置きが長くなりましたが、今回は上で挙げたActionSheetの2つの弱点
 ・ボタンコールバック内の処理が煩雑
 ・アクションシートの操作対象となるオブジェクトとの紐付けができない
という点の解決を目指して改良を加えようと思います。

大まかなイメージとしては、ActionSheetの各ボタンをタイトル名のみで管理するのではなく、タイトル名を含むボタン用クラスで管理します。
ボタン用クラスは配列で管理し、拡張したUIActionSheetクラスのメンバとして記録します。
ボタンがタップされた際は、拡張UIActionSheetクラスのフレームワークとして、ボタン用クラスのactionメソッドをコールします。
actionメソッドはボタン用クラスから派生した各処理クラスで実装を行ないます。

ではまずボタン用クラス(ボタンの基底クラス)を作成しましょう。
.hでは次のように記述します。

// 各種通知から対象オブジェクトを取り出す為の共通キー

#define ACTION_SHEET_BUTTON_KEY_OBJECT @"ActionSheetButtonKey_Object"


// アクションシートのボタンベースクラス

@interface ActionSheetButtonBase : NSObject

{

NSString* _title;

id _object;

}


@property (nonatomic, retain, readwrite) NSString* Title;

@property (nonatomic, retain, readwrite) id Object;


-(id) initWithObject:(id)object;


// アクション

-(void) action;


@end


_titleはボタンの名称となるタイトルを表します。
_objectはこのボタンがタップされた際に操作対象となるオブジェクトを代入します。必要がない場合はnilでも構いません。

.mの実装は次のようになります。

@implementation ActionSheetButtonBase


@synthesize Title = _title;

@synthesize Object = _object;


-(id) initWithObject:(id)object

{

if (self = [super init]) {

self.Title = @"";

self.Object = object;

}

return self;

}


// 継承クラスでオーバーライドする

-(void) action

{

}


@end


こちらも単純で、メンバの初期化とactionメソッドの空実装のみです。
actionメソッドの実装は派生先で行ないますので、まだ必要ありません。

次にActionSheetButtonBaseクラスを継承した、各ボタンクラスを実装します。
例として対象となるオブジェクトAのプロパティ「Title」を変更するアクションボタンクラスを載せますので、応用して実装してみてください。
.hは以下のようになります。

// 計算オブジェクトのタイトル編集ボタン

@interface CalcObjectTitleEditButton : ActionSheetButtonBase


@end


必要なメンバやメソッドはベースクラスで宣言済みですので、新しい要素は必要ありません。
.mは以下のようになります。

@implementation CalcObjectTitleEditButton


-(id) initWithObject:(id)object

{

if (self = [super initWithObject:object]) {

self.Title = @"タイトルの編集";

}

return self;

}


-(void) action

{

// タイトル編集ビューを表示する

UIAlertView* view = [[UIAlertView alloc] initWithTitle:@"タイトルの編集" message:@"新しいタイトルを入力してください。" delegate:_object cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil];

view.alertViewStyle = UIAlertViewStylePlainTextInput;

[view show];

}


@end


initメソッド内でこのボタンのタイトルを設定しています。
次にactionメソッド内の処理を実装します。ここでは新しくタイトルを入力してもらう為のAlertViewを表示しています。
AlertViewのdelegateは対象オブジェクトである_objectに設定し、対象オブジェクトごとに受け取った値を処理する作りになっています。
delegateの設定先をselfにして、CalcObjectTitleEditButtonクラス内で_object.Titleを書き換える処理を実装しても良いかもしれません。

こんな感じでまずは必要となるActionSheetボタンの数だけクラスを作成してください。

必要なボタンクラスの作成が終わりましたら、次にUIActionSheetの拡張クラスを実装します。
.hは以下のようになります。

#import "ActionSheetButtonBase.h"


// アクションシートの拡張クラス

@interface UIActionSheetEx : UIActionSheet

<UIActionSheetDelegate>

{

NSArray* _buttonList;

}


@property (nonatomic, readonly) NSArray* ButtonList;


// アクションシートのボタンを追加する

-(void) addButtonList:(NSArray*)buttonList;


@end



ボタンタッチイベントをフレームワークとして実装する為、UIActionSheetDelegateプロトコルの宣言を追加します。
メンバ変数としてActionSheetで使われるボタンクラスの一覧を宣言します。ActionSheet内部でボタンクラスを管理する事により、他のアクションシートとの区分けを気にする必要が無くなります。
最後に各ボタンクラスのTitleプロパティを抽出してボタンを作成するのが面倒ですので、ボタンクラスの配列を渡すだけで一連のボタン作成と記憶を行なってくれるメソッドを宣言しています。

.mは以下のようになります。

@implementation UIActionSheetEx


@synthesize ButtonList = _buttonList;


-(id) init

{

if (self = [super init]) {

self.delegate = self;

}

return self;

}


-(void) dealloc

{

[_buttonList release];

[super dealloc];

}


// アクションシートのボタンを追加する

-(void) addButtonList:(NSArray*)buttonList

{

_buttonList = [buttonList retain];

for (ActionSheetButtonBase* button in buttonList) {

[self addButtonWithTitle:button.Title];

}

}


// アクションシートのコールバック

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex

{

ActionSheetButtonBase* button = [_buttonList objectAtIndex:buttonIndex];

// ボタンに設定されたアクションを実行する

[button action];

}


@end


initで自身のdelegateを自身に設定します。
ActionSheetの各コールバックをフレームワーク化する為に必要な設定ですので、忘れずに行なってください。
-(void) addButtonList:(NSArray*)buttonListでは、配列の各要素をループで回し、Titleプロパティからボタンを作成しています。
また_buttonListに受け取った配列を格納します。これはActionSheetコールバックで該当するactionメソッドを呼び出す為に用います。
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndexコールバックでは、_buttonListから該当のオブジェクトを得、そのactionメソッドをコールするのみです。
これで選択したボタンのactionメソッドがコールされるというフレームワークが完成しました。

最後に実際の使用例のサンプルを挙げておきます。

// アクションシートを生成する

UIActionSheetEx* sheet = [[[UIActionSheetEx alloc] init] autorelease];

sheet.title = @"";

sheet.actionSheetStyle = UIActionSheetStyleBlackTranslucent;

[sheet addButtonList:[NSArray arrayWithObjects:

    [[[CalcObjectTitleEditButton alloc] initWithObject:obj] autorelease], 

    [[[CalcObjectSetDefaultValueButton alloc] initWithObject:obj] autorelease],

    [[[CalcObjectDeleteButton alloc] initWithObject:obj] autorelease],

    [[[ActionSheetCancelButton alloc] initWithObject:obj] autorelease],

    nil]];

sheet.destructiveButtonIndex = 2;

sheet.cancelButtonIndex = 3;

[sheet showInView:self.view];


普通のUIActionSheetの利用法とほぼ変わりません。
違いはボタン作成で文字列の配列を渡すのではなく、ActionSheetButtonBaseクラスを継承したクラスオブジェクトの配列を渡すくらいです。

以上の実装でActionSheet利用元のコントロールでActionSheetの管理全般から解放されます。
メッセージを表示するだけのAlertView並みにお手軽になったのではないでしょうか。

もし「ここを改良できる」や「ここに問題がある」などありましたら、是非是非教えて下さいませ。
こんにちは。開発担当のICTFです。

RDBのつもりでCoreDataを使い始めたのですが、全然別物でした。
まだ勉強を初めて間もないですが、今のところ「本当に便利なのかな?」というのが正直な感想です。
しかし、とりあえずはCoreDataに関する知識を深めたいという事もあり、現在開発中のアプリにCoreDataを用いる事にしました。

アプリ内では複数のentity(RDBで言うところのtableに相当します)を用いるのですが、entity間の連結をどう表現するか。という部分の解決に随分苦労しました。
RDBであればautoNumberタイプの項目を主キーとして、別テーブルの外部キーに設定したいところなのですが、CoreDataには主キーという概念もautoNumber型の変数も存在しません。
かといってuserDefaultに主キーの最大値を保存しておき、レコード追加時にそこから値を読み取って使用する・・・というのも格好が悪い気がします。

理想はレコード追加時に意識せずとも正しく一意な値が「自動で」記録される事です。
今回はそれを目指して実装してみました。
CoreDataの仕組みのみで動作し、外部の機能(userDefaultに値を保存するなど)にも頼らないので汎用性は高いかと思います。

前提として、
・使用するentityは「Template」とする
・Template entity内のAttibuteは以下の通り
 ⇒templateID : Integer 64
 ⇒createDate : Date
 ⇒title : String
となります。
templateIDが今回の首題となる一意キー項目です。

まず上記entityのNSManageObjectサブクラスを作ります。
サブクラス名はentity名と同じ「Template」です。
一意値の取得と設定はこのTemplateクラス内に実装します。

Template.mにawakeFromInsertメソッドを追加します。
このメソッドはレコードが新規に追加される際に呼び出されるメソッドです。
awakeFromInsert内に一意値の取得し、それを初期値として設定する為の処理を記述します。

-(void) awakeFromInsert

{

[super awakeFromInsert];

self.createDate = [NSDate date];

self.title = @"";

// 一意キーを設定する

NSManagedObjectContext* managedObjectContext = [CommonFunctions getAppDelegate].managedObjectContext;

NSFetchRequest* request = [[[NSFetchRequest alloc] init] autorelease];

NSEntityDescription* entity = [NSEntityDescription entityForName:@"Template" inManagedObjectContext:managedObjectContext];

[request setEntity:entity];

[request setResultType:NSDictionaryResultType];

NSExpression* keyPathExpression = [NSExpression expressionForKeyPath:@"templateID"];

NSExpression* maxIDExpression = [NSExpression expressionForFunction:@"max:" arguments:[NSArray arrayWithObject:keyPathExpression]];

NSExpressionDescription* expressionDescription = [[[NSExpressionDescription alloc] init] autorelease];

expressionDescription.name = @"maxID";

expressionDescription.expression = maxIDExpression;

expressionDescription.expressionResultType = NSDecimalAttributeType;

[request setPropertiesToFetch:[NSArray arrayWithObject:expressionDescription]];

NSError* error = nil;

NSArray* objects = [managedObjectContext executeFetchRequest:request error:&error];

if (objects == nil) {

// エラー

NSLog(@"TemplateID get Error");

self.templateID = [NSNumber numberWithInteger:0];

}

else {

NSInteger initID = 0;

if (objects.count > 0) {

NSDecimalNumber* maxID = [[objects objectAtIndex:0] valueForKey:@"maxID"];

NSLog(@"maxID: %@", maxID);

initID = [maxID integerValue] + 1;

}

NSLog(@"new Template ID: %d", initID);

self.templateID = [NSNumber numberWithInteger:initID];

}

}


Template entity内のレコードからtemplateIDの最大値をフェッチし、その値に1を加算したものを一意キーとして設定しています。

以下のケースで正しく動作しない事が懸念されますので、その辺は適宜実装して下さい。
・templateIDの最大値取得失敗時に一意キーとして0を設定している。
 ⇒通常の動作であれば最低値は1となりますので、キー値が0である場合はエラー結果であると断定できます。できますが、実際に処理を行なう箇所にその判定を入れ忘れていたら意味がありません。
・NSInteger型の最大値に達した場合の処理が記述されていない
 ⇒早々無いとは思いますが、必要であれば値のループ処理などを入れた方が安全でしょう。
・コンテキストをsaveする前に次のレコードの追加処理が実行される
 ⇒コンテキストがsaveされない限り、永続dataStoreにはコミットされません。それはつまり、templateIDの最大値取得結果が正しくなくなる事を意味します。
 例)
  templateID最大値取得 > 1
  一意キー値として1+1=2 を設定
  save
  templateID最大値取得 > 2
  一意キー値として2+1=3 を設定  ①
  [save しない]
  templateID最大値取得 > 2
  一意キー値として2+1=3 を設定  ②
  
  ①と②の一意キーが重複し、一意性が失われます。

特に最後の項目の心配をしないといけないケースでは、おとなしく普通のRDBを使った方が楽なのかもしれません。