iOS で使える等幅フォントどれだっけな?てのをたまに知りたくなる。
検索しても引っかからないなーと思ったら gist に放り込んだまま(4年も前に!)でブログに書いてなかった模様。てことで記事化しておく。
iOS UIKit, macOS AppKit, どちらでも利用可能な Core Text と3通りの実装を書いている。
パラメータを変えれば等幅フォントの一覧の他にも縦書き対応フォントの一覧を取得したり応用できる。
iOS で使える等幅フォントどれだっけな?てのをたまに知りたくなる。
検索しても引っかからないなーと思ったら gist に放り込んだまま(4年も前に!)でブログに書いてなかった模様。てことで記事化しておく。
iOS UIKit, macOS AppKit, どちらでも利用可能な Core Text と3通りの実装を書いている。
パラメータを変えれば等幅フォントの一覧の他にも縦書き対応フォントの一覧を取得したり応用できる。
良く見かける技で、デバッグビルド時のみ出力されるログを以下の様に定義出来る。
#if DEBUG #define DebugLog(...) NSLog(__VA_ARGS__) #else #define DebugLog(...) #endif
Before
#if DEBUG NSLog(@"debug message"); #endif
After
DebugLog(@"debug message");
応用して 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;
});
これでデバッグビルド時のみ実行される実装をブロック内に記述する事が出来る。
上記ではデバッグビルド時のみビューの位置と大きさを確認出来る様に枠線を表示している。
一応上記の方法で retain count を調べてみたところ、デバッグビルド時のみ変数のキャプチャが行われリリースビルド時はキャプチャが行われなかった。
Xcode で記述する際にブロック構文のコード補完が行われないのが惜しいけど、まぁまぁ使えそう。
まず取得方法をざっくり
Exif等のデータをプロパティとして Core Image
か Image I/O
で取得可能
CIImage *image = [CIImage imageWithContentsOfURL:URL]; NSDictionary<NSString *,id> *properties = image.properties; NSLog(@"%@", properties);
CGImageSourceRef src = CGImageSourceCreateWithURL((__bridge CFURLRef)URL, nil); NSDictionary *properties = CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(src, 0, nil)); NSLog(@"%@", properties); CFRelease(src);
Image I/O
でメタデータとしても取得可能
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);
CoreImage
と Image I/O + CGImageSourceCopyPropertiesAtIndex
で取得出来る内容は同じ。
ただし後者の CGImageSourceCopyPropertiesAtIndex
の場合は1つのファイルに複数の画像が含まれる画像形式に対応しているので、仮に複数画像が存在して且つ画像毎に取得出来るプロパティが異なっている場合 CIImage
から取得出来るプロパティとどう違いがあるかは検証が必要。(おそらくindex 0の1枚目の画像のプロパティを返すんじゃないかと予想)
以下が取得出来るプロパティのサンプル
Exif data with iOS 11 iPhone X back camera
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
はプロパティを見ると対応するものが無いように見える。
ググって見つけた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
は ImageIO の prefix でApple独自のメタデータタグだと思われる。(ImageIOBase.h を見ると IIO_HAS_IOSURFACE
や IIO_BRIDGED_TYPE
といった定義が見られる。)
名前の通りxmp
のタグが存在しているかどうかを示すものだと推測。
先のCIPAのPDFには以下のように掲載されていたので、xmp:CreateDate
と xmp: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 のバージョンの違いを吸収をしてくれるという利点がある。
ただし ColorModel
や ProfileName
だったりプライベート仕様の 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 の読み上げ言語の設定画面(設定>アクセシビリティ>スピーチ>声)
テストコード
カタカナの部分に関してはいずれも差異は無いが、漢字のソート結果が違っている。
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
が常に UIStatusBarStyleDefault
で黒文字になってしまう。通常は問題無いがアプリのデザインでナビゲーションバーやボタン類に色を付けている場合、ステータスバーの文字色とマッチしない事がある。
Stack Overflow で検索してみると
UIViewControllerBasedStatusBarAppearance
を NO
にして UIStatusBarStyle
を UIStatusBarStyleLightContent
に設定する [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent animated:YES];
という話が出てくる。
ところが iOS 11 で試してみたところ Info.plist での設定がうまく効かず(以前はこれでいけた様な?)またコードでの設定は iOS 9 で deprecated になっており今後完全に remove になる可能性もあるので他の方法も探してみる。
メールが起動された時の画面を Debug View Hierarchy で確認してみると MFMailComposeViewController
と他2つの Private な MFMailComposeInternalViewController
MFMailComposeRemoteViewController
クラスが使われている事が分かった。
MFMailComposeViewController
は公開クラスなので、カテゴリで既存メソッドをオーバーライドすれば挙動を変える事が出来る(ただしデフォルト挙動の書き換えはリスクを鑑みて変更は最小限にしておいた方が良い)。
実装は以下
Change the status bar to white when MFMailComposeV ...
以下の2つのメソッドが呼ばれる。
- (UIStatusBarStyle)preferredStatusBarStyle; - (UIViewController *)childViewControllerForStatusBarStyle;
MFMailComposeViewController
のモーダルアニメーションと同時にステータスバーのスタイルが変更される。
このケースでは viewDidAppear:
はオーバーライドしなくてもOK。
以下のメソッドが呼ばれる。
- (void)viewDidAppear:(BOOL)animated;
先の2つのメソッドは呼ばれない。
MFMailComposeViewController
のモーダルアニメーションの完了のタイミングの viewDidAppear:
でステータスバーのスタイルが変更される。
このケースでは viewDidAppear:
のみをオーバーライドすればOK。
*1
これで以下の様にメール画面でのステータスバーのスタイルを UIStatusBarStyleLightContent
に変更する事が出来る。
*1:[super viewDidAppear:animated] は MFMailComposeViewController ではなくてその親クラスの UIViewController の実装を呼び出している。なくても良い。
iOS 10 までは UIImagePickerController
と UIImagePickerControllerSourceTypePhotoLibrary
の組み合わせでフォトライブラリを表示する際にアクセス権限の確認を自動で行ってくれていた。
アクセス権限がなければ鍵マークが表示され一切の情報を取得する事ができない。ユーザの操作としては必ずキャンセルが行われる。*1
自動でアクセス許可のアラートが表示される
権限なしの状態ではキャンセルしか出来ない
権限があればフォトライブラリを表示、写真を選択可能
iOS 11 ではアクセス権限の確認が自動で行われない様になった。そして権限がなくともフォトライブラリの表示は行われ、写真の選択も出来てしまう。
権限の確認無しにいきなりフォトライブラリが表示
写真の選択を行うと通常通り 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 の場合でアクセス権限無しでフォトライブラリを表示選択してこのコードが実行された場合 PHFetchResult
は PHUnauthorizedFetchResult
という Private クラスが返され、アセットの取得が出来ない。
アセットの取得には失敗するがアセットにアクセスしようとした事がトリガーとなってアクセス許可のアラートが表示される。
このタイミングのアラートでアクセス権限を得たとしても既にアセットのフェッチには失敗しているので、再度フォトライブラリを開くか assetURL を使って再度アセットをフェッチするなどしないとアセットは取得出来ない。
iOS 11 に対応する処理の流れとしては UIImagePickerController
を表示する前かアセットをフェッチする前に PHPhotoLibrary
の
+ (void)requestAuthorization:(void(^)(PHAuthorizationStatus status))handler;
で権限を得ておく必要がある。
iOS 10 まではそもそもアクセスが許可されていないとフォトライブラリが表示されないので、こういった事は問題は起こり得ない。
アクセス権限が無い状態でも UIImagePickerControllerOriginalImage
で取得した UIImage
は利用する事が出来る。
また UIImagePickerControllerImageURL
を使って NSURL
から UIImage
や NSData
を作ったりファイルコピーなどする事も出来る。
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
によってフォトライブラリを開く事が出来る。
画像に様に許可アラートや設定画面でアクセスが許可されていなくてもフォトライブラリの表示と利用が可能
UIImagePickerController
によるフォトライブラリの表示はユーザのアクセス権限が不要になった。UIImagePickerController
経由で得られる UIImage
や画像の NSURL
を使ってユーザが選択した画像データを取り扱う事が出来る。PHAsset
の取得はアクセス権限が必要。UIImagePickerController
での写真の表示と選択はユーザ操作によるものなのでOK(許可されていると同義)、PHAsset
の取得などアプリがコードでフォトライブラリへアクセスする場合はユーザには何をしているか見えないのでアクセス権限が必要という方針なんだろうと推察される。
※動作確認 iOS 11.0 Xcode 9
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 は記載済みの前提
保存場所を固定的に変更する方法は前回の記事を参照
今回の保存場所の切り替えというのは既に使用している A.sqlite ファイルから別の B.sqlite を使用するようにしたりまた A.sqlite に戻したりするという意味での動的な切り替え。
利用ケースの想定としては
等々。
実装はシンプル。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 の実装のみ記述)