こんにちは、きんくまです。
オリジナルアプリをコツコツ作ってきまして、ついに完成しました。
今、審査待ちに入っています。
名前はHabitKeeperというもので、日課を記録できるアプリです。
審査してもらっている間に、アプリの説明文の英訳やらを翻訳サービスに頼んでみたり、
プレスキットとか作ってみようと思っています。
今月中にリリースできるといいな。
2012/10/3
2012/09/27
こんにちは。きんくまです。
CoreDataのメモです。
別プロジェクトで作った.xcdatamodeldファイル(EntityやAttribute設定ずみ)を他のプロジェクトにドラッグ&ドロップで持ってきました。
それで読み込んだ方のAppDelegateで読み込むファイル名をそのファイル名に変更しました。
//AppDelegate内
//- (NSManagedObjectModel *)managedObjectModelのところ
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"CoreDataTestModel" withExtension:@"momd"];
//- (NSPersistentStoreCoordinator *)persistentStoreCoordinatorのところ
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CoreDataTestModel.sqlite"];
それでビルドするとエラーがでます。
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Cannot create an NSPersistentStoreCoordinator with a nil model'
最初、絶対にファイルメニューから新規でxcdatamodelファイルを作って、そこからスタートするのかと思いました。
調べてみたところ、きちんとやり方がありました。
>> iPhone – Add Core Data To existing Project?
Chris Gummerさんによると
・momdファイルはバージョン化されたmomファイル
・外から持ってきたxcdatamodelはまだバージョン化されていないmomファイルの状態
・なので、もしインポートしたいときは.xcdatamodelを選択してEditor > Add Model Versionする
(元コメントはXcode3用?っぽかったのでXcode4だとこうなる)
・Xcodeのファイルメニューから作るxcdatamodelファイルは作成時に自動でバージョン化されている
・もしバージョン化しない場合はmodelを読み込む拡張子をmomdでなくmomにする
バージョン化って何なのかというと、CoreDataのモデリングしてアプリをリリースした後に、
アップデートなどで、例えばEntityやAttributeを追加したいときがあります。
そういうときに、モデリングのバージョンを別バージョンとして定義することが可能です。
古いバージョンから新しいバージョンにするときは、更新というか統合というかそういう作業が必要なのですが、まだ詳しくないのでそこは省略。
今回の場合、ドラッグドロップしたばっかりがこんな感じです。
このときはバージョン化されてない状態なので、拡張子momで読み込み。.xcdatamodelというファイルになっている。
それをバージョン化するとこんな感じになります。拡張子momdで読み込み。xcdatamodeld(dが末尾に追加)というファイルになっている
緑のチェックが現在のバージョン
さて、これでうまくいくのですが、なんかインポートしただけなのにいらないバージョンがあるというのはどうもいやな感じです。
で、いらないバージョンを削除する方法を調べてみました。
>> How to delete an old/unused Data Model Version in xCode 4
David Avendasoraさんのコメントによると
1. 欲しいバージョンをXcode上でカレントバージョンにセット。
2. .xcdatamodeldをファイルメニューから削除(Remove Reference Only)
3. Finderで.xcdatamodeldを選択して、右クリック > Show Package Contents
4. いらない.xcdatamodelファイルを削除
5. もう一回プロジェクトに.xcdatamodeldを追加
するとXcodeで新規ファイルを作成した状態になっています。
良かった良かった。
2012/09/25
こんにちは。きんくまです。
今回はとりとめもないCoreDataまわりの日記です。
MVCパターンの基本はModelにDBのロジックを組み込むことらしいので、
NSManagedObjectのサブクラスに挿入/検索/削除/更新みたいなメソッド作りました。
そんで、これがうまくいってるのかためしたいので、ダミーのコードを書いてみて、
合ってる、合ってないって目でみていたのだけれど、「あっ、これがユニットテストなのか」とここで気づきました。
実はちゃんとしたxUnitでテストコードを書いたことはないです、、。ハハ。
何ヶ月か前にテストってそもそも何なのよ?と気になり本を読んだりした。
これ読む限り、体系立ててやってはいなかったけど、近いことはやっていたみたい。
機能作って、テスト用のコード書いて合ってるか調べたり、実際に動くものを手でテストしたりとか。
それで今回良い機会なので、Objective-Cでユニットテストやってみたいなと思ったのです。
調べたところ、OCUnit、GHUnit、OCMockという単語が上がってきた。
>> GHUnitで単体テストをしてみよう – mixi Engineers’ Blog
>> GHUnitとOCMockでUnit Test効率化 – jarinosuke blog
>> A-Liaison BLOG: Jenkins を iOS アプリ開発に導入してみた (GHUnit編)
>> Unit Testing in Xcode 4 Quick Start Guide
OCUnit シンプル
GHUnit 高機能
OCMock ユニットテストとは違うモックオブジェクトというものを使ったテスト
全部理解しようとしても時間がかかるので、GHUnit + OCMockは後回しにして、
取り急ぎOCUnitで基本的な使い方を覚えてみようと思ってる。
そんで、調べてる途中で見つけたのだけれど、MagicalRecordというものがあるらしい。
>> MagicalRecord(git-hub)
>> Magical Record: how to make programming with Core Data pleasant « Yannick Loriot
なんでロゴが漢字の「棒」なのかは全くわからないのだけれど、CoreDataをActiveRecordパターンっぽく使うものらしい。
もっと簡単にいうと、CoreDataのラッパークラスみたいな。
ActiveRecordパターンって何なのか調べてみると、SQL文かかなくてもよくて、オブジェクトにプロパティ設定するみたいにDBいじれるって理解でOK? たぶん。
APIのサンプル見た感じだと、「あれこれって、今自分で書いてるNSManagedObjectのサブクラスにメソッド設定するやつ、ActiveRecordがやってくれるからしなくてもよくね?」と思いましたです。
自分で書いてるのがマニュアルだとすると、ActiveRecord使ったやつはオートマですな。
なれるとこっちの方が、書くのは速そう。
2012/09/22
こんにちは。きんくまです。
またまた備忘録です。
DBからNSManagedObjectでなく、EntityのAttributeの最大値や平均をもとめたいときがあります。
最初はManagedObject全部引っ張ってきて手動で計算するのかな?と思ったのですが、ちゃんとやり方がありました。
Appleのドキュメントに書いてありましたです。
Core Data Programming Guide – Fetching Specific Values
今回のコードはここに書いてあるそのまんまです。すみません、、。
やり方は通常とちょっとだけ違っていて
1. NSFetchRequestにsetResultType:NSDictionaryResultTypeとすること
2. NSExpressionDescriptionをセットすること
となってます。FetchRequestの結果は通常と同じArrayなんですが、その中身がDictionaryになってるところがポイントです。
NSExpressionDescriptionを作るのがまた少し手間ですが
1. NSExpressionを作成
評価したいAttributeをセットして、さらにそいつを引数に使い、expressionForFunction:argumentsを作る感じです。
expressionForFunction:argumentsの最初の引数は求めたいタイプ(最大値、最小値、平均など)を入れます。
そのリストはここにあります。
>> expressionForFunction:arguments:
例えば最大値なら@”max:”となります。
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:@"salary"];
NSExpression *maxSalaryExpression = [NSExpression expressionForFunction:@"max:"
arguments:[NSArray arrayWithObject:keyPathExpression]];
2. NSExpressionDescriptionを作る
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init]; [expressionDescription setName:@"maxSalary"]; [expressionDescription setExpression:maxSalaryExpression]; [expressionDescription setExpressionResultType:NSDecimalAttributeType];
nameはfetchで返ってくるdictionaryのキーになります。
expressionは1でつくったNSExpressionです。
expressionResultTypeはNSAttributeType一覧にあります。返ってくるdictionaryの値のタイプです。整数、小数、日付とかです。
3.NSFetchRequestにセット
最後にfetchRequestにセットして準備完了です。
[request setPropertiesToFetch:[NSArray arrayWithObject:expressionDescription]];
公式のサンプルのまねして書いてみるとこんな感じ。
- (NSInteger)maxDisplayOrderValue
{
NSFetchRequest *req = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event" inManagedObjectContext:self.managedObjectContext];
[req setEntity:entity];
[req setResultType:NSDictionaryResultType];
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:@"displayOrder"];
NSExpression *maxExpression = [NSExpression expressionForFunction:@"max:" arguments:@[keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName:@"maxDisplayOrder"];
[expressionDescription setExpression:maxExpression];
[expressionDescription setExpressionResultType:NSInteger32AttributeType];
[req setPropertiesToFetch:@[expressionDescription]];
NSError *error = nil;
NSArray *obj = [self.managedObjectContext executeFetchRequest:req error:&error];
NSInteger maxValue = NSNotFound;
if(obj == nil){
NSLog(@"error");
}else if([obj count] > 0){
maxValue = [obj[0][@"maxDisplayOrder"] integerValue];
}
return maxValue;
}
こんにちは。きんくまです。
個人的メモです。すんません。
CoreDataを使って、手動で並び替えをしたかったので調べてました。
Matt Long さんが書いている記事が参考になります。
>> RE-ORDERING NSFETCHEDRESULTSCONTROLLER
ただ、このままだと自分の方だとうまくいかなかったので、調整しました。
並び替えをしたいEntityのAttributesにdisplayOrderを追加。Integer32にしといた。
fetchedResultsControllerをつくるときにdisplayOrderでソート
例えば、Xcodeのテンプレートからできるやつでやると
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"displayOrder" ascending:YES];
NSArray *sortDescriptors = @[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
という感じ。さらにこいつも追加しておく
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
それで、- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPathのところを設定すればOKなんだけど、こいつの挙動について
調べてみた。カメラでとりましたです。
この挙動にそって、各NSManagedObjectのdisplayOrderを再設定してあげれば大丈夫なはず。
今回このロジックのところだけ考えましたです。
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
NSInteger minRowIdx, maxRowIdx;
BOOL isMoveDirectionSmallToLarge;
if(sourceIndexPath.row == destinationIndexPath.row){
return;
}else if(sourceIndexPath.row < destinationIndexPath.row){
minRowIdx = sourceIndexPath.row;
maxRowIdx = destinationIndexPath.row;
isMoveDirectionSmallToLarge = YES;
}else{
minRowIdx = destinationIndexPath.row;
maxRowIdx = sourceIndexPath.row;
isMoveDirectionSmallToLarge = NO;
}
for(NSInteger i = minRowIdx; i <= maxRowIdx; i++){
NSManagedObject *managedObj = [self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
NSNumber *displayOrder = [managedObj valueForKey:@"displayOrder"];
NSInteger newOrder;
if(i == sourceIndexPath.row){
newOrder = destinationIndexPath.row;
}else if(isMoveDirectionSmallToLarge){
newOrder = [displayOrder integerValue] - 1;
}else{
newOrder = [displayOrder integerValue] + 1;
}
[managedObj setValue:@(newOrder) forKey:@"displayOrder"];
}
NSError *error = nil;
if (![_managedObjectContext save:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
}
// NSLog(@"source %d, destination %d", sourceIndexPath.row, destinationIndexPath.row);
}
2012/09/21
こんにちは。きんくまです。
正規化(normalization)という言葉があります。
wikiによると
正規化(せいきか、normalization)とは、 データ等々を一定のルール(規則)に基づいて変形し、利用しやすくすること。
だそうです。
そんで、いろんな分野で利用されてるみたいなんですけど、自分もたまに利用してマス。気づかずに使ってる人もいると思います。
自分の場合は0から1の間に正規化してます。
クライアント系のプログラムだとTweenのイージング関数とか、テクスチャのUVとかもこの値が使われてたりしますよね。
今回作成したのは、正規化を利用したスライダーです。
スライダーを動かすと、そのときの数値がラベルに表示されて、また緑の円の大きさがそれに合わせてかわります。
このときに、スライダーから出てくる数値が常に1から0であれば、
現在の円の大きさ = 円の最大の大きさ x スライダーの数値
で表すことができます。
また、このスライダーを利用して様々なものを作れます。
例えば、これをスクロールバーにしたりとか。
ここで大事なことは、スライダーのタブの位置情報が1から0に変換されているということです。スライダーがすごいとかじゃなくて。
タブのX座標という具体的な情報が、1から0という他のところで使いやすい値に抽象化されているところがポイントです。
具体的なコードです。
ARC使用 iOS5以上
QuartzCore.frameworkを足しておいてください。
KKSliderView.h
#import <UIKit/UIKit.h> #ifndef RGB #define RGB(r,g,b) [UIColor colorWithRed:r/255.0 green:g/255.0 blue:b/255.0 alpha:1] #endif @class KKSliderView; @protocol KKSliderViewDelegate <NSObject> @optional - (void)sliderValueChanged:(KKSliderView *)sliderView value:(CGFloat)value; @end @interface KKSliderView : UIView @property (nonatomic, weak) id<KKSliderViewDelegate> delegate; @end
KKSliderView.m
#import "KKSliderView.h"
#import <QuartzCore/QuartzCore.h>
const CGFloat kKKSliderTabWidth = 44.0;
const CGFloat kKKSliderTabPadding = 2.0;
@interface KKSliderPartsLayer : CALayer
{
UIColor *_fillColor;
}
- (id)initWithFillColor:(UIColor *)aFillColor frame:(CGRect)aFrame;
@end
@implementation KKSliderPartsLayer
- (id)initWithFillColor:(UIColor *)aFillColor frame:(CGRect)aFrame
{
self = [KKSliderPartsLayer layer];
if(self){
_fillColor = aFillColor;
self.frame = aFrame;
self.contentsScale = [UIScreen mainScreen].scale;
[self setNeedsDisplay];
}
return self;
}
- (void)drawInContext:(CGContextRef)ctx
{
CGContextSetFillColorWithColor(ctx, _fillColor.CGColor);
CGPathRef path = CGPathCreateCopy([UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:4.0].CGPath);
CGContextAddPath(ctx, path);
CGContextFillPath(ctx);
}
@end
@interface KKSliderView()
{
CALayer *_tabLayer;
CALayer *_tabBackgroundLayer;
BOOL _dragging;
CGSize _dragOffset;
CGFloat _minTabX;
CGFloat _maxTabX;
}
@end
@implementation KKSliderView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
_tabLayer = [[KKSliderPartsLayer alloc] initWithFillColor:RGB(43, 170, 154) frame:CGRectMake(kKKSliderTabPadding, kKKSliderTabPadding, kKKSliderTabWidth - kKKSliderTabPadding * 2, self.bounds.size.height - kKKSliderTabPadding * 2)];
_tabBackgroundLayer = [[KKSliderPartsLayer alloc] initWithFillColor:RGB(38,48,37) frame:self.bounds];
[self.layer addSublayer:_tabBackgroundLayer];
[self.layer addSublayer:_tabLayer];
_dragging = NO;
_minTabX = kKKSliderTabPadding;
_maxTabX = self.bounds.size.width - kKKSliderTabWidth + kKKSliderTabPadding;
}
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self];
if([_tabLayer hitTest:location]){
_dragging = YES;
_dragOffset = CGSizeMake(_tabLayer.frame.origin.x - location.x, _tabLayer.frame.origin.y - location.y);
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self];
if(_dragging){
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue
forKey:kCATransactionDisableActions];
CGFloat posX = location.x + _dragOffset.width;
if(posX < _minTabX){
posX = _minTabX;
}else if(posX > _maxTabX){
posX = _maxTabX;
}
_tabLayer.frame = CGRectMake(posX, _tabLayer.frame.origin.y, _tabLayer.frame.size.width, _tabLayer.frame.size.height);
[CATransaction commit];
if([_delegate respondsToSelector:@selector(sliderValueChanged:value:)]){
[_delegate sliderValueChanged:self value:[self normalizedValue]];
}
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
_dragging = NO;
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
_dragging = NO;
}
- (CGFloat)normalizedValue
{
return (_tabLayer.frame.origin.x - _minTabX) / (_maxTabX - _minTabX);
}
@end
delegateを設定すると、値がかわったときに
– (void)sliderValueChanged:(KKSliderView *)sliderView value:(CGFloat)value;
で呼び出されます。
使い方です。メインのViewController
KKViewController.h
#import <UIKit/UIKit.h> #import "KKSliderView.h" @interface KKViewController : UIViewController<KKSliderViewDelegate> @end
KKViewController.m
#import "KKViewController.h"
#import <QuartzCore/QuartzCore.h>
const CGFloat kCircleLayerMaxDiameter = 200;
const CGPoint kCircleCenterPt = {320 * 0.5, 200 + kCircleLayerMaxDiameter * 0.5};
@interface KKViewController ()
{
KKSliderView *_sliderView;
UILabel *_currentValueLabel;
CALayer *_circleLayer;
CGPoint _circleCenterPt;
}
@end
@implementation KKViewController
- (void)loadView
{
UIView *myView = [[UIView alloc] init];
myView.backgroundColor = [UIColor whiteColor];
_sliderView = [[KKSliderView alloc] initWithFrame:CGRectMake(50, 50, 220, 30)];
_sliderView.delegate = self;
[myView addSubview:_sliderView];
_currentValueLabel = [[UILabel alloc] initWithFrame:CGRectMake(50, 120, 220, 36)];
[_currentValueLabel setFont:[UIFont fontWithName:@"Helvetica-Bold" size:24]];
_currentValueLabel.textAlignment = NSTextAlignmentCenter;
[myView addSubview:_currentValueLabel];
_circleLayer = [CAShapeLayer layer];
_circleLayer.delegate = self;
_circleLayer.frame = CGRectMake(kCircleCenterPt.x, kCircleCenterPt.y, 0, 0);
_circleLayer.contentsScale = [UIScreen mainScreen].scale;
[myView.layer addSublayer:_circleLayer];
self.view = myView;
}
- (void)sliderValueChanged:(KKSliderView *)sliderView value:(CGFloat)value
{
NSNumberFormatter *numFormatter = [[NSNumberFormatter alloc] init];
[numFormatter setPositiveFormat:@"#.###"];
_currentValueLabel.text = [numFormatter stringFromNumber:@(value)];
CGFloat currentCircleDiameter = kCircleLayerMaxDiameter * value;
_circleLayer.frame = CGRectMake(kCircleCenterPt.x - currentCircleDiameter * 0.5, kCircleCenterPt.y - currentCircleDiameter * 0.5, currentCircleDiameter, currentCircleDiameter);
[_circleLayer setNeedsDisplay];
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
if([layer isEqual:_circleLayer]){
CGContextSetFillColorWithColor(ctx, RGB(43, 170, 154).CGColor);
CGContextFillEllipseInRect(ctx, layer.bounds);
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
2012/09/20
こんにちは。きんくまです。
Auto Layoutをすべてのものに適用してもよいのですが、
大枠はAuto Layoutにして各パーツは普通のframeを使ったレイアウトにしたい場合もあります。
例えば、ヘッダー、フッター、コンテンツ部分はAutoLayoutを使って、コンテンツ部分はframeを使いたいなどです。
そんなときのコードサンプルです。
今回の場合は赤い部分がAutoLayoutを使っていて、そのsubviewの青い部分はframeを使っています。
ARC利用
- (void)loadView
{
UIView *myView = [[UIView alloc] init];
myView.backgroundColor = [UIColor greenColor];
UIView *parentRedView = [[UIView alloc] init];
parentRedView.backgroundColor = [UIColor redColor];
UIView *childBlueView = [[UIView alloc] init];
childBlueView.backgroundColor = [UIColor blueColor];
[myView addSubview:parentRedView];
[parentRedView addSubview:childBlueView];
if([myView respondsToSelector:@selector(addConstraint:)]){
[myView setTranslatesAutoresizingMaskIntoConstraints:NO];
[parentRedView setTranslatesAutoresizingMaskIntoConstraints:NO];
NSMutableArray *constraints = [NSMutableArray array];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"|-50-[parentRedView(==180)]" options:0 metrics:nil views:NSDictionaryOfVariableBindings( parentRedView)]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-80-[parentRedView]-(<=30)-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(parentRedView)]];
[myView addConstraints:constraints];
}else{
parentRedView.frame = CGRectMake(50, 80, 180, 200);
}
childBlueView.frame = CGRectMake(20, 30, 80, 120);
self.view = myView;
}