iPhoneの電話帳のグループを扱う ABAddressBookRef


ABAddressBookRef

アドレス帳データの生成、保存、削除などを行う
複数の連絡先を格納している手帳みたいなもの


ABRecordRef


アドレス帳の中にレコード(ABRecord)がある
1件毎の各個人の電話帳みたいなもの

レコードには2種類
 - Person Records
  - 氏名、電話番号、Email、住所など
 - Group Records
  - 名前、IDぐらいしかない
    - Groupには複数のPersonRecordをひも付ける事ができる

※ただiPhone単体ではグループの作成はできないらしい

グループ関連の操作

アドレス帳のグループ取得

    ABAddressBookRef book = ABAddressBookCreate();
    CFArrayRef recodes = ABAddressBookCopyArrayOfAllGroups(book);
    for (int i = 0; i < CFArrayGetCount(recodes); i++) {
        ABRecordRef group = CFArrayGetValueAtIndex(recodes, i);
        ABRecordID recodeId = ABRecordGetRecordID(group);
        NSString* str = (NSString*) ABRecordCopyValue(group, kABGroupNameProperty);
        NSLog(@"recodeId:%d group name:%@", (int)recodeId, str);
        CFRelease(str);
    }
    CFRelease(recodes);
    CFRelease(book);

グループの作成

NSString* groupName = @"会社";
    ABAddressBookRef book = ABAddressBookCreate();
    ABRecordRef group = ABGroupCreate();
    ABRecordSetValue(group, kABGroupNameProperty,groupName, nil);
    ABAddressBookAddRecord(book, group, nil);
    ABAddressBookSave(book, nil);
    NSInteger groupId = ABRecordGetRecordID(group);
NSLog(@"groupId:%d", groupId);
    CFRelease(book);
    CFRelease(group);

グループの削除

NSInteger groupId = 1;
    ABAddressBookRef book = ABAddressBookCreate();
    // 削除
    ABRecordRef recode = ABAddressBookGetGroupWithRecordID(book, groupId);
    ABAddressBookRemoveRecord(book, recode, nil);
    ABAddressBookSave(book, nil);
   
    CFRelease(book);

余談ですが、Personの最終更新日は下記で取得可能です

最終更新日の取得
        CFDateRef cfCreateDate = ABRecordCopyValue(person, kABPersonCreationDateProperty);

iOS コピー&ペースト UIPasteboard と UIMenuControllerの拡張


切り取り(cat)やコピー(copy)した内容を置き換える


 クリップボード(iOSではペーストボードというらしい)に追加や削除したタイミングでnotificationが発行される
  - 追加 : UIPasteboardChangedNotification
  - 削除 : UIPasteboardChangedTypesRemovedKey

以下サンプル
-(void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
 
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pasteboardChanged:) name:UIPasteboardChangedNotification object:nil];
}
- (void)pasteboardChanged:(NSNotification*)notification
{
    NSLog(@"pasteboardChanged");
    NSDictionary* userInfo = [notification userInfo];
    NSArray* keys = [userInfo objectForKey:UIPasteboardChangedTypesAddedKey];
    if ([keys count] != 0) {
        UIPasteboard *board = [UIPasteboard generalPasteboard];
        [board setValue:@"上書きする文字" forPasteboardType:[keys objectAtIndex:0]];
    }
}

ポップアップをカスタマイズする UIMenuController


 ポップアップの表示は、UIMenuControllerを使う

  1. UIMenuControllerの設定
    UIMenuController* menuController = [UIMenuController sharedMenuController];
  2. 表示するアイテムを追加
    menu.menuItems = [NSArray ...];
  3. 表示メッセージを送る
    [menu setMenuVisible:YES animated:YES];
   
そして、注意しないと行けないところは、
 - 表示するviewがfirstResponderになっていること
 - ポップアップが表示できる領域が確保されていること(こだわりない場合はDefaultで)
   menuController.arrowDirection = UIMenuControllerArrowUP; // 注意

特に2番目の所ははまりました。。
 viewをCGRectMake(10,10, 50, 44)などの位置につくってしまうと、ポップアップが表示される領域がないので。

以下サンプル
@interface ViewEx : UIView
@end
@implementation ViewEx
- (BOOL)canBecomeFirstResponder{
    return YES;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UIMenuController* menuController = [UIMenuController sharedMenuController];
    [menuController setTargetRect:CGRectZero inView:self];
 
    NSMutableArray* menuItems = [NSMutableArray array];
 
    [menuItems addObject:
     [[[UIMenuItem alloc] initWithTitle:@"メニュー"
                                 action:@selector(menu:)] autorelease]];
    menuController.menuItems = menuItems;
    [menuController setMenuVisible:YES animated:YES];
}
- (void)menu:(id)sender
{
    NSLog(@"menu1: %@", sender);
}
@end

呼び出し側
    ViewEx* viewEx = [[ViewEx alloc] initWithFrame:CGRectMake(110, 100, 50, 44)];
    [viewEx setBackgroundColor:[UIColor greenColor]];
    [self.view addSubview:viewEx];
    [viewEx becomeFirstResponder];
ちなみに標準のコピー等を表示したい場合、下記のように対応するメソッドのみYESを返して上げる必要があります

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if (action == @selector(cat:)) {
  return YES;
}
  return NO;
}

UITableView(UITableViewCell)内でのメニュー表示

やる事は同じです
まずはUITableViewCellの継承クラスを作成

@interface CellEx : UITableViewCell
@end
@implementation CellEx
- (BOOL)canBecomeFirstResponder{
    return YES;}
@end
使う側
- (void)viewDidLoad{    [super viewDidLoad];// Do any additional setup after loading the view.
    UITableView* tableView = [[[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain] autorelease];    tableView.delegate = self;    tableView.dataSource = self;    [self.view addSubview:tableView];}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{    return 10;}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{    CellEx *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];    if (cell == nil) {        cell = [[[CellEx alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"] autorelease];    }    cell.textLabel.text = [NSString stringWithFormat:@"row:%d", indexPath.row];
    return cell;}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{    CellEx* cell = (CellEx*)[tableView cellForRowAtIndexPath:indexPath];    [cell becomeFirstResponder];
    UIMenuController* menuController = [UIMenuController sharedMenuController];    [menuController setTargetRect:CGRectZero inView:cell];    menuController.arrowDirection = UIMenuControllerArrowDefault;        NSMutableArray* menuItems = [NSMutableArray array];        [menuItems addObject:     [[[UIMenuItem alloc] initWithTitle:@"メニュー"                                 action:@selector(menu:)] autorelease]];    menuController.menuItems = menuItems;    [menuController setMenuVisible:YES animated:YES];    }- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {    if (action == @selector(menu:)) {  return YES;    }  return NO;}
- (void)menu:(id)sender{    NSLog(@"menu: %@", sender);}

実行するアクションの実装場所

上記のテーブルの例だと、CellExにcanPerformActionを実装しても動きます。
もちろん、menu:(id)senderも一緒に持って行きます。
どのような仕組みかは理解してませんがなんか気持ち悪いですね。
setTargetRnage:inViewで指定したViewのメソッドを呼んでると思ったんですが、違うみたいですね。

UITextFieldやUITextViewへのカスタムキーボード適用


前回のカスタムキーボードではまりかけたところ。

単純に各view(UITextField, UITextView)に文字列を追加するだけでは、
それぞれのdelegateが呼ばれません。

-(void)selectWord:(UIButton*)button{ _textField.text = [_textField.text stringByAppendingString:str];}

標準のキーボードと同じようにdelegateを考慮した作りをしたいので、調べてみました。

UITextInput

UITextFieldや、UITextViewはUITextInputを実装しています。
UITextInputはどのようなプロトコルかというと、
 - テキスト入力と相互作用するもの
 - 位置や、範囲などを管理

テキストの入出力の中心的なプロトコルを決めている
他には
 - UITextPosition, UITextRange
 - UITextInputTokenizer, UITextInputStringTokenizer
 - UITextInputDelegate
 - UIKeyInput
などなど

そして、そこに定義してある通りにUITextViewやUITextFieldは実装してあるわけで。。。
ここら辺は表示側と、入力側がうまくやりとりできるように定義しているみたい

textInRange : 選択中の文字列を取得
selectedTextRange : 選択中の位置を取得
textInputView : 実態への参照(UITextField, UITextViewなど)
inputDelegate : UITextInputDelegate


UIKeyInput Protocol

ここが本題かも。
文字列の入力や削除等はUIKeyInput Protocolで実装されているため、
今回だと、[_textField.text insertText]を呼び出せばOK

 - insertText 文字の入力
 - deleteBackward 削除
 - hasText 文字の存在確認


UITextInputDelegate Protocol

そして、テキスト入力などはtextFieldShouldBeginEditingや、textFieldDidBeginEditingなどがあり、
そのDelegateを正しく呼び出すために
 - textWillChangeやtextDidChangeを呼ぶ

iPhone開発 DCIntrospect ショートカット


iPhone開発に置いて、Interface Builderは絶対に使わないと心に決めたのはいいですが、やっぱり微調整するのはちょっとしんどいなーって思ってると、すっごく便利なものを発見!
「DCIntrospect」というツール
https://github.com/domesticcatsoftware/DCIntrospect
実際に落としてきてサンプル(DCIntrospectDemo)を実行するとわかりますが、
起動して表示された画面の座標やサイズを確認できたり、その場で修正できたりするものです。

使い方
AppDelegateの[self.window makeKeyAndVisible]の直後に以下を追加
#if TARGET_IPHONE_SIMULATOR
    [[DCIntrospect sharedIntrospector] start];
#endif
※もちろん #import "DCIntrospect.h" も忘れずに

シミュレータで起動したあと、スペースを押してモードを切り替える。
後はマウスでコンポーネントをクリックすると、情報が表示される。



これはマジで便利すぎる!!

ショートカット
  • スペース:切り替え
  • ? : ヘルプ
表示
  • o : アウトライン
  • O : ハイライト指定してないアイテム?
  • f : drawRectを再度呼び出す?
  • c : 座標
ログ
  • p : 詳細出力
  • a : アクセサープロパティ
  • v : descriptionを呼び出す
  • ` : デバック?
  • y : 親ビューに移動
  • t : 子ビューに戻る
移動、リサイズ
  • 左矢印 or 4 : 左へ1px 移動
  • 右矢印 or 6 : 右へ1px 移動
  • 上矢印 or 8 : 上へ1px 移動
  • 下矢印 or 2 : 下へ1px 移動
  • alt + (左矢印 or 7) : 1px サイズ幅を広げる
  • alt + (右矢印 or 9) : 1px サイズ幅を狭める
  • alt + (上矢印 or 1) : 1px 高さを広げる
  • alt + (下矢印 or 3) : 1px 高さを狭める
表示系
  • - : 輝度を下げる
  • + : 輝度を上げる
  • 0 : 編集内容を出力
再描画
  • d : 再描画(setNeedsDisplay)?
  • l : 再描画(setNeedsLayout)?
  • r : 再描画?

ssh + agent forward + screen の設定

sshで接続元にある秘密鍵を使う場合、agent fowardすればOK
ssh -A [ホスト1]

すると、ホスト1からホスト2へsshする場合、ローカルの秘密鍵を使ってくれる。

ただし、ホスト1でscreenを起動している場合、うまくホスト2へ秘密鍵が受け継がれない。

agent fowardした場合、環境変数にSSH_*に接続元情報が格納されている。
screenでは、起動時の環境変数を保持しているため、デタッチした場合、初期の環境変数を保持しているので、うまく秘密鍵が取得できない

> env | grep SSH
SSH_CLIENT=XXX.XXX.XX.XXX XXX XX
SSH_TTY=/dev/pts/0
SSH_AUTH_SOCK=/tmp/ssh-xxxxxxxxxx/agent.xxxxxxxx
SSH_CONNECTION=XXXX.XXX.XXX.XXX XXX.XXX.XXX 


そこでちょっと強引ですが、ssh でログイン時に上記環境変数をファイルに保存。
デタッチ後のscreenプロセスでそのファイルを使って、最新の状態に変更するようにしてみました。


bashrcに以下を追加

SSHV="SSH_CLIENT SSH_TTY SSH_AUTH_SOCK SSH_CONNECTION DISPLAY"
for x in ${SSHV} ; do
    (eval echo $x=\$$x) | sed  's/=/="/
                                s/$/"/
                                s/^/export /'
done 1>$HOME/bin/fixssh

screen を起動してsource ~/bin/fixsshすればOK


iPhone カスタムキーボードを作る

iPhone開発に置いて、キーボード入力をデフォルトのキーボードではなく、個別に用意したものと切り替え可能にする

1.UITextView, UITextFieldのinputViewに置き換えたいviewを設定

UITextField* textField = [[UITextField alloc] initWithFrame:CGRectMake(10, 10, bounds.size.width - 20, 30)];
    textField.borderStyle = UITextBorderStyleRoundedRect;
    CustomKeyboard* keyboard = [[CustomKeyboard alloc] initWithFrame:CGRectMake(0, 0, bounds.size.width, 200)];
    textField.inputView = keyboard;

2.キーボードが表示されている場合に切り替える場合はreloadInputViewsメッセージを送る

textView.inputView = nil; // デフォルトに戻す場合
[textView reloadInputViews]

以下サンプルコード
https://github.com/hrk-ys/ios-custom-inputview

iPhone開発の多言語化

ちょっとはまったのでメモ的な意味で投稿

Xcode4でプロジェクトを新規作成すると、「InfoPlist.strings」 が作成されます。

自分はこのファイルに多言語の文字列を設定できます。
主にシステム的な設定ですね。

例:ja.lproj/InfoPlist.strings
CFBundleName = "テストアプリケーション";
CFBundleDisplayName = "テストアプリ";
 例:en.lproj/InfoPlist.strings
CFBundleName = "Test Application";
CFBundleDisplayName = "Test App";


ただ、アプリから参照する文字列は上記のファイルでは定義できず、
ファイルを作成して「Localized.string」を定義しないと使えないのです。
(ここが結構はまりました)
こちらはアプリ内から参照できる文字列
NSLocalizedString(@"msg_get", @"message get");

例 : 日本語
"msg_get" = "メッセージ取得";
例:英語
"msg_get" = "Get Message";

Core Dataについて

CoreDateとは

  • Modelオブジェクトをファイルに保存したり、ファイルから復元したりする
  • アーカイブよりも多機能
    • Modelオブジェクトの変更履歴(アンドゥとレドゥ、オブジェクト間の相互関係の管理)
    • Modelオブジェクトのサブセットだけをメモリ内の保持する事ができる
    • GUIベースのエディタでModelクラスを定義する事ができる
    • データストアのバージョン管理および移行のためのインフラストラクチャ

登場人物

  • 管理オブジェクト(Managed Object Model)
    • NSManagedObjectまたはNSManagedObjectのサブクラス
    • 概念的にはデータベースのテーブル内のレコードオブジェクト
    • MVCのModelオブジェクト
  • 管理オブジェクトコンテキスト(Managed Ojbject Context)
    • NSManagedObjectContextのインスタンス
    • 管理オブジェクトのコレクションを管理
    • Modelオブジェクトのグループを形成
    • ライフサイクル管理、妥当性検証、関係の管理、アンドゥ/リドゥまで管理
  • 管理オブジェクトモデル
    • NSManagedObjectModelのインスタンス
    • データベースを定義するスキーマオブジェクト
    • モデルはエンティティ記述オブジェクトのコレクション
  • 永続ストアコーディネータ
    • 永続ストアコーディネータはNSPersistentStoreCoordinatorのインスタンス
    • 永続オブジェクトストア(外部ストア:ファイルなどを指す)のコレクションを管理
    • アプリケーション内のオブジェクトとデータベース内のレコードをマッピングする

サンプル

AppleのDeveloper Libraryにあるドキュメントをもとに使ってみる

プロジェクトの作成

プロジェクトの作成の作成じに、Core Dataを使用するにチェックを入れる (Use Core Data for storage)
  • アプリケーションデリゲートクラス
    • プロパティーが自動生成される
// 管理オブジェクトコンテキスト
@property (nonatomic, retain, readonly) NSManagedObjectContext
*managedObjectContext;
// 管理オブジェクトモデル
@property (nonatomic, retain, readonly) NSManagedObjectModel *managedObjectModel;
// 永続ストアコーディネータ
@property (nonatomic, retain, readonly) NSPersistentStoreCoordinator
*persistentStoreCoordinator;
- (NSURL *)applicationDocumentsDirectory;
- (void)saveContext;
  • Core Dataのモデル(.xcdatamodeld)ファイル(通常、管理オブジェクトモデルと呼ばれる)

データのモデリング

xcodeで[プロジェクト名].xcdatamodelを選択後、Entityを追加、そのEntityに対してAttributeを追加していく

カスタム管理クラスオブジェクト

通常のクラスファイルを作成する手順で「Managed Object Class」を選択
Entityが選択できるので、先ほど作成したEntityを選択する
プロパティはsynthesizedではなく、dynamicとなる。コンパイル時ではなく、実行時にアクセサーを生成する
実装ファイル(Event.m)にはdeallocがない。オブジェクトのライフサイクルはCoreDataが行うため

管理クラスオブジェクトの生成/保存

通常は、NSEntityDescriptionの簡易メソッド(insertNewObjectForEntityForName:inManagedObjectContext:)を使用して管理オブジェクトを作成
指定したエンティティーに対応するクラスを初期化し、コンテキストに挿入する
オブジェクトを追加したり変更したりすると、その変更はsave:が呼び出されるまではメモリ内に保持される

// Eventエンティティの新規インスタンスを作成して設定する
Event *event = (Event *)[NSEntityDescription insertNewObjectForEntityForName:@"Event" inManagedObjectContext:managedObjectContext];

// ここでeventに対してデータを設定

// データの保存
NSError *error = nil;
if (![managedObjectContext save:&error]) {
    // エラーを処理する
}

管理オブジェクトのフェッチ

フェッチ要求では、最低限、関心のあるエンティティを指定
オブジェクトが持つ値に関する制限を指定したり、オブジェクトを返す順番を指定したりすることも可能

NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event"
inManagedObjectContext:managedObjectContext];
[request setEntity:entity];

// ソート
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]
initWithKey:@"creationDate" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor,
nil];
[request setSortDescriptors:sortDescriptors];
[sortDescriptors release];
[sortDescriptor release];

// 実行
NSError *error = nil;
NSMutableArray *mutableFetchResults = [[managedObjectContext
executeFetchRequest:request error:&error] mutableCopy];
if (mutableFetchResults == nil) {
    // エラーを処理する
}

[self setEventsArray:mutableFetchResults];
[mutableFetchResults release];
[request release];

管理オブジェクトの削除

レコードを削除するには、NSManagedObjectContextのdeleteObject:メソッドを使用する
登録同様にseveメソッドを読んで、反映させる

// 指定のインデックスパスにある管理オブジェクトを削除する。
NSManagedObject *eventToDelete = [eventsArray
objectAtIndex:indexPath.row];
[managedObjectContext deleteObject:eventToDelete];

// 変更をコミットする。
NSError *error = nil;
if (![managedObjectContext save:&error]) {
 
はじめてのCoreDataではまりどころ
"The operation couldn’t be completed. (Cocoa error 134100.)"
スキーマを変更する場合、マイグレーション処理をしましょうってことらしい。
アプリを削除して再度実行すればなおる

ReactNativeでAndroid対応する話

前提 ReactNativeでiOS版のアプリをリリースしていて、Android版をリリースする話 トラブルシューティング Build.VERSION_CODES.Q が存在しないエラー compileSdkVersionを29以上にすると解決 メモリー足りないエラー Execu...