UIImage から縮小画像を生成する

2010年6月30日水曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

UIImage からサムネイル用途で使用する縮小画像を作る。

縮小処理


こんな感じ。
@implementation UIImage (extension)

- (UIImage*)imageByShrinkingWithSize:(CGSize)size
{
 CGFloat widthRatio  = size.width  / self.size.width;
 CGFloat heightRatio = size.height / self.size.height;
 
 CGFloat ratio = (widthRatio < heightRatio) ? widthRatio : heightRatio;

 if (ratio >= 1.0) {
  return self;
 }

 CGRect rect = CGRectMake(0, 0,
     self.size.width  * ratio,
     self.size.height * ratio);
 
 UIGraphicsBeginImageContext(rect.size);

 CGContextRef context = UIGraphicsGetCurrentContext();
 [self drawInRect:rect];

 UIImage* shrinkedImage = UIGraphicsGetImageFromCurrentImageContext();
 
 UIGraphicsEndImageContext();
 
 return shrinkedImage;
}
UIImage のカテゴリとして実装した。縦横比は保持したまま、与えられたサイズ内に収まる最大の画像を返す。


参考情報

補間方法を指定してUIImageを縮小する(1) - tosh-tの日記
画像を縮小する時の補完方法について

UIView上のコントロールへの IBOutlet接続は retain で(assign改め)

2010年6月29日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: UIView上のコントロールへの IBOutlet接続は assignで

コメントで指摘を受け考え直した。
前回 assign と書いたが、これを改め retain にする。


見本


UIViewController でコントロールへの IBOutletを作成する時の見本
@property (nonatomic, retain) IBOutlet UITextField* textField;

あわせて viewDidUnload で開放する。

- (void)viewDidUnload {
    self.textField = nil;
}

この2つはセットで使う。


理由


iPhoneの場合メモリ不足などが発生した場合、UIViewControllerが使用している UIView が開放される場合がある。前回の assign の場合、明示的に開放しなくても親ビューの開放と共にその上のコントロールも開放されるのでメモリリークの観点からは問題ない。しかし、UIViewの開放は意図しないタイミングで起こるために、誤って親ビューの解放後にコントロールへアクセスすることが起きうる。この場合、親ビューの開放に伴ってコントロールも開放されているので例外が出てクラッシュしてしまう(開放済みのオブジェクトへのアクセス)。また場合によっては親ビューの突然の開放があった場合でも入力中の文字列を退避したいケースがあるかもしれない。これらの理由により、コントロールのメモリ管理のオーナーシップを自分で持ってコントロールした方が良いと思われる。


参考


Xcodeが生成するテンプレートのコメントにも IBOutlet接続された subviewsの開放の件は書いてありますな。
- (void)viewDidUnload {
 // Release any retained subviews of the main view.
 // e.g. self.myOutlet = nil;


ドキュメントにも。
Memory Management Programming Guide: Memory Management of Nib Objects

Xcodeのテンプレート出力のコードが Google Objective-C Style Guide っぽくなっていた

| Published in | 0 コメント

このエントリーをはてなブックマークに追加

それだけ。

Xcode 3.2.3


@interface CoreDataRelations2AppDelegate : NSObject  {
    
    UIWindow *window;
    UINavigationController *navigationController;

@private
    NSManagedObjectContext *managedObjectContext_;
    NSManagedObjectModel *managedObjectModel_;
    NSPersistentStoreCoordinator *persistentStoreCoordinator_;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UINavigationController *navigationController;

@property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain, readonly) NSManagedObjectModel *managedObjectModel;
@property (nonatomic, retain, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;

- (NSString *)applicationDocumentsDirectory;

@end



Xcode 3.2.2


@interface TestAppDelegate : NSObject  {
    
    NSManagedObjectModel *managedObjectModel;
    NSManagedObjectContext *managedObjectContext;
    NSPersistentStoreCoordinator *persistentStoreCoordinator;

    UIWindow *window;
    UINavigationController *navigationController;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UINavigationController *navigationController;

- (NSString *)applicationDocumentsDirectory;

@end


Google Objective-C Style Guide


Google Objective-Cスタイルガイド 日本語訳 | textdrop
Google Objective-C Style Guide

自分も Google Objective-C Style Guide は、一部採用(メンバ変数の後ろに _ を付ける所など)。

UIView上のコントロールへの IBOutlet接続は assignで

| Published in | 2 コメント

このエントリーをはてなブックマークに追加

[関連]Cocoaの日々: UIKit コントロールの View構成と retainCount

(追記)前提条件が抜けていました。UIViewController での話です。

(追記 6/29)assign はやめて retain にしました。下記を参照のこと。
Cocoaの日々: UIView上のコントロールへの IBOutlet接続は retain で(assign改め)


※以下、内容が古くなってます。


プロパティ見本


@property (nonatomic, assign) IBOutlet UILabel* label;
多分これでいいと思う。dealloc時の開放は不要。親の UIViewが Ownershipを持つので、親が relelaseされるタイミングで relelaseされる為。


retainしたら


もし retain した場合、dealloc の他、親 UIView が破棄されるタイミングで releaseが必要になる。後者は didReceiveMemoryWarning が該当する。※通常 retain は不要。


参考


Memory Management Programming Guide: Memory Management of Nib Objects
ここで言う Top-Level Object とは Interface Builder のウィンドウに表示されるレベルのオブジェクトを指す。これは通常 Super View を持たない。
UIView 上に配置されたコントロールはこれに該当しない。

- - - -
結局検証せず。多分これでいいと思うが..

Core Data - Unidirectional Relationships(単方向関連)について

| Published in | 0 コメント

このエントリーをはてなブックマークに追加

Core Data では Relationships は Bidirectional(双方向)が推奨される。

ビルド時の Warning


例えば次のようなエンティティを定義してビルドすると..
Warningが出る。

Bidirectional(双方向)にするとこの Warningは消える。


Bidirectional が推奨される理由


ADC のリファレンスに書いてある。
Core Data Programming Guide: Relationships and Fetched Properties - Unidirectional Relationships

ここではそれぞれの方式によってオブジェクトグラフへの影響が説明されている。
  • Bidirectional Relationships(双方向)にすると、オブジェクトグラフの整合性を保つことができる(オブジェクトへの変更をトラッキングする、アンドゥ管理)。
  • Unidirectional Relationships(単方向)にすると、不整合が発生する場合がある。
Unidirectional で不整合が発生する例として次のケースが説明されている。※わかりやすいように図は勝手に作成)

Relationalship のルールとして、「非オプション(non-optional)」「削除禁止 (deny delete rule)」が設定されているものとする。

この時、下記のコードを実行したとする。
[employee setDepartment:department];
[managedObjectContext deleteObject:department];
BOOL saved = [managedObjectContext save:&error];
すると成功する。通常だとルール違反により save: が失敗するはず。

理由は次の通り。
  • Bidirectional Relationalship の場合、departmentを削除すると employee に変更マークがつけられる。その結果、save: 時にルール検証が行われ、エラーとなる。
  • Unidirectional Relationships の場合 departmentを削除しても emploee に変更マークがつけられない。これは department側に employeeへの参照が無いため。その結果、 save: 時にルール検証が行われずエラーとならない。

当然不整合状態なのでこの後下記のコードを実行するとエラーとなる(※1)
id x = [employee department];

※1 Guideにはとある。具体的にどのようなエラーになるのかは未検証。
x will be a fault to "nowhere" rather than nil.

対策としては Bidirectional にするか、-[NSManagedObjectContext deleteObject:] の代わりに setValue:forKey: を使う。
[employee setValue:nil forKey:@"department"]



SQL


Bidirectional にした時に気になるのが発行されている SQL(※SQLite使用時)。関連先エンティティの情報が不要な時にも(JOINなどで)情報を持ってきていないか?

⇒ これは持ってきていない。
(参考)(旧) Cocoaの日々: CoreData - リレーションシップ(1) シンプルなモデル

基本的に関連先エンティティの情報は必要になったタイミングで SQLが発行される。


結論


Core Data のモデリングでは Bidirectional Relationships(双方向関連)を使う。

Core Data - モデリング考察(階層構造の保持)#2

| Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: Core Data - モデリング考察(階層構造の保持)

いくつか思いつくままに..

階層保存用のエンティティを用意する

良い点
  • トラバーサルが楽(上位階層から下位へ簡単に降りていける)

悪い点
  • Category/SubCategory の親子関係の整合性制約をかけられない。これは別途 Validationなどを用意する必要あり。なお前回のモデリングも同じ問題あり。
  • 階層の数×2倍のエンティティになる。数が増えると扱いが煩雑になる。

課題
このモデリングでは EventCategory と Category の関連が Unidirectional Relationship(単方向関連) になっている。Core Data的には Bidirectional Relationship が望ましいが、マスタのエンティティにデータへの参照が入るのがなんとなく気になったのでこのようにした。


再帰構成にする


先のものを改良して再帰構成にしてみる( @yuma2471 さんアイディアどうも)。

汎用的になった。なお元々マスタ階層を扱っているので、階層毎に持っている情報が異なるケースが想定される(例えば、カテゴリはアイコン画像を持っているが、サブカテゴリは持っていないなど)。それらに対応するには少し一工夫が必要となる。

Core Data - モデリング考察(階層構造の保持)

2010年6月28日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

Core Data での階層構造保持するケースのモデリングについて考えてみた。

(追記)Event と Category, SubCategory,... 間の関連が「単一」⇔「複数」となっているが、これは誤り。正しくは「複数」⇔「複数」。関連名も events(複数形)が望ましい。


2階層のケース


ここでは例として1つのデータと2つのマスタからなるモデルを考えてみる。
Event がデータで、Category, SubCategory がマスタ。Event 1件について、複数のカテゴリとそれに紐づく複数のサブカテゴリを設定できるものとする。この時のモデリングはどうすべきか。

最初に考えついたのはこんなもの。
ただこれは Eventに紐づく Category は管理できるものの、SubCategoryが Eventに紐づかない。

となるとこうか。
Event に2つの関連 categories と subCategories を持たせる。これら自身は階層情報を持たないので、利用する時には Category, SubCategory の関連をたどっていくことになる。

例えば、ある Event に設定されたカテゴリとサブカテゴリの一覧を表示する場合のアプローチはこう。

(1) Event の subCategories から1件 SubCategory を取得
(2) SubCategory.category から Category を取得 => 表示
(3) SubCategory を表示
(4) (1)〜(3)を繰り返す

必要なら Categoryを使ってグルーピングする。

2階層の場合はなんとなくこれでも良さそうだ。


n階層


SubSubCategory,... 等、階層が深くなる場合はそれに合わせて Event.subSubCategories,... と Eventに属性を増やして行く。

.....なにか引っかかる気もするが目的は達成できそうだ。


サブ階層がオプション(非必須)の場合


この場合、下の階層から情報を追って行く手法が使えない。これは2階層のケースでもそう。先の例の場合、SubCategoryがオプションの場合が該当する。

このケースを考慮して表示アプローチを考え直すとこんな感じ。

(1) Event の categories から Categoryを1件取得する => 表示
(2) Event の subCategories で、(1)のCategoryに該当する SubCategoriesを取得する => 表示
(3) (1)〜(2)を繰り返す

n階層の場合は (1)〜(2)と同等のことを下階層まで行って行く。

このケースで気になるのは(2)が簡単にできるかどうか。Event.subCategories は NSManagedObjectのサブクラスのプロパティとして定義され、型は NSSet になる。このままだと自前で NSSetの中をチェックすることになる。方法はともかく取得した Categoryのプロパティで SubCategoryが取り出せると使い勝手がいい(取得要求を使う?)。なお Category.subCategories は Eventに無関係の SubCategory まで含まれてしまうので使えない。


- - - -
もっと良いモデリング方法を知っていたら是非教えて下さい(コメントでどうぞ)。

UIKit コントロールの View構成と retainCount

2010年6月27日日曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

Nib利用時のメモリ管理について調査中。

こんなビューを作り、View構成とそれぞれのViewの retainCountを出してみた。

"display RetainCount" を押した時の処理(UIViewController内)
- (void)displayRetainCountOfView:(UIView*)view level:(int)level
{
 NSMutableString* indent = [NSMutableString string];
 for (int i=0; i < level; i++) {
  [indent appendString:@"  "];
 }

 for (UIView* subview in [view subviews]) {
  NSLog(@"%@[%@ retainCount]: %d", indent,
     [subview class], [subview retainCount]);
  if ([[subview subviews] count] > 0) {
   [self displayRetainCountOfView:subview level:level+1];
  }
 }
}

-(IBAction)display:(id)sender
{
 NSLog(@"[self.view  retainCount]: %d", [self.view retainCount]);
 [self displayRetainCountOfView:self.view level:0];
 
}

再帰的に回してサブビューも含めて表示している。

結果はこう。
[self.view  retainCount]: 4
[UILabel retainCount]: 2
[UIRoundedRectButton retainCount]: 7
  [UIButtonLabel retainCount]: 3
[UISwitch retainCount]: 2
  [_UISwitchSlider retainCount]: 3
    [UIImageView retainCount]: 3
    [UIImageView retainCount]: 3
    [UIView retainCount]: 3
      [UILabel retainCount]: 3
      [UILabel retainCount]: 3
    [UIImageView retainCount]: 3
[UISlider retainCount]: 2
  [UIImageView retainCount]: 3
  [UIImageView retainCount]: 3
  [UIImageView retainCount]: 3
[UIProgressView retainCount]: 2
[UITableView retainCount]: 2
  [UIImageView retainCount]: 3
  [_UITableViewSeparatorView retainCount]: 3
  [_UITableViewSeparatorView retainCount]: 3
  [_UITableViewSeparatorView retainCount]: 3
  [UIImageView retainCount]: 3
[UISegmentedControl retainCount]: 2
  [UISegment retainCount]: 3
    [UISegmentLabel retainCount]: 3
    [UIImageView retainCount]: 2
  [UISegment retainCount]: 3
    [UISegmentLabel retainCount]: 3
    [UIImageView retainCount]: 2
[UIRoundedRectButton retainCount]: 2
  [UIButtonLabel retainCount]: 3
[UITextField retainCount]: 2
  [UITextFieldRoundedRectBackgroundView retainCount]: 3
    [UIImageView retainCount]: 3
    [UIImageView retainCount]: 3
    [UIImageView retainCount]: 3
    [UIImageView retainCount]: 3
    [UIImageView retainCount]: 3
    [UIImageView retainCount]: 3
    [UIImageView retainCount]: 3
    [UIImageView retainCount]: 3
    [UIImageView retainCount]: 3
    [UIImageView retainCount]: 3
    [UIImageView retainCount]: 3
    [UIImageView retainCount]: 3
[UIImageView retainCount]: 2

メインのビューに配置したコントロールの retainCount は 2となっている(一つはsuperviewによるもので、もう一つは不明)。
"diplay retainCount" ボタンのみ retainCountが 7となっているのがわかる。action-targetで接続するとその管理下におかれ、増えているのだと思われる。
[UIRoundedRectButton retainCount]: 7

- - - -

基本的に UIViewController.view 上に配置されたコントロールは、その UIViewController が破棄された時に UIViewController.view が release され、芋づる式にすべて開放(release)される。この為、これらのコントロールを UIViewController 内で使う場合、メモリのオーナーシップは考えなくて良いはず。
(例) @propety (nonatomic, assign) IBOutlet UILabel* label;

逆に reatain してしまった場合、その UIViewController がメモリ解放の責任の一旦を担わなければならない。
(例) @propety (nonatomic, retain) IBOutlet UILabel* label;

開放のタイミングは dealloc 以外に、UIViewController の場合はその生存期間と viewのそれとが異なるのでそれを考慮する必要がある。具体的にはメモリ不足が発生すると didReceiveMemoryWarning が呼ばれ、そのタイミングで viewが開放される。retainしている場合はこの時に開放しないと UIViewControllerのOultet参照が残ったままになってしまう。
(ただ、その後 viewの再生成で再び Outletが再設定されるので、その時に古い参照が開放される。なので viewの破棄・再作成毎に発生するメモリリークは起こっても限定的?)

。。。この辺りを現在検証中。

「メモリ警告をシミュレート」で didReceiveMemoryWarning を擬似的に発生

| Published in | 0 コメント

このエントリーをはてなブックマークに追加

# 多分よく知られていることだと思うが、なにぶん iPhone開発ビギナーなので。。

UIViewController に下記のコードが書いてあった時
- (void)didReceiveMemoryWarning {
    NSLog(@"didReceiveMemoryWarning");
    [super didReceiveMemoryWarning];
}

iPhone シミュレータの「メモリ警告をシミュレート」を選ぶと、

didReceiveMemoryWarning が呼び出される。

[Session started at 2010-06-27 07:54:17 +0900.]
2010-06-27 07:54:25.545 MemoryStudy[33192:207] Received simulated memory warning.
2010-06-27 07:54:25.664 MemoryStudy[33192:207] didReceiveMemoryWarning


※シミュレートされたものであるとのメッセージも出力される。

Bezelボタンを作る[12]画像をグレースケールで表示する(その2)

2010年6月26日土曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回]

以前紹介した画像をグレースケール変換して表示する方法が、Alphaチャネル(半透明マスク)に対応していないことがわかった。

透明部分が黒くなる


以前は背景が白い画像を使っていたので気がつかなかったのだが、背景が透明な画像をグレースケール変換するとその部分が黒くなってしまうことがわかった。

元画像の例。図ではわからないが周りの部分が透明になっている。
これを前回の方法でグレースケール変換するとこうなる。
周りの透明部分が黒くなってしまっている。これはグレースケール変換用に作成した CGBitmapContextCreate() の引数で kCGImageAlphaNone を渡していたため。この引数は Alphaチャネルを持たないことを示している。
CGContextRef context = CGBitmapContextCreate(
             nil, image.size.width, image.size.height, 8, 0,
             colorSpace, kCGImageAlphaNone);



Alphaチャネルの合成


これを解決するには透明部分、すなわちAlphaチャネルのみを取り出した画像を作り、これをマスク情報として前回の画像に合成してやれば良い。

Alphaチャネルの画像は CGBitmapContextCreate() の引数に kCGImageAlphaOnly を渡すと作ることができる。
先の例の画像の場合、Alphaチャネルはこんな感じになる。
白い部分が透明で、黒い部分が不透明をな領域を表している。

合成には CGImageCreateWithMask() が使える。

最終的なソースコードはこんな感じ
-(UIImage*)convertGrayScaleImage:(UIImage*)image
{
 CGRect rect = CGRectMake(0.0, 0.0, image.size.width, image.size.height);
 
 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
 
 // (1) create alpha chanel
 CGContextRef alphaContext = CGBitmapContextCreate(
   nil, image.size.width, image.size.height, 8, 0,
   colorSpace, kCGImageAlphaOnly);
 CGContextDrawImage(alphaContext, rect, [image CGImage]);
 CGImageRef alphaImage = CGBitmapContextCreateImage(alphaContext);
 CGContextRelease(alphaContext);
 
 // (2) create gray scale image
 CGContextRef context = CGBitmapContextCreate(
   nil, image.size.width, image.size.height, 8, 0,
   colorSpace, kCGImageAlphaNone);
 CGContextDrawImage(context, rect, [image CGImage]);
 CGImageRef grayScaleImage = CGBitmapContextCreateImage(context);
 CGContextRelease(context);
 
 // (3) composite images
 UIImage* grayScaleUIImage = [UIImage imageWithCGImage:
          CGImageCreateWithMask(grayScaleImage, alphaImage)];
 
 CGImageRelease(grayScaleImage);
 CGImageRelease(alphaImage);
 CGColorSpaceRelease(colorSpace);
 
 return grayScaleUIImage;
}


さて実行してみよう。
背景が透明になった。


参考情報


iPhoneアプリ開発、その(185) 出るもんが出たか…|テン*シー*シー
kCGImageAlphaOnly と CGImageCreateWithMask() について参考になった。

-[NSFetchedResultsController performFetch:] でクラッシュ

2010年6月25日金曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

FATAL ERROR

NSFetchedResultsController を使っているアプリで下記のエラーが出た。

2010-06-25 12:29:11.679 HairConcierge[29825:207] *** Terminating app due to
 uncaught exception 'NSInternalInconsistencyException', reason: 'FATAL ERROR:
 The persistent cache of section information does not match the current
 configuration.  You have illegally mutated the NSFetchedResultsController's
 fetch request, its predicate, or its sort descriptor without either disabling
 caching or using +deleteCacheWithName:'

状況としては、下記のデータがありそれぞれで NSFetchedResultsController を使い UITableViewを表示しているケース。

|[ブログ][コメント]
|
|ーブログA
|  |ーーーコメント1
|  |ーーーコメント2
|  |ーーーコメント3
|  |   :
|ーブログB
|  |ーーーコメント1
|  |ーーーコメント2
|  |ーーーコメント3
|  |   :

ブログAのコメント一覧を見た後、ブログBのコメント一覧を見ようとするところで先程のエラーが発生した。


原因は NSFetchedResultsController のキャッシュ。エラーメッセージにも書いてあるようにキャッシュの内容と実体との整合性が取れていないが為に FATAL ERROR となっている( +deleteCacheWithName: を使ってキャッシュを削除しろともある)。


NSFetchedResultsController のキャッシュ


NSFetchedResultsControllerインスタンスの作成コードはこんな感じ。
NSFetchedResultsController *aFetchedResultsController =
 [[NSFetchedResultsController alloc]
  initWithFetchRequest:fetchRequest
  managedObjectContext:coreDataManager.managedObjectContext
  sectionNameKeyPath:@"postedYear" cacheName:@"Root"];

NSFetchedResultsController のキャッシュは、インスタンス生成時に名前をつけると作成される。上記の場合、@"Root" と名付けている。これは Xcodeのテンプレートで作成されたものをそのまま使っている。どうもこれが原因らしい。

リファレンス:
NSFetchedResultsController Class Reference - The Cache

リファレンスによるとキャッシュは order と sections で使われるとのこと。データそのもののキャッシュではない(実際 SQLを見てみると、キャッシュの有無にかかわらず発行されている SQLの数に違いはない)。sectionを利用していて、特に件数が多い場合にこのキャッシュが有用だと思われる(例えば各行が日付を持っていて、年単位のsectionを指定している場合など)。

キャッシュの再利用条件としてリファレンスでは次の説明がある。
The controller compares the current entity name, entity version hash, sort descriptors, section key-path, and total object count with those stored in the cache, as well as the modification date of the cached information file and the persistent store file.
これらの諸条件が揃った場合のみキャッシュが再利用される。大抵の場合、レコードに変更が入った場合には上記条件を満たさないことが多いと思われるので、キャッシュの効果が期待できるのは変更が入らない場合がほとんどだと考えて良いと思われる。


エラーの原因


今回 @"Root" という名前でキャッシュを作っていたが、NSFetchedResultsController のインスタンスはブログ毎に作り直していたので検索条件が違っていた。
ブログAのコメント一覧用の NSFetchedResultsController(条件:ブログ==ブログA)
ブログBのコメント一覧用の NSFetchedResultsController(条件:ブログ==ブログB)

コードはこんな感じ。
NSPredicate* predicate =
 [NSPredicate predicateWithFormat:@"blog == %@", blog];  // blogは NSManagedObject

[fetchRequest setPredicate:predicate];
NSFetchedResultsController *aFetchedResultsController =
 [[NSFetchedResultsController alloc]
  initWithFetchRequest:fetchRequest
  managedObjectContext:coreDataManager.managedObjectContext
  sectionNameKeyPath:@"postedYear" cacheName:@"Root"];

本来であれば別のリストを扱っているのでキャッシュ名をブログ毎に変えるべきだったのに、同じ名前@"Root"を使い回したのが今回のエラーの原因。今回はテストデータを自動生成して使っていた為、件数までも同じでキャッシュの再利用条件が満たされる状況になっていた。その為、NSFetchedResultController はブログAのコメント一覧で作成したキャッシュを、ブログBのコメント一覧で再利用する判断を行った。しかし実際にはブログAのコメント一覧とブログBのコメント一覧の中身がまったくことなる為に整合性確認で失敗し FATAL ERRORとなった(逆に同じ件数でなかったら再利用条件が満たされないのでキャッシュが再作成されてエラーに気がつかなかったのかもしれない)。

イメージ:
1. ブログAのコメント一覧作成 => キャッシュ@"Root"に格納
2. ブログBのコメント一覧作成 <= キャッシュ@"Root"の再利用
3. データの整合性が取れず、FATAL ERROR


解決策


キャッシュの使用をやめるか、キャッシュ名をリスト毎に変えれば良い。

まずキャッシュを使わない設定を行ったところ、このエラーは出なくなった。
NSFetchedResultsController *aFetchedResultsController =
 [[NSFetchedResultsController alloc]
  initWithFetchRequest:fetchRequest
  managedObjectContext:coreDataManager.managedObjectContext
  sectionNameKeyPath:@"postedYear" cacheName:nil];

次にキャッシュを使う場合は、条件に応じた名前を用意すればいいから違いとなっている部分すなわちブログの情報を元にキャッシュ名を作ればいい。

(例)blogが NSManagedObject の場合、ユニークな値として objectIDが使える
NString* cacheName = [[[blog objectID] URIRepresentation] description];
NSFetchedResultsController *aFetchedResultsController =
 [[NSFetchedResultsController alloc]
  initWithFetchRequest:fetchRequest
  managedObjectContext:coreDataManager.managedObjectContext
  sectionNameKeyPath:@"postedYear" cacheName:cacheName];

ただしこの場合、閲覧したブログの数だけキャッシュが作成されるのでその分ディスク(リファレンスにはそう書いてあった)を消費することになる。
If the controller can’t find an appropriate cache, it calculates the required sections and the order of objects within sections. It then writes this information to disk.

Bezelボタンを作る[11]ディゼーブル状態の画像をグレースケールで表示する

| Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回]

画像をグレースケールに変換する方法がわかったのでディゼーブル時の表示に適用してみる。

画像をグレースケール変換する

下記のサイトが参考になった。
[参考]Core Graphicsを使った画像のグレースケール変換と、その速度 - Ni chicha, ni limona - 平均から抜けられない僕 - iPhoneアプリ開発グループ

グレースケールの色空間を使ったビットマップコンテキストを作り、そこへ画像を書きだせばいいようだ。なるほど。

このサイトのコードを参考に(ほとんどコピー&ペーストだが)次のメソッドを作ってみた。
-(UIImage*)convertGrayScaleImage:(UIImage*)image
{
 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
 CGContextRef context = CGBitmapContextCreate(nil, image.size.width, image.size.height, 8, 0,
             colorSpace, kCGImageAlphaNone);
 CGRect rect = CGRectMake(0.0, 0.0, image.size.width, image.size.height);
 CGColorSpaceRelease(colorSpace);
 CGContextDrawImage(context, rect, [image CGImage]);
 CGImageRef grayscale = CGBitmapContextCreateImage(context);
 CGContextRelease(context);
 UIImage* grasyScaleImage = [UIImage imageWithCGImage:grayscale];
 CFRelease(grayscale);
 
 return grasyScaleImage;
}


適用前:
適用後:


上記は alpha=0.5f としてある。また毎回変換するのは勿体無いので一旦グレースケール画像ができたらキャッシュしておく。

- - - -
iPhoneでは Core Image が使えないので諦めかけていたが、簡易に出来る方法が見つかってちょっとうれしい。

iOS 4.0 でアプリを一時停止しない設定 - UIApplicationExitsOnSuspend

2010年6月24日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

iOS4.0 SDK (Xcode 3.2.3-) でビルドすると自動的に Fast App Switching が効くようになる。ホームボタンを押して終了させて、次回起動した時には終了直前の状態(画面)に復帰することができる。

これはこれで便利なのだが iPhoneシミュレータでもこれを忠実に再現していて開発時には困ったことになる。修正して再ビルドしても初期実行にならずに直前の走っていたバージョンが復帰してしまう。

これを防ぐには Info.plist に UIApplicationExitsOnSuspend=YES の設定を行う。

UIApplicationExitsOnSuspend

YESの場合は、iOS3以前と同様にアプリは完全終了する。
NOまたは設定なしの場合は、一時停止状態となり次回起動時に直前の状態が復帰される。

開発時は UIApplicationExitsOnSuspend=YES にした方が良さそうだ。


(追記 7/12)plistエディタでは「Application does not run in background」が相当する。Keyに手で「UIApplicationExitsOnSuspend」と入力すると自動的にその名称に置き換わる。

Bezelボタンを作る[10]動作モードを追加する

| Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回]Cocoaの日々: Bezelボタンを作る[09]ディゼーブル(無効)状態

前回までは押すと状態が変わるトグル動作だけだったが、今回は普通のプッシュボタンや表示を変えないモードを導入する。


動作モード


新たに modeプロパティを設けて3つのモードを追加する。
enum {
 kBezelButtonModeNormal,
 kBezelButtonModeToggle,
 kBezelButtonModeFixed
};

@interface BezelButton : UIControl {

  :
 NSInteger mode_;
}
         :
@property (nonatomic, assign) NSInteger mode;

  • kBezelButtonModeNormal は押すと引込み、離すと元に戻るモード
  • kBezelButtonModeToggle は押すと状態が替り、その後離しても元に戻らないモード
  • kBezelButtonModeFixed は押しても表示が変わらないモード(イベントは取れる)

今回ソースコードは割愛する(後日ソースは公開予定)。

利用側のコードはこんな感じ。
CGRect frame = CGRectMake(100, 100, 64, 64);
 BezelButton* button = [[BezelButton alloc] initWithFrame:frame];
 [button addTarget:self
      action:@selector(touched:)
  forControlEvents:UIControlEventTouchUpInside];
 button.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle]
              pathForResource:@"button100x100" ofType:@"png"]];
 button.innerColor = [UIColor whiteColor];
 button.enabled = YES;
 button.mode = kBezelButtonModeNormal;

[実行例]

kBezelButtonModeNormal

初期状態
押すと引込み
離すと元に戻る

kBezelButtonModeToggle

初期状態(これは recessed プロパティで変えられる)
押して
離すと引っ込んだまま

kBezelButtonModeFixed

初期状態(これは recessed プロパティで変えられる)
押しても何も起こらない

iPhone3 と 4 で座標系が変わった?

2010年6月23日水曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

iPhone3と4で影のつき方が変わった。
※どちらもシミュレータでの確認

コード
CGContextSetShadow(context, CGSizeMake(3.0, -3.0), 2.0);

iPhone3の場合
iPhone4の場合
座標系が変わっている?

Bezelボタンを作る[09]ディゼーブル(無効)状態

| Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回]Bezelボタンを作る[08]タッチした時のリアクションを実装する

今回はディゼーブル状態の描画を行う。


ディゼーブル状態の描画

Bezelボタンは UIControl のサブクラスなので有効/無効状態を表すプロパティ enabled を持っている。この値を見てディゼーブル状態の描画を行う。今回は画像の色を薄くしてみよう。具体的には alpha値を下げて半透明化する。

こんなかんじ。
- (void)drawImageOnContext:(CGContextRef)context offset:(CGPoint)offset
{
 if (self.image) {
  CGRect buttonFrame = [self bounds];
  CGRect imageFrame = CGRectInset(buttonFrame, IMAGE_PADDING, IMAGE_PADDING);
  imageFrame.origin.x += offset.x;
  imageFrame.origin.y += offset.y;

  if (self.enabled) {
   [self.image drawInRect:imageFrame];
  } else {
   [self.image drawInRect:imageFrame blendMode:kCGBlendModeNormal alpha:0.5];
  }
 }
}
半透明に画像を描くには -[UIImage drawInrect:blendMode:alpha:]を使う。

実行結果:
enabled == YES
enabled == NO

Bezelボタンを作る[08]タッチした時のリアクションを実装する

2010年6月22日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回]

タッチした時にそのリアクションとしてボタンを少し暗くする処理を追加する。

タッチ開始イベント


今回 BezelButtonは UIControlから派生しているので beginTrackingWithTouch:withEvent: をオーバライドすればいい。押された状態を管理するインスタンス変数 pushed_ を用意しておき、これをタッチ開始・終了で書き換える。
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
 pushed_ = YES;
 [self setNeedsDisplay];
 return YES;
}

- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
 pushed_ = NO;
 self.recessed = !self.recessed;
 [self setNeedsDisplay];
 
 [super endTrackingWithTouch:touch withEvent:event];
}

状態が変わったら -[UIView setNeedsDisplay] で再描画してやる。


黒半透明の矩形


次にボタンを黒くする処理。これはタッチされた状態の時に黒半透明の角丸矩形を一番上(最後)に描画する。
if (pushed_) {
   CGContextAddPath(context, roundRectPath_);
   [[UIColor colorWithWhite:0.0f alpha:0.2f] setFill];
   CGContextFillPath(context);
  }

角丸矩形は前回パス(CGPath)を作っておいたのでそれが使える。このため実質たった3行で済んだ。


さて実行してみよう。
タッチすると
暗くなる。
この状態でタッチしても
暗くなる。

いい感じだ。

Bezelボタンを作る[07]パスの作りおき CGPathRef

2010年6月21日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回]Cocoaの日々: Bezelボタンを作る[06]画像を表示する

CGPathRef


これまではパスを作るのに直接 CGContextを操作していたが、角丸矩形(RoundRect)のパスは頻繁に使われるので CGPathRef を使うことにする。
CGPath Reference


使い方は次の通り
1. CGPathCreateMutable() を使い CGMutablePathRef を作成する(CGMutablePathはCGPathのサブクラス)
2. CGPath系の関数を使い、CGMutablePath パスを追加する
3. CGContextAddPath() でコンテキストへパスを追加する
4. 利用が終わった後、CGContextAddPath で開放

今回は角丸矩形のパスを予め作成しておき、これを必要な箇所で使う方法に切り替えた。こんな感じ。
- (void)createRoundRectPath
{
 roundRectPath_ = CGPathCreateMutable();

 // Top Left
 CGPathMoveToPoint(roundRectPath_, NULL,
       contentFrame_.origin.x, contentFrame_.origin.y + CORNER_RADIUS);
 CGPathAddArcToPoint(roundRectPath_, NULL,
      contentFrame_.origin.x,
      contentFrame_.origin.y,
      contentFrame_.origin.x + CORNER_RADIUS,
      contentFrame_.origin.y,
      CORNER_RADIUS);
 // Top right
 CGPathAddArcToPoint(roundRectPath_, NULL,
      contentFrame_.origin.x + contentFrame_.size.width,
      contentFrame_.origin.y,
      contentFrame_.origin.x + contentFrame_.size.width,
      contentFrame_.origin.x + CORNER_RADIUS,
      CORNER_RADIUS);
 // Bottom right
 CGPathAddArcToPoint(roundRectPath_, NULL,
      contentFrame_.origin.x + contentFrame_.size.width,
      contentFrame_.origin.y + contentFrame_.size.height,
      contentFrame_.origin.x + contentFrame_.size.width - CORNER_RADIUS,
      contentFrame_.origin.y + contentFrame_.size.height,
      CORNER_RADIUS);
 // Bottom left
 CGPathAddArcToPoint(roundRectPath_, NULL,
      contentFrame_.origin.x,
      contentFrame_.origin.y + contentFrame_.size.height,
      contentFrame_.origin.x,
      contentFrame_.origin.y,
      CORNER_RADIUS);
 
 CGPathCloseSubpath(roundRectPath_);
}

Bezelボタンを作る[06]画像を表示する

2010年6月20日日曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回]Cocoaの日々: Bezelボタンを作る[05]タッチしてon/offを切り替える

今回は画像を表示する。

画像表示


まずプロパティを追加する。
@property (nonatomic, retain) UIImage* image;

次に描画コードを追加する。
- (void)drawImageOnContext:(CGContextRef)context offset:(CGPoint)offset
{
 if (self.image) {
  CGRect buttonFrame = [self bounds];
  CGRect imageFrame = CGRectInset(buttonFrame, IMAGE_PADDING, IMAGE_PADDING);
  imageFrame.origin.x += offset.x;
  imageFrame.origin.y += offset.y;

  [self.image drawInRect:imageFrame];
 }
}
offset はボタンの状態(浮き出ているのか凹んでいるのか)によって画像の位置を微調整する。そうしないと画像の位置が固定で枠だけが浮き出たり凹んだりしてして不自然な感じなる。

画像は次のものをサンプルで用意した。
実行してみよう。
押すと浮き出る。
いい感じだ。


なお画像の描画に CGContextDrawImage() を使うこともできるが、引数として渡す UIImage.CGImage が座標系の違いにより上下反転(flip)したものになってしまう。
[例]
CGContextDrawImage(context, imageFrame, self.image.CGImage);
この点、-[UIImage drawInRect:] を使うと座標系の違いを気にしなくて良い。


参考情報


CGContextDrawImage draws image upside down when passed UIImage.CGImage - Stack Overflow

CGContextDrawImage と -[UIImage drawInRect:] の件

(旧) Cocoaの日々: NSImage の isFlipped/setFlipped: が Deprecated
※この例は CGAnffineTransform ではなく NSAffineTransformを使っている。

補間方法を指定してUIImageを縮小する(1) - tosh-tの日記
画像を縮小する時の補完方法について。

Bezelボタンを作る[05]タッチしてon/offを切り替える

2010年6月19日土曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回]

タッチして on/offを切り替える処理を加える。

Bezelボタンクラス


BezelButtonクラスを作る。ボタンを実装するにあたっては UIButton ではなく、UIControl のサブクラスを使う。

@interface BezelButton : UIControl {

 BOOL recessed_;
}
@property (nonatomic, assign) BOOL recessed;

@end

プロパティ recessed は、ボタンの状態を表し、YESの場合 reccesed(凹んだ)状態、NOの場合は浮き出た状態を表す。このプロパティを見て(前回までの)描画を切り替える。


UIControlのイベント処理


UIControlのサブクラスでイベントを処理する場合、beginTrackingWithTouch:withEvent:, continueTrackingWithTouch:withEvent:, endTrackingWithTouch:withEvent: をオーバライドする。今回は単純に endTrackingWithTouch:withEvent: をオーバライドして、このタイミングで recessed を切り替える。
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
 self.recessed = !self.recessed;
 [self setNeedsDisplay];
 
 [super endTrackingWithTouch:touch withEvent:event];
}

実行してみよう。

初期状態は凹んだ状態。

タッチアップすると浮き出た状態になる。

以後、この繰り返し。

Bezelボタンを作る[04]もう一つの状態を描く

2010年6月18日金曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回]Cocoaの日々: Bezelボタンを作る[03]矩形の内側に影を落とす

さて目標はボタンを作ることなので on/offの状態を描かなければならない。前回までで描いた Bezel の反対、すなわち今度は浮き出た(飛び出た?)表現を作る。
といっても既に影を描くところで作ったのでコードを整理するだけ。
- (void)drawRect:(CGRect)rect {
 CGRect frame = CGRectInset([self bounds], INSET_SIZE, INSET_SIZE);
 CGContextRef context = UIGraphicsGetCurrentContext();

 CGContextSaveGState(context);
 [self addRoundRectPath:context rect:frame];

 CGContextSetShadow(context, CGSizeMake(SHADOW_SIZE, -SHADOW_SIZE), SHADOW_BLUR);
 [[UIColor whiteColor] setFill];
 CGContextFillPath(context);

 CGContextRestoreGState(context);

 [self addRoundRectPath:context rect:frame];
 [[UIColor colorWithRed:0.75f green:0.75f blue:0.75f alpha:1.0f] setStroke];
 CGContextSetLineWidth(context, 0.5f);
 CGContextStrokePath(context);
}
こちらも縁取りしてある。

さて次は Touchイベントを広って表示を切り替える(トグル動作)。

Bezelボタンを作る[03]矩形の内側に影を落とす

2010年6月17日木曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: Bezelボタンを作る[02]影を描く

さて今回はいよいよ Bezel表現を完成させる。


矩形の内側に影を落とす


[サンプル]

矩形の内側に影を落とす、と書いたが何の影を落とすのか?矩形の左上に影ができるのだから...

こんな中を抜いた図形が作って描画すれば矩形の内側に影ができるだろう。
影ができればこの図形自体は不要なので(矩形の内側に)クリッピングして隠してしまえば良い。

コードはこんな感じ。
- (void)drawRect:(CGRect)rect {

 CGRect frame = CGRectInset([self bounds], INSET_SIZE, INSET_SIZE);
 CGContextRef context = UIGraphicsGetCurrentContext();

 CGContextSaveGState(context);

 // [1] clipping
 [self addRoundRectPath:context rect:frame];
 CGContextClip(context);

 // [2] fill background in round rect
 [[UIColor whiteColor] setFill];
 CGContextFillRect(context, frame);
 
 // [3] create a shape for shadow
 CGContextAddRect(context, [self bounds]);
 [self addRoundRectPath:context rect:frame];
 [[UIColor whiteColor] setFill];

 // [4] add shadow
 CGContextSetShadow(context, CGSizeMake(SHADOW_SIZE, -SHADOW_SIZE), SHADOW_BLUR);
 
 // [5] draw the shape
 CGContextEOFillPath(context);
}
実行するとこうなる。

コードの [3]と[5]の部分が影を作るための中抜き図形を描いている部分。CGContextEOFillPath() は、それまでに追加されたパスで重なる部分を描画しないという性質を持っているのでこれを使っている。[3]の部分で最初に外側の四角を描き、次にその内側に角丸矩形を描いているので、CGContextEOFillPath() を使うと内側の角丸矩形を抜いた形で塗りつぶされる。これはなかなか便利だ。
またこのままだとこの図形が描画されてしまうので、最初の[1]でクリッピングしておく。


なお若干縁の部分が白くなったりしているので、これを隠すために縁に線を引く。
[self addRoundRectPath:context rect:frame];
 [[UIColor colorWithRed:0.5f green:0.5f blue:0.5f alpha:1.0f] setStroke];
 CGContextSetLineWidth(context, 0.5f);
 CGContextStrokePath(context);

これでできあがり。

Bezelボタンを作る[02]影を描く

2010年6月16日水曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回]Cocoaの日々: Bezelボタンを作る[01]Round Rect を描く

今回は Quartz 2D を使い影を描く。

影を描く

CGContextSetShadow()を使う。

CGContext Reference

こんな感じ。
- (void)drawRect:(CGRect)rect {

 CGContextRef context = UIGraphicsGetCurrentContext();
 CGRect frame = CGRectInset([self bounds], 4.0f, 4.0f);
 CGContextSetShadow(context, CGSizeMake(3.0f, -3.0f), 2.0f);
 [[UIColor whiteColor] set];
 [self addRoundRectPath:context rect:frame];
 CGContextFillPath(context);
}

最初に呼び出しておけば、それ以降の描画に影がつく。

[実行例]


影付けをoffにする関数は無いので、on/offを制御したい場合は CGContextSaveGState() と CGContextStoreGState() を組みで使う。

(例)
  :
CGContextSaveGState(context);
CGContextSetShadow(context, CGSizeMake(3.0f, -3.0f), 2.0f);
  :
影付き描画
  :
CGContextRestoreGState(context)
  :
影なし描画
  :

Bezelボタンを作る[01]Round Rect を描く

2010年6月15日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

Bezel

Bezel とはこんなやつ。
くぼんでいるように見える表現で、大抵は左上から光が当たっているのを仮定して、左と上側に影が落ちているような表現になっている。最終的にはボタンとして機能するようにする。

まずは、これを iPhone で描いてみよう。
iPhone 上でのグラフィック描画には Quartz 2D のCG系関数を使う。
CGContext Reference

描くにあたって次のステップで進めてみた。

  1. 角丸矩形(RoundRect)を描く
  2. 影を描く
  3. 矩形の内側に影を落とす
順番にやっていく。

角丸矩形(RoundRect)を描く

Mac OS X だと NSBezierPath を使い簡単に角丸矩形が描けるが、iPhoneの場合はこれが使えない。実は iPhone OS 3.2 からだと UIBezierPath が用意されたので代わりにこれが使える。ただ今回は OS 3.1.3 向けに作りたいので、自前で描くことにする。

UIBezierPath Class Reference

ネットで調べてみると CALayer を使う方法と自前で描く方法の2種類があった。今回は UIView へ直接描画するので後者の方法をとることにした。これは角丸の部分を CGContextAddArcToPoint() を使い一つ一つ描いていく。コードはこんな感じ。
- (void)addRoundRectPath:(CGContextRef)context rect:(CGRect)rect
{
 // Top Left
 CGContextMoveToPoint(context, rect.origin.x, rect.origin.y + CORNER_RADIUS);
 CGContextAddArcToPoint(context,
         rect.origin.x,
         rect.origin.y,
         rect.origin.x + CORNER_RADIUS,
         rect.origin.y,
         CORNER_RADIUS);
 // Top right
 CGContextAddArcToPoint(context,
         rect.origin.x + rect.size.width,
         rect.origin.y,
         rect.origin.x + rect.size.width,
         rect.origin.y + CORNER_RADIUS,
         CORNER_RADIUS);
 // Bottom right
 CGContextAddArcToPoint(context,
         rect.origin.x + rect.size.width,
         rect.origin.y + rect.size.height,
         rect.origin.x + rect.size.width - CORNER_RADIUS,
         rect.origin.y + rect.size.height,
         CORNER_RADIUS);
 // Bottom left
 CGContextAddArcToPoint(context,
         rect.origin.x,
         rect.origin.y + rect.size.height,
         rect.origin.x,
         rect.origin.y,
         CORNER_RADIUS);

 CGContextClosePath(context);
} 
面倒だが複雑ではない。四辺と四つの角丸(arc)を一つづつ繋げているだけ。

参考サイト:Drawing a round rect with Quartz « dPompa

このメソッドでパスを描いた後、CGContextFillPath() などで色を塗れば良い。
- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    [[UIColor whiteColor] set];
    [self addRoundRectPath:context rect:rect];
    CGContextFillPath(context);
}

実行例

人気の投稿(過去 30日間)