ObjecTips

基本Objective-Cで iOS とか OS X とか

DebugLog を応用した DebugBlock

DebugLog

良く見かける技で、デバッグビルド時のみ出力されるログを以下の様に定義出来る。

#if DEBUG
#define DebugLog(...) NSLog(__VA_ARGS__)
#else
#define DebugLog(...)
#endif

Before

#if DEBUG
    NSLog(@"debug message");
#endif

After

    DebugLog(@"debug message");
DebugBlock

応用して Block を扱ってみる。

#if DEBUG
#define DebugBlock(block) block()
#else
#define DebugBlock(block)
#endif

Before

#if DEBUG
    view.layer.borderColor = [UIColor redColor].CGColor;
    view.layer.borderWidth = 1;
#endif

After

    DebugBlock(^{
        view.layer.borderColor = [UIColor redColor].CGColor;
        view.layer.borderWidth = 1;
    });

これでデバッグビルド時のみ実行される実装をブロック内に記述する事が出来る。
上記ではデバッグビルド時のみビューの位置と大きさを確認出来る様に枠線を表示している。


koze.hatenablog.jp

一応上記の方法で retain count を調べてみたところ、デバッグビルド時のみ変数のキャプチャが行われリリースビルド時はキャプチャが行われなかった。
Xcode で記述する際にブロック構文のコード補完が行われないのが惜しいけど、まぁまぁ使えそう。

画像のExifデータの取得方法と取得データの比較 iOS

検証環境
  • Xcode 9.2, iOS 11.2 Simulator
  • iOS 11.1 iPhone X の背面カメラで撮影したものを AirDrop で Mac に送ってシミュレータ経由で iCloud Photo Library に取り込んだ写真を用いてデータを確認

Exifの取得

まず取得方法をざっくり
Exif等のデータをプロパティとして Core ImageImage I/O で取得可能

CoreImage
    CIImage *image = [CIImage imageWithContentsOfURL:URL];
    NSDictionary<NSString *,id> *properties = image.properties;
    NSLog(@"%@", properties);
Image I/O + CGImageSourceCopyPropertiesAtIndex
    CGImageSourceRef src = CGImageSourceCreateWithURL((__bridge CFURLRef)URL, nil);
    NSDictionary *properties = CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(src, 0, nil));
    NSLog(@"%@", properties);
    CFRelease(src);


Image I/O でメタデータとしても取得可能

Image I/O + CGImageSourceCopyMetadataAtIndex
    CGImageSourceRef src = CGImageSourceCreateWithURL((__bridge CFURLRef)URL, nil);
    CGImageMetadataRef metadataRef = CGImageSourceCopyMetadataAtIndex(src, 0, nil);
    NSArray *tags = CFBridgingRelease(CGImageMetadataCopyTags(metadataRef));
    for (id tag in tags) {
        CGImageMetadataTagRef tagRef = (__bridge CGImageMetadataTagRef)tag;
        CFShow(tagRef);
    }
    CFRelease(src);
    CFRelease(metadataRef);
結果1

CoreImageImage I/O + CGImageSourceCopyPropertiesAtIndex で取得出来る内容は同じ。
ただし後者の CGImageSourceCopyPropertiesAtIndex の場合は1つのファイルに複数の画像が含まれる画像形式に対応しているので、仮に複数画像が存在して且つ画像毎に取得出来るプロパティが異なっている場合 CIImage から取得出来るプロパティとどう違いがあるかは検証が必要。(おそらくindex 0の1枚目の画像のプロパティを返すんじゃないかと予想)

以下が取得出来るプロパティのサンプル

Exif data with iOS 11 iPhone X back camera

結果2

Image I/O + CGImageSourceCopyMetadataAtIndex で取得出来るものは他のものと内容や形式が異なっている。
ちなみに NSArray のままログ出力すると CFBasicHash あたりの表示が見辛くなってしまうのでサンプルではfor文で回して CFShow でタグを1つずつログ出力した。

以下が取得出来るメタデータのサンプル

Metadata with iOS 11 iPhone X back camera

取得データの比較

結果1と結果2を見比べると、ざっと見て結果2の方が数が少なそうに見える。比較して確認してみる。
メタデータのタグに対応するプロパティのキーやデータを探して、対応するものをCSVの別カラムに記載した。(MakerApple のキー2は512バイトのデータなので記載を省略)

Compare Exif and metadata tag.

補足

  • exifEX:PhotographicSensitivity

exifEX:PhotographicSensitivity はプロパティを見ると対応するものが無いように見える。
ググって見つけたCIPA(一般社団法人カメラ映像機器工業会)に掲載のPDFによると

http://www.cipa.jp/std/documents/e/DC-010-2012_E.pdf

PhotographicSensitivity は Exif 2.3以降の規格で、Exif 2.21 まで使用されていた ISOSpeedRatings を代替するものらしい。ISOSpeedRatings は deprecated との事。
テストに利用した写真の ExifVersion を見てみると 0221 とあるので、この写真では PhotographicSensitivity は使用されておらず ISOSpeedRatings に値が入っていて、タグ取得のAPIではマッピングによりこれを参照しているという事らしい。


  • iio:hasXMP

iio:hasXMP は正確な仕様は不明。
iio は ImageIO の prefix でApple独自のメタデータタグだと思われる。(ImageIOBase.h を見ると IIO_HAS_IOSURFACEIIO_BRIDGED_TYPE といった定義が見られる。)
名前の通りxmp のタグが存在しているかどうかを示すものだと推測。


  • xmp:CreateDate
  • xmp:ModifyDate
  • photoshop:DateCreated

先のCIPAのPDFには以下のように掲載されていたので、xmp:CreateDatexmp:ModifyDate はそれをそのまま適用。

DateTimeDigitized = xmp:CreateDate
DateTimeOriginal = exif:DateTimeOriginal
DateTime = xmp:ModifyDate

またメタデータタグには exif:DateTimeOriginal が存在しておらず、またプロパティには photoshop:DateCreated のタグに当てはまるものは無かった。
このタグの扱いをどうしたものかと思っていると CGImageMetadata.h のコメントに以下の記載を発見

  • Metadata Working Group guidance is factored into the mapping of CGImageProperties to
  • XMP compatible CGImageMetadataTags.
  • For example, kCGImagePropertyExifDateTimeOriginal will get the value of the
  • corresponding XMP tag, which is photoshop:DateCreated.

という事で、マッピングされているらしい。
よって DateTimeOriginal = photoshop:DateCreated として適用。

結果より

メタデータタグ CGImageMetadataRef を使うと exif:Flash のビットや exif:ExifVersion の配列を利用しやすい形式に変換してくれたり、マッピングにより Exif のバージョンの違いを吸収をしてくれるという利点がある。
また GPSLatitude を例にとるとメタデータタグに表記されている 34,59.1905 の度、分、秒が大元の記録データであり、プロパティの 34.98650833333333 はこれを計算した結果になる。
よって正確なrawデータを表示する場合にはメタデータタグを利用した方が良さそう。
ただし ColorModelProfileName だったりプライベート仕様の MakerApple の情報も余す事なく表示したい場合はプロパティを使用する必要がある。
用途に合わせてうまく併用するのが良いかも知れない。

システムと同じ並びに日本語の文字列をソートする

NSString の比較メソッド一覧

- (NSComparisonResult)compare:(NSString *)string;
- (NSComparisonResult)compare:(NSString *)string options:(NSStringCompareOptions)mask;
- (NSComparisonResult)compare:(NSString *)string options:(NSStringCompareOptions)mask range:(NSRange)rangeOfReceiverToCompare;
- (NSComparisonResult)compare:(NSString *)string options:(NSStringCompareOptions)mask range:(NSRange)rangeOfReceiverToCompare locale:(nullable id)locale;

- (NSComparisonResult)caseInsensitiveCompare:(NSString *)string;
- (NSComparisonResult)localizedCompare:(NSString *)string;
- (NSComparisonResult)localizedCaseInsensitiveCompare:(NSString *)string;

- (NSComparisonResult)localizedStandardCompare:(NSString *)string API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));

例として iOS の読み上げ言語の設定画面(設定>アクセシビリティ>スピーチ>声)

f:id:Koze:20180119121548j:plain

テストコード

String sort similar to iOS.

カタカナの部分に関してはいずれも差異は無いが、漢字のソート結果が違っている。
localizedCompare:localizedStandardCompare: は漢字の部分もOSと並びが合っている。
localizedStandardCompare: のドキュメントを見ると

This method should be used whenever file names or other strings are presented in lists and tables where Finder-like sorting is appropriate.

Finder-like との事なので iOS, macOS のシステムの表示に合わせるには localizedStandardCompare: を使うのが良さそう。

UIActivityViewController からメールが呼び出された時の statusBarStyle の調整

UIActivityViewController からメールが呼びだされた時に statusBarStyle が常に UIStatusBarStyleDefault で黒文字になってしまう。通常は問題無いがアプリのデザインでナビゲーションバーやボタン類に色を付けている場合、ステータスバーの文字色とマッチしない事がある。

f:id:Koze:20171119095954p:plain

Stack Overflow で検索してみると

  • Info.plist で UIViewControllerBasedStatusBarAppearanceNO にして UIStatusBarStyleUIStatusBarStyleLightContent に設定する
  • ViewController の表示や非表示の良きタイミングで以下のコードで設定
    [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent animated:YES];

という話が出てくる。

ところが iOS 11 で試してみたところ Info.plist での設定がうまく効かず(以前はこれでいけた様な?)またコードでの設定は iOS 9 で deprecated になっており今後完全に remove になる可能性もあるので他の方法も探してみる。

メールが起動された時の画面を Debug View Hierarchy で確認してみると MFMailComposeViewController と他2つの Private な MFMailComposeInternalViewController MFMailComposeRemoteViewController クラスが使われている事が分かった。

f:id:Koze:20171119094325p:plain

MFMailComposeViewController は公開クラスなので、カテゴリで既存メソッドをオーバーライドすれば挙動を変える事が出来る(ただしデフォルト挙動の書き換えはリスクを鑑みて変更は最小限にしておいた方が良い)。
実装は以下

Change the status bar to white when MFMailComposeV ...

UIViewControllerBasedStatusBarAppearance = YES の時

以下の2つのメソッドが呼ばれる。

- (UIStatusBarStyle)preferredStatusBarStyle;
- (UIViewController *)childViewControllerForStatusBarStyle;

MFMailComposeViewController のモーダルアニメーションと同時にステータスバーのスタイルが変更される。
このケースでは viewDidAppear: はオーバーライドしなくてもOK。

UIViewControllerBasedStatusBarAppearance = NO の時

以下のメソッドが呼ばれる。

- (void)viewDidAppear:(BOOL)animated;

先の2つのメソッドは呼ばれない。
MFMailComposeViewController のモーダルアニメーションの完了のタイミングの viewDidAppear: でステータスバーのスタイルが変更される。
このケースでは viewDidAppear: のみをオーバーライドすればOK。 *1

これで以下の様にメール画面でのステータスバーのスタイルを UIStatusBarStyleLightContent に変更する事が出来る。

f:id:Koze:20171119100025p:plain

*1:[super viewDidAppear:animated] は MFMailComposeViewController ではなくてその親クラスの UIViewController の実装を呼び出している。なくても良い。

iOS 11 での UIImagePickerController の変更点

iOS 10 までは UIImagePickerControllerUIImagePickerControllerSourceTypePhotoLibrary の組み合わせでフォトライブラリを表示する際にアクセス権限の確認を自動で行ってくれていた。
アクセス権限がなければ鍵マークが表示され一切の情報を取得する事ができない。ユーザの操作としては必ずキャンセルが行われる。*1

iOS 10

自動でアクセス許可のアラートが表示される
f:id:Koze:20170920044429p:plain

権限なしの状態ではキャンセルしか出来ない
f:id:Koze:20170920044433p:plain

権限があればフォトライブラリを表示、写真を選択可能
f:id:Koze:20170920044437p:plain

iOS 11 ではアクセス権限の確認が自動で行われない様になった。そして権限がなくともフォトライブラリの表示は行われ、写真の選択も出来てしまう。

iOS 11

権限の確認無しにいきなりフォトライブラリが表示
f:id:Koze:20170920044959p:plain

何が起こるか

写真の選択を行うと通常通り delegate メソッドが呼ばれる。

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

この時 info には
UIImagePickerControllerMediaType
UIImagePickerControllerOriginalImage
UIImagePickerControllerReferenceURL
UIImagePickerControllerImageURL
といった情報が入っている。

UIImagePickerControllerReferenceURL には ALAsset のURLが入っている。
iOS 11 で deprecated になったがまだ使う事は出来る。

ex.)
assets-library://asset/asset.HEIC?id=97F34D03-1230-4F4E-9AA6-E7A7469158DE&ext=HEIC

フォトライブラリへのアクセスが許可されていれば以下の様にしてアセットへアクセスする事が出来る。

    NSURL *assetURL = info[UIImagePickerControllerReferenceURL];
    PHFetchResult<PHAsset *> *result = [PHAsset fetchAssetsWithALAssetURLs:@[assetURL] options:nil];
    PHAsset *asset = result.firstObject;
    NSLog(@"%@ %@", result, asset);

ただし、iOS 11 の場合でアクセス権限無しでフォトライブラリを表示選択してこのコードが実行された場合 PHFetchResultPHUnauthorizedFetchResult という Private クラスが返され、アセットの取得が出来ない。

アセットの取得には失敗するがアセットにアクセスしようとした事がトリガーとなってアクセス許可のアラートが表示される。

f:id:Koze:20170920052745p:plain

このタイミングのアラートでアクセス権限を得たとしても既にアセットのフェッチには失敗しているので、再度フォトライブラリを開くか assetURL を使って再度アセットをフェッチするなどしないとアセットは取得出来ない。

iOS 11 に対応する処理の流れとしては UIImagePickerController を表示する前かアセットをフェッチする前に PHPhotoLibrary

+ (void)requestAuthorization:(void(^)(PHAuthorizationStatus status))handler;

で権限を得ておく必要がある。

iOS 10 まではそもそもアクセスが許可されていないとフォトライブラリが表示されないので、こういった事は問題は起こり得ない。

アクセス権限無しでフォトライブラリを開ける意味

アクセス権限が無い状態でも UIImagePickerControllerOriginalImage で取得した UIImage は利用する事が出来る。
また UIImagePickerControllerImageURL を使って NSURL から UIImageNSData を作ったりファイルコピーなどする事も出来る。

UIImagePickerControllerImageURL は iOS 11 で登場したもので元画像のファイルURLが入っている。

ex.)
file:///private/var/mobile/Containers/Data/Application/1770CD90-C268-48BF-8A35-6210698472EF/tmp/EBD7BD90-2A50-4BEB-86D3-63ADB1E12CB0.jpeg


先の例だとアクセス権限が無い状態 = 未確認、つまり PHAuthorizationStatusNotDetermined でのケースだったが、アクセス許可のアラートでアクセスが拒否された場合、つまり PHAuthorizationStatusDenied の状態でも UIImagePickerController によってフォトライブラリを開く事が出来る。

画像に様に許可アラートや設定画面でアクセスが許可されていなくてもフォトライブラリの表示と利用が可能

f:id:Koze:20170920060545p:plain f:id:Koze:20170920060548p:plain

まとめ

  • iOS 11 では UIImagePickerController によるフォトライブラリの表示はユーザのアクセス権限が不要になった。
  • アクセス権限が明確に拒否 (Denied) されている場合でも UIImagePickerController 経由で得られる UIImage や画像の NSURL を使ってユーザが選択した画像データを取り扱う事が出来る。
  • フォトライブラリへのアクセスが発生する PHAsset の取得はアクセス権限が必要。

UIImagePickerController での写真の表示と選択はユーザ操作によるものなのでOK(許可されていると同義)、PHAsset の取得などアプリがコードでフォトライブラリへアクセスする場合はユーザには何をしているか見えないのでアクセス権限が必要という方針なんだろうと推察される。

※動作確認 iOS 11.0 Xcode 9

iOS 11 UIImagePicker test


追記

WWDC 2017 - Session 702 Privacy and Your Apps
https://developer.apple.com/videos/play/wwdc2017/702/

Image picker without prompting for access

12:25 あたりから上記について写真ライブラリの書き込みのみパーミッションが必要な様に変更されたと言及有り。

WWDC 2017 - Session 505 What's New in Photos APIs
https://developer.apple.com/videos/play/wwdc2017/505/

2:15 からもこの仕様変更について詳しく言及あり。

*1:Info.plist に NSPhotoLibraryUsageDescription は記載済みの前提

Core Dataのデータベースの保存場所を切り替える

保存場所を固定的に変更する方法は前回の記事を参照

koze.hatenablog.jp

今回の保存場所の切り替えというのは既に使用している A.sqlite ファイルから別の B.sqlite を使用するようにしたりまた A.sqlite に戻したりするという意味での動的な切り替え。
利用ケースの想定としては

  • ローカルDBとクラウド上のデータのローカルキャッシュのDBを分けて管理する *1
  • サービスのアカウント毎にローカルキャッシュのDBファイルを切り替える *2
  • ローカルデータを削除せず、テストデータを読み込ませてまた後にローカルデータへ戻す *3

等々。

実装

実装はシンプル。Xcode のテンプレートに沿って AppDelegate への実装とする。
まず NSPersistentContainer の管理クラス(ここでは AppDelegate)にDBのURLを設定出来るようにする。

#import <UIKit/UIKit.h>
#import <CoreData/CoreData.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;
@property (readonly, strong) NSPersistentContainer *persistentContainer;
@property (strong) NSURL *databaseURL; // <-- here

- (void)saveContext;

@end

databaseURL が設定されたら値を保持して、persistentContainer を nil にする。

- (void)setDatabaseURL:(NSURL *)databaseURL
{
    _databaseURL = databaseURL;
    _persistentContainer = nil;
}

persistentContainer の初期化コードにこの databaseURL を使って初期化するよう仕込む。
初期化処理は次に persistentContainer メソッドが呼ばれる時に実行される。

- (NSPersistentContainer *)persistentContainer {
    @synchronized (self) {
        if (_persistentContainer == nil) {
            _persistentContainer = [[NSPersistentContainer alloc] initWithName:@"AppName"];
            if (self.databaseURL) {
                NSPersistentStoreDescription *storeDescription = _persistentContainer.persistentStoreDescriptions.firstObject;
                storeDescription.URL = self.databaseURL;
            }
            [_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
                if (error != nil) {
                    NSLog(@"Unresolved error %@, %@", error, error.userInfo);
                    abort();
                }
            }];
        }
    }
    
    return _persistentContainer;
}

以下の様に databaseURL を変更した後に persistentContainer へのアクセスが発生すると指定の場所へDBが作られる事になる。

    NSURL *databaseURL = [[NSURL fileURLWithPath:NSTemporaryDirectory()] URLByAppendingPathComponent:@"Test.sqlite"];
    appDelegate.databaseURL = databaseURL;

もしかするとテンプレートの persistentContainer メソッドの様に _persistentContainer が nil の場合に初期化処理が走るのではなく、初期化処理をメソッドとして用意してやって、databaseURL の設定と共に初期化処理を明示的に実行させてやっても良いかも知れない。

サンプルコードの全体は以下。
(その他の AppDelegate の処理は割愛して Core Data の実装のみ記述)

Change PersistentContainer URL dynamically.

*1:例えばメモアプリのローカルデータとiCloud上のデータ

*2:DBの作りで1つのDBファイルで複数アカウントを管理する事も可能ではある

*3:開発中の話なので前回の方法+マクロで切り替えとかでも可能

NSPersistentStoreDescription のデフォルト値とカスタマイズ

iOS 10で導入された NSPersistentContainer を使うとCore Dataのセットアップのコードがずいぶん短くなる。Xcode のテンプレートはこんな感じ

- (NSPersistentContainer *)persistentContainer {
    @synchronized (self) {
        if (_persistentContainer == nil) {
            _persistentContainer = [[NSPersistentContainer alloc] initWithName:@"AppName"];
            [_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
                if (error != nil) {
                    NSLog(@"Unresolved error %@, %@", error, error.userInfo);
                    abort();
                }
            }];
        }
    }
    
    return _persistentContainer;
}

これだけで済んでしまう。
でも以前セットアップで使ってた NSMigratePersistentStoresAutomaticallyOption NSInferMappingModelAutomaticallyOption NSSQLitePragmasOption といったオプション設定はどうなっているの?と疑問に思ったので調査。

設定内容は _persistentContainer.persistentStoreDescriptions で取得できる NSPersistentStoreDescription のインスタンスから参照出来るらしい。
このインスタンスから取得可能なプロパティを調べてみると以下の様になった。

property value
type SQLite
configuration null
URL ~/Library/Application Support/<Name>.sqlite
options {
NSInferMappingModelAutomaticallyOption = 1;
NSMigratePersistentStoresAutomaticallyOption = 1;
}
readOnly NO
timeout 240
sqlitePragmas {
}
shouldAddStoreAsynchronously NO
shouldMigrateStoreAutomatically YES
shouldInferMappingModelAutomatically YES

設定をカスタマイズする

loadPersistentStoresWithCompletionHandler: を呼ぶ前に NSPersistentStoreDescription の各値を設定する事でカスタマイズが出来る。

例えば URL の様に readwrite なプロパティはそのまま素直に変更可能

_persistentContainer = [[NSPersistentContainer alloc] initWithName:@"AppName"];
NSPersistentStoreDescription *storeDescription = _persistentContainer.persistentStoreDescriptions.firstObject;
NSURL *URL = [storeDescription.URL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"NewLocation.sqlite"];
storeDescription.URL = URL;
[_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {

optionssqlitePragmas は readonly なプロパティになっている代わりに以下の値設定用のメソッドが用意されているのでこれを使う。

- (void)setOption:(nullable NSObject *)option forKey:(NSString *)key;
- (void)setValue:(nullable NSObject *)value forPragmaNamed:(NSString *)name;

例えば以下で SQLite の journal_mode を shm とwal ファイルを生成しない DELETE モードに設定する事が出来る。

_persistentContainer = [[NSPersistentContainer alloc] initWithName:@"AppName"];
NSPersistentStoreDescription *storeDescription = _persistentContainer.persistentStoreDescriptions.firstObject;
[storeDescription setValue:@"DELETE" forPragmaNamed:@"journal_mode"];
[_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {

これらのメソッドを使えばCore Dataのセットアップが従来通り柔軟に出来そうな目処が立つ。

ちなみに shouldMigrateStoreAutomaticallyshouldInferMappingModelAutomaticallyNO にすると optionsNSInferMappingModelAutomaticallyOptionNSMigratePersistentStoresAutomaticallyOptionNO になっていた。
おそらくこれらは良く使うオプションなのでプロパティのフラグ設定と自動で連携して簡単に options にキーと値を設定してくれる様になっているらしい。


参考 SQLite の pragma のリスト
https://sqlite.org/pragma.html