何をやりたいか
むかし買った「Cocoa Programming Guide for Mac OS(HillegassさんPrebleさん共著)」にCoreDataの解説があるのですが、何回やっても本のとおりに動かず、挫折していました。おそらく発売のすぐ後にXcodeがバージョンアップされて仕様が変わったのでしょう。
@pe-taさんのやっとわかったSwift/CoreData入門などでCoreDataが少しわかってきたので、原作を簡易化したCoreDataのプログラムを書いてみます。基本的に私の投稿「やっとわかったCoreData」のモノマネを出発点としますが、HillegassさんはTableViewを直接に操作しているので、かなりハードルの高い挑戦になりそうです。
なおバージョンはmacOSが12.3.1、Xcodeが13.3です。
小さなノウハウ(Hillegassさんの受け売り)
CoreDataでたとえばモンスターの配列を扱うとき、Entity名を@"Monster"とし、そのAttributeとして名前を@"monsterName"とするなど、Objective-Cの文字列がよく出てきますね。CoreDataに限らずKey-Value Codingなどでは引数としてナマの文字列を与えることがよくあります。この文字列の綴りをまちがえてもコンパイラはエラーを指摘してくれないので、バグの発見が困難です。
こんなときはナマの文字列を定数にしておくのがいいそうです。私は@"Monster"をMYENTITY、@"monsterName"をTHE_NAMEにしました。これなら綴りをまちがえるとXcodeが即座に指摘してくれます。
プロジェクトの作成
以下の記述はこの投稿だけで完結するようにいたします。前回の投稿と同じような文章が出てきますが、ご了承下さい。
Xcode→File→New→Projectとし、最上段をmacOS(iOSではない)とし、APPを指定します。次のページに移って、プロダクト名は任意(たとえばMyCoreData)、言語はObjective-Cにします。UseCoreDataのチェックをお忘れなく。
チーム名などはNoneのままでOKです。
Entityの設定
新しいEntityを作り、名前をMonsterとします。そのAttributeは一つだけで、NameはmonsterName、TypeはStringとします。
EntityのMonsterを選択し、右側のCodegenというところがClass Definitionになっているのを確認して下さい。多分デフォールトでそうなっている筈です。
ViewController.m
いつも乱暴なやり方ですが、元のコードをすべて削り、代わりに次のコードをこのままペーストして下さい。
//
// ViewController.m
// MyCoreData
//
// Created by Myself on 2022/03/25.
// Copyright © 2022 None. All rights reserved.
//
#import "ViewController.h"
ViewController *viewctrl_ptr = nil; /* (referenced by 'AppDelegate') */
@implementation ViewController
{
#pragma mark global variables within ViewController
const NSString *MYENTITY; /* for @"Monster" */
const NSString *THE_NAME; /* for @"monsterName" */
AppDelegate *parent_ptr;
NSManagedObjectModel *my_mom;
NSPersistentStoreCoordinator *my_psc;
NSManagedObjectContext *my_moc;
NSFetchRequest *my_fetch;
NSMutableArray<NSManagedObject *> *monstary; /* array of monsters */
NSInteger nbr_rows; /* number of rows in table-view */
BOOL editflag; /* editing now or not */
}
#pragma mark initializers
- (void)viewDidLoad
{ [super viewDidLoad];
MYENTITY = @"Monster";
THE_NAME = @"monsterName";
viewctrl_ptr = self; /* set public variable */
tbl_view_ptr.delegate = self;
tbl_view_ptr.dataSource = self;
monstary = [[NSMutableArray alloc] init]; /* initialize monster array */
nbr_rows = 0; /* no row in table-view */
editflag = NO; /* not editing */
} // viewDidLoad
- (void)setRepresentedObject:(id)representedObject
{ [super setRepresentedObject:representedObject];
// Update the view, if already loaded.
} // setRepresentedObject
#pragma mark public functions
- (void)setParent_ptr:(AppDelegate *)myparent
/* called by 'AppDelegate' */
{ parent_ptr = myparent;
NSLog(@"parent ready");
} // setParent_ptr
-(void)set_pc
/* set persistent-container variables */
/* called by 'AppDelegate' */
/* called after AppDelegate ready */
{ [self initcore]; /* initialize core data */
[tbl_view_ptr reloadData]; /* display registered Monsters */
} // set_pc
- (void)save_ary
/* save the whole managed object context */
/* called by 'AppDelegate' */
{ NSError *errorptr;
BOOL myresult;
errorptr = nil;
myresult = [my_moc save: &errorptr];
if ( ( myresult == NO ) || ( errorptr != nil ) )
NSLog(@"managed object context save error");
else
NSLog(@"managed object context successfully saved");
} // save_ary
#pragma mark private function
- (void)editdoneWith: (NSString *)new_name
/* edit done .. create one monster with new_name */
/* called by 'controlTextDidEndEditing' */
{ NSLog(@"starting edit-done");
NSAssert(nbr_rows > 0, @"table empty after editing");
if ( editflag == NO )
NSLog(@"strange .. 'edit done' called while not editing");
else
{ editflag = NO;
if ( new_name.length == 0 )
{ NSLog(@"invalid .. monster name == empty string");
nbr_rows--; /* delete bottom row */
}
else /* valid monsterName */
{ [self createOneWithName: new_name]; /* create one monster */
NSLog(@"one monster created with name '%@'", new_name);
[tbl_view_ptr reloadData]; /* ('nbr_rows++' unnecessary) */
}
} // editflag == YES
} // editdone
#pragma mark event handlers
- (IBAction)add_pressed: (NSButton *)sender
{ nbr_rows++; /* set new empty row */
[tbl_view_ptr reloadData];
editflag = YES; /* editing now */
NSLog(@"going to input monster name");
[tbl_view_ptr editColumn: 0 row: (nbr_rows - 1) withEvent: nil select: YES];
} // add_pressed
- (IBAction)remove_pressed: (NSButton *)sender
{ NSManagedObject *one_monster;
NSInteger curr_row; /* current row to delete */
curr_row = [tbl_view_ptr selectedRow]; /* (only one by definition) */
if ( curr_row < 0 ) /* no selected row if curr_row == -1 */
NSLog(@"no selected row");
else
{ one_monster = [monstary objectAtIndex: curr_row];
[my_moc deleteObject: one_monster];
NSLog(@"one_monster deleted from context");
[monstary removeObjectAtIndex: curr_row];
NSLog(@"one_monster removed from monster array");
nbr_rows--;
[tbl_view_ptr reloadData];
}
} // remove_pressed
- (void)controlTextDidEndEditing:(NSNotification *)obj
/* event handler (NSControlTextEditingDelegate) */
/* probably enter-key pressed during input */
/* extract monster-name here (with some luck) */
/* note: notified by tableView, not by textfield-cell */
{ NSString *mystring;
NSLog(@"notification from table-view");
mystring = [tbl_view_ptr stringValue]; /* get monster name */
NSAssert(mystring != nil, @"monster name in cell == nil");
NSLog(@"string in cell == %@", mystring);
[self editdoneWith: mystring]; /* edit done .. create monster */
} // controlTextDidEndEditing
#pragma mark core data functions
- (void)initcore
/* initialize core data */
/* note: 'my_mom' and 'my_psc' not used */
{ NSArray *temp_ary;
NSError *errorptr;
NSAssert(parent_ptr.persistentContainer != nil, @"persistentContainer failed");
my_mom = parent_ptr.persistentContainer.managedObjectModel;
NSAssert(my_mom != nil, @"managed object managedobjectmode failed");
my_psc = parent_ptr.persistentContainer.persistentStoreCoordinator;
NSAssert(my_psc != nil, @"persistent store coordinator failed");
my_moc = parent_ptr.persistentContainer.viewContext;
NSAssert(my_moc != nil, @"managed object context failed");
my_fetch = [[NSFetchRequest alloc] initWithEntityName: MYENTITY];
errorptr = nil; /* (unnecessary ... result not used) */
temp_ary = [my_moc executeFetchRequest: my_fetch error: &errorptr];
NSAssert(temp_ary != nil, @"initial fetch failed");
if ( temp_ary.count == 0 )
NSLog(@"no monster in sandbox");
else
{ NSLog(@"%d monster(s) in sandbox", (unsigned int)temp_ary.count);
[monstary addObjectsFromArray: temp_ary];
nbr_rows = temp_ary.count; /* set initial number of rows */
}
} // initcore
- (void)createOneWithName: (NSString *)new_name
/* create (and save) one entity */
{ NSManagedObject *one_monster;
NSEntityDescription *myentity;
NSError *errorptr;
BOOL myresult;
/* create one monster(entity) */
myentity = [NSEntityDescription entityForName: MYENTITY
inManagedObjectContext: my_moc];
one_monster = [[NSManagedObject alloc]
initWithEntity: myentity
insertIntoManagedObjectContext: my_moc];
NSAssert(one_monster != nil, @"managed object creation failed");
/* set attribute (monster name) */
[one_monster setValue: new_name forKey: THE_NAME];
/* add new monster in monster-array */
[monstary addObject: one_monster];
/* do not save new monster now */
/* save the whole context at 'applicationWillTerminate' */
// NSLog(@"start saving monster");
// errorptr = nil; /* (&errorptr = '\0') */
// myresult = [one_monster.managedObjectContext save: &errorptr];
// if ( ( myresult == NO ) || ( errorptr != nil ) )
// NSLog(@"monster '%@' save error", the_name);
// else /* (here errorptr == nil) */
// { NSLog(@"new monster with name '%@' saved", the_name);
// monstary = [my_moc executeFetchRequest: my_fetch error: &errorptr];
// NSAssert(monstary != nil, @"monster-array fetch failed");
// }
} // createOne
#pragma mark delegate and datasource
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{ return nbr_rows;
} // numberOfRows
- (id)tableView: (NSTableView *)aTableView
objectValueForTableColumn: (NSTableColumn *)aTableColumn
row: (NSInteger)rowIndex
{ NSManagedObject *one_monster;
NSString *mystring;
if ( monstary.count == 0 ) /* no need to display name */
mystring = @""; /* (never occurs?) */
else if ( rowIndex > (monstary.count-1) ) /* processing new row */
mystring = @"";
else
{ one_monster = monstary[rowIndex];
mystring = [one_monster valueForKey: THE_NAME];
}
return mystring;
} // objectValue
@end
ViewController.h
変更は少しだけです。
#import <Cocoa/Cocoa.h>
#import <CoreData/CoreData.h> // ← これを追加する
#import "AppDelegate.h" // ← これを追加する
@interface ViewController : NSViewController
<NSTableViewDelegate, NSTableViewDataSource> // ← これを追加する
/* -- public functions (called by 'AppDelegate') -- */
// AppDelegateから呼ばれるfunctionを3行追加
- (void)setParent_ptr:(AppDelegate *)myparent;
- (void)set_pc;
- (void)save_ary;
@end
AppDelegate.m
変更は次のとおりです。
#import "AppDelegate.h"
#import "ViewController.h" // ← これを追加する
extern ViewController *viewctrl_ptr; // ← これを追加する
@interface AppDelegate ()
- (IBAction)saveAction:(id)sender;
@end
@implementation AppDelegate
// この関数の内容を次のようにする
// この関数はViewDidLoadよりも後で呼ばれる
// だからviewctrl_ptrはすでにセットされている筈
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{ NSAssert(viewctrl_ptr != nil, @"ViewController not ready");
[viewctrl_ptr setParent_ptr: self];
[viewctrl_ptr set_pc];
// Insert code here to initialize your application
}
// この関数の内容を次のようにする
// この関数はメニューバーからアプリを終了させたときに呼ばれる
// Xcodeの四角い終了ボタンを押したときは呼ばれない
- (void)applicationWillTerminate:(NSNotification *)aNotification
{ [viewctrl_ptr save_ary];
// Insert code here to tear down your application
}
AppDelegate.hの変更はありません。
ストーリーボードの設定
Viewに部品TableViewをセット(ドラッグドロップ)します。TableViewのContent ModeをCell Based、列数を1にします。
TableViewにはTextCellと表示されていますが、ここにTextFieldCellをセットします。似たような部品が多いのでご注意を。
TableViewの横にプッシュボタンを2個セットし、タイトルをaddとremoveにします。
ここから3本、コネクションをやります。アシスタントエディタを開き、ViewController.mを表示させます。
TableViewからコントロール・ドラッグして、ViewController.mの@implementationのすぐ下のブレースの中にIBOutletを作り、名前をtbl_view_ptrにします。
addボタンからコントロール・ドラッグして、ViewController.mの中程にある(IBAction)add_pressedにコネクトします。同様にremoveボタンからコントロール・ドラッグして、(IBAction)remove_pressedにコネクトします。
なおdelegateとdatasourceはViewController.mで手当てしているので、処理は不要です。
完成
ビルドさせて実行し、addボタンを押すとモンスター名が追加され、removeボタンを押すと選択されたモンスターが削除されます。いつものようにデバッグウィンドウに作業内容を細かく表示させるようにしました。
addボタンを押したとき直ちにカーソルが表示されて入力待ちになるのは、Hillegassさん直伝の技法です(笑)。
ちょっと目障りなのは、すでに表示されているモンスター名をダブルクリックすると編集できてしまうのですが、編集結果は無視されるし、保存もされないので、ここではいちおう目をつぶることにしましょう。第2案では解決できるかもしれません。
試運転が終わったらメニューバーからアプリケーションを終了させて下さい。再び起動させると前回のモンスターが保存されているのがわかります。Xcodeのストップボタンでは保存されません。
第2案ではHillegassさんに従い、Bindingsに挑戦してみます。