ObjecTips

Swift & Objective-C で iOS とか macOS とか

カメラで撮影した写真と動画を Photos.framework でライブラリに保存する

撮影は UIImagePickerController にお任せして撮影後のデータを Photos で保存する実装方法。

まず UIImagePickerControllersourceType でカメラ撮影を指定。
デフォルトのままだと写真撮影しかできないので、カメラ撮影で利用可能な形式を全て設定して動画撮影もできる様に mediaTypes を設定する。
これで写真と動画の撮影はOK

    UIImagePickerControllerSourceType sourceType = UIImagePickerControllerSourceTypeCamera;
    UIImagePickerController *picker = [[UIImagePickerController alloc] init];
    picker.sourceType = sourceType;
    picker.mediaTypes = [UIImagePickerController availableMediaTypesForSourceType:sourceType];
    picker.delegate = self;
    [self presentViewController:picker animated:YES completion:nil];

撮影したデータの取得は UIImagePickerControllerDelegate のデリゲートメソッドで行う

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info

写真撮影時は info に UIImagePickerControllerOriginalImage キーで UIImage で画像データが入ってきて、動画撮影時には UIImagePickerControllerMediaURLNSURL で動画ファイルのURLが入ってくるという違いがあるので撮影方法によって条件分岐して処理する必要がある。
どちらで撮影されたかは info から UIImagePickerControllerMediaType キーで取得して判定できる。

    NSString *mediaType = info[UIImagePickerControllerMediaType];
    if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) {
    }
    else if ([mediaType isEqualToString:(NSString *)kUTTypeMovie]) {
    }

なおここの kUTTypeImagekUTTypeMovie のキーを使うために MobileCoreServices.framework も import しておく必要がある。
分岐の後、画像であれば PHAssetChangeRequest

+ (instancetype)creationRequestForAssetFromImage:(UIImage *)image;

動画ファイルのURLであれば

+ (nullable instancetype)creationRequestForAssetFromVideoAtFileURL:(NSURL *)fileURL;

これらのメソッドを使ってライブラリへのデータ保存を行う。

PHAssetChangeRequest は若干変わった利用方法になっていて、以下のように PHPhotoLibrary の変更を行うブロック内でこのインスタンスを作成すると PHPhotoLibrary が勝手に保存処理を行ってくれる。
目に見えて解りやすい save, write などの直接的なメソッド呼び出しでの保存ではないのでちょっと注意。

    PHPhotoLibrary *library = [PHPhotoLibrary sharedPhotoLibrary];
    [library performChanges:^{
            [PHAssetChangeRequest creationRequestForAssetFromImage:image];
    } completionHandler:^(BOOL success, NSError * _Nullable error) {
    }];

コード全体は以下のようになる

Xcode プロジェクトの watchOS 1 から watchOS 2 への移行

watchOS 1 から watchOS 2 への移行は、Xcode のプロジェクトを選択して
Editor > Validate Settings...
を選択して Xcode にお任せてプロジェクト更新するのが良い。
でもなぜかこの時に watchOS の移行についての項目が出てこなくてプロジェクトがうまくアップデートされない事があった。
その際 Xcode プロジェクト hoge.xcodeproj のパッケージを開いて中の project.pbxproj ファイルを直接エディタで開き、
以下のように記載されているところを

productType = "com.apple.product-type.application.watchapp";
productType = "com.apple.product-type.watchkit-extension";

それぞれ以下のように変更したら Xcode がちゃんと watchOS 2 のターゲットだと認識してうまくビルドできるようになった。
productType = "com.apple.product-type.application.watchapp2";
productType = "com.apple.product-type.watchkit2-extension";


あまり無いケースだと思うけど、もし同様の問題が起きたら上記の様な事はしないで一旦 git でプロジェクトを元の状態に戻してキャッシュを消すなり Xcode を起動し直すなりして正しく Validate Settings が完了する様リトライした方が良い(と後から思った)。
参考程度に。

自作の iOS Framework を watchOS に対応させる

自作Farmework内で watchOS で利用できないAPIを使っていると not available on watchOS のエラーが出て watchOS 向けにビルドできない。
Apple純正の Framework と同じようにヘッダのメソッド定義に __WATCHOS_PROHIBITED を付けて watchOS では使えません宣言をすると ifdef を使ったりせずメソッドをそのままビルド可能になる。

ビルドエラー
// .h
- (void)test:(EKEventStore *)eventStore;

// .m
- (void)test:(EKEventStore *)eventStore
{
    [eventStore commit:nil]; // 'commit:' is unavailable: not available on watchOS
}
OK
// .h
- (void)test:(EKEventStore *)eventStore __WATCHOS_PROHIBITED;

// .m
- (void)test:(EKEventStore *)eventStore
{
    [eventStore commit:nil];
}

クラス自体を watchOS で使えないように宣言する場合はクラス宣言の @interface の前にこれを付けると良い

__WATCHOS_PROHIBITED @interface MyClass : NSObject

NSString の真偽値の判定

NSString で表される文字列の真偽値を判定するには boolValue メソッドが使える。

@property (readonly) BOOL boolValue NS_AVAILABLE(10_5, 2_0);

数字、YES/NO、true/false などの文字列を boolValue メソッドで判定できる

    NSLog(@"%d", @"1".boolValue); // 1
    NSLog(@"%d", @"0".boolValue); // 0
    NSLog(@"%d", @"YES".boolValue); // 1
    NSLog(@"%d", @"NO".boolValue); // 0
    NSLog(@"%d", @"true".boolValue); // 1
    NSLog(@"%d", @"false".boolValue); // 0

ドキュメントによれば最初の文字が Y, y, T, t または1~9の数字だとYESを返すとの事。
よって以下は全てYES

    NSLog(@"%d", @"Y".boolValue); // 1
    NSLog(@"%d", @"y".boolValue); // 1
    NSLog(@"%d", @"T".boolValue); // 1
    NSLog(@"%d", @"t".boolValue); // 1
    NSLog(@"%d", @"1".boolValue); // 1
    NSLog(@"%d", @"9".boolValue); // 1

以下のような文字列も最初の1文字で評価されるので全てYES

    NSLog(@"%d", @"Yabc".boolValue); // 1
    NSLog(@"%d", @"yNO".boolValue); // 1
    NSLog(@"%d", @"tfalse".boolValue); // 1

また先頭文字列のスペースは無視される、
文字列が数字の場合、先頭文字列の0の連続は無視される、0の連続の前に置かれる+-の記号は無視されるという仕様があるので以下のケースも全てYESになる。

    // スペース無視
    NSLog(@"%d", @"  y".boolValue); // 1
    // 先頭の0の連続は無視
    NSLog(@"%d", @"001".boolValue); // 1
    // 数字の前の+と-は無視
    NSLog(@"%d", @"+1".boolValue); // 1
    NSLog(@"%d", @"-1".boolValue); // 1
    // 数字の前の+と-と0の連続は無視
    NSLog(@"%d", @"+01".boolValue); // 1
    NSLog(@"%d", @"-001".boolValue); // 1

以下のケースは全てNOになる。

    // 先頭文字列がスペースではない
    NSLog(@"%d", @"- 1".boolValue); // 0
    NSLog(@"%d", @"- t".boolValue); // 0
    // 文字列が数字ではないので0の連続は無視されない
    NSLog(@"%d", @"00y".boolValue); // 0
    NSLog(@"%d", @"00t".boolValue); // 0
    // 数字の文字列の前の+と-の記号を無視するのは1つまで
    NSLog(@"%d", @"++001".boolValue); // 0
    NSLog(@"%d", @"++1".boolValue); // 0

Touch IDのエラーまとめ

LAContext の以下のAPI

- (void)evaluatePolicy:(LAPolicy)policy
       localizedReason:(NSString *)localizedReason
                 reply:(void(^)(BOOL success, NSError * __nullable error))reply;

このAPI指紋認証LAPolicyDeviceOwnerAuthenticationWithBiometrics を使ってみて実際に遭遇したエラー集
LocalAuthentication.framework の LAError.h に書かれているエラーの定義も合わせて確認すると良いかも。


Error Domain=com.apple.LocalAuthentication Code=-1 "Application retry limit exceeded." UserInfo={NSLocalizedDescription=Application retry limit exceeded.

Touch IDの認証失敗を繰り返して制限回数に達した時


LAPolicyDeviceOwnerAuthentication でパスコード認証中にキャンセル Error Domain=com.apple.LocalAuthentication Code=-2 "Canceled by user." UserInfo={NSLocalizedDescription=Canceled by user.}

Touch IDのシステムアラートを表示中にホームボタンを押す
またはTouch IDのシステムアラートの「キャンセル」ボタンを押した時


Error Domain=com.apple.LocalAuthentication Code=-3 "Fallback authentication mechanism selected." UserInfo={NSLocalizedDescription=Fallback authentication mechanism selected.}

Touch IDのシステムアラートを表示中に「パスコードを入力」ボタンを押した時


Error Domain=com.apple.LocalAuthentication Code=-4 "Canceled by another authentication." UserInfo={NSLocalizedDescription=Canceled by another authentication.}

Touch IDのシステムアラートを表示中に再度APIでTouch IDを表示しようとした時


Error Domain=com.apple.LocalAuthentication Code=-4 "Caller moved to background." UserInfo={NSLocalizedDescription=Caller moved to background.}

Touch IDのシステムアラートをAPIで表示するのと同時にホームボタンを押してホーム画面を表示した時


Error Domain=com.apple.LocalAuthentication Code=-4 "UI canceled by system." UserInfo={NSLocalizedDescription=UI canceled by system.}

Touch IDのシステムアラートを表示中に端末をロックした時


Error Domain=com.apple.LocalAuthentication Code=-8 "Biometry is locked out." UserInfo={NSLocalizedDescription=Biometry is locked out.}

Touch IDの認証制限回数に達した状態でAPIからTouch IDを呼んだ時


Error Domain=com.apple.LocalAuthentication Code=-1004 "User interaction is required." UserInfo={NSLocalizedDescription=User interaction is required.}

バックグラウンドにいる時にTouch IDのAPIを呼んだ時

これは - (void)applicationDidEnterBackground:(UIApplication *)application のタイミングでTouch IDのAPIを呼ぶとたまに起こる。

このエラーを避けるにはAPIの呼び出しは - (void)applicationDidBecomeActive:(UIApplication *)application で行いつつ、2重にAPIコールをしてしまわないようにAPIの呼び出し時に使った LAContextインスタンスを reply ブロックが呼ばれるまで保持したりして状態管理すると良い。

ターミナルでUUIDを生成する

iOS, macOS でプログラム内でUUIDを使う場合 CFUUIDNSUUID が使える。

プログラム外で識別子としてUUIDが欲しい時は uuidgen というコマンドが用意されているのでこれを使う。
ターミナルで

$uuidgen

結果は以下のように生成されたUUIDが表示される

14B78297-8560-4144-AFA4-41563A4BE71B

-hdr オプションを使うと CFUUID を使ったヘッダ定義用のスニペットが生成される。

$uuidgen -hdr

hdr は header の略らしい。

結果は以下

// 14B78297-8560-4144-AFA4-41563A4BE71B
#warning Change the macro name MYUUID below to something useful!
#define MYUUID CFUUIDGetConstantUUIDWithBytes(kCFAllocatorSystemDefault, 0x14, 0xB7, 0x82, 0x97, 0x85, 0x60, 0x41, 0x44, 0xAF, 0xA4, 0x41, 0x56, 0x3A, 0x4B, 0xE7, 0x1B)

このままコピペしてソースコード内で使用できるように #define が記載されている。

Google Firebase で既存の AdMob アカウントを使う

新たに作成したFirebaseアカウント(GoogleアカウントA)で今まで使っていた既存のAdMobアカウント(別のGoogleアカウントB)を引き続き使いたいというパターンの手順。
*1 *2

  • GoogleアカウントAでFirebaseにログイン
  • 新規プロジェクトを作成
  • 歯車アイコンから「権限」を選択
  • IAMを選択してプロジェクトを選択し「メンバーを追加」からGoogleアカウントBのメールアドレスと権限を入力してメンバーの追加を実行*3
  • GoogleアカウントBに招待メールが届くので承認
  • 承認するとGoogleアカウントBのFirebaseのダッシュボードが開いてGoogleアカウントAで作成したプロジェクトが表示される
  • GoogleアカウントBでAdMobにログインして「分析」タブのアプリ一覧から「Firebase にリンク」を選択
  • iOSの場合バンドルIDを入力して、そのバンドルIDがFirebaseプロジェクトで設定したものと一致していれば「既存の Firebase プロジェクトと既存の Firebase アプリにリンクする」という選択肢にデフォルトでチェックが付くので「続行」を選択

これでリンク完了
GoogleアカウントAとBどちらのFirebaseのダッシュボードでもプロジェクトのEARN AdMobを表示すると「リンク済み」となっている
あとはFirebaseのAdMob, Analytics, その他を使ってアプリへの実装を行う


権限設定やアカウントリンク機能をちゃんと使えばプロジェクト毎に異なるAdMobアカウントを使う事もできる他、プロジェクト毎にログを閲覧できる人、設定を編集できる人、ストレージにファイルをアップできる人などいろいろと制御できそう。
自分がプロジェクトに招待される側としても、複数の参加プロジェクトを1つのGoogleアカウントとダッシュボードで表示できるので便利そう。

*1:社内、社外の権限とか管理の都合でFirebaseとAdMobのアカウントが異なるケースの話。AdMobとFirebaseのGoogleアカウントが同じでいい場合はそのアカウントでそのままFirebaseに登録すればいいはず。この手順は不要

*2:Google Apps環境化で既存のAdMobアカウントとFirebaseに未登録のアカウント2つを使って手順を確認した

*3:権限はとりあえずプロジェクトオーナーにしてみた