ObjecTips

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

NSUserDefaults に時・分のみ記録する(日付は不要なケース)

日付は不要で時・分のみを NSUserDefaults に保存したい場合、パッと以下の様な方法が思い付く

  • 時・分を2つに分けて2つの NSNumber で保存する
  • 時・分を分換算して1つの NSNumber で保存する
  • NSDate で保存して時・分のみを利用する

他には以下も考えられる

  • 時・分を設定した NSDateComponents を Archive して NSData にして保存する

Apple はどう実装しているのか実例を探してみる。


「システム環境設定>省エネルギー>ディスプレイをオフにするまで」の時間を1時間5分に設定
System Preferences>Energy Saver>Turn display off after

f:id:Koze:20180706001624p:plain

defaults read /Library/Preferences/com.apple.PowerManagement.plist
{
    "AC Power" =     {
        "Automatic Restart On Power Loss" = 0;
        DarkWakeBackgroundTasks = 1;
        "Disk Sleep Timer" = 10;
        "Display Sleep Timer" = 65;
        "Display Sleep Uses Dim" = 1;
        GPUSwitch = 2;
        "System Sleep Timer" = 65;
        "Wake On LAN" = 1;
    };
    SystemPowerSettings =     {
        "Update DarkWakeBG Setting" = 1;
    };
}

分に換算


「システム環境設定>省エネルギー>スケジュール...」で「起動またはスリープ解除」を毎日1:23「スリープ」を毎日4:56に設定
System Preferences>Energy Saver>Schedule...>Start up or wake, sleep

f:id:Koze:20180706001629p:plain

defaults read /Library/Preferences/SystemConfiguration/com.apple.AutoWake.plist
{
    RepeatingPowerOff =     {
        eventtype = sleep;
        time = 296;
        weekdays = 127;
    };
    RepeatingPowerOn =     {
        eventtype = wakepoweron;
        time = 83;
        weekdays = 127;
    };
}

分に換算


「システム環境設定>通知>おやすみモード>おやすみモードをオンにする設定」で開始を23:45、終了を1:23に設定
System Preferences>Notifications>Do Not Disturb>Turn on Do Not Disturb

f:id:Koze:20180706002503p:plain

sudo defaults read ~/Library/Preferences/ByHost/com.apple.notificationcenterui.*

(なぜか sudo しないと読めない、且つ設定変更直後3秒間ぐらいも読めない)

{
    dndEnd = 83;
    dndMirroring = 0;
    dndStart = 1425;
    doNotDisturb = 0;
}

分に換算


「システム環境設定>ディスプレイ>Night Shift>スケジュール」をカスタムにして開始を23:45、終了を1:23に設定
System Preferences>Displays>Night Shift>Schedule

f:id:Koze:20180706013113p:plain

sudo defaults read /private/var/root/Library/Preferences/com.apple.CoreBrightness.plist

又は

sudo defaults read com.apple.CoreBrightness.plist

抜粋

        CBBlueReductionStatus =         {
            AutoBlueReductionEnabled = 1;
            BlueLightReductionAlgoOverride = 4;
            BlueLightReductionAlgoOverrideTimestamp = "2018-07-05 16:51:14 +0000";
            BlueLightReductionDisableScheduleAlertCounter = 3;
            BlueLightReductionSchedule =             {
                DayStartHour = 1;
                DayStartMinute = 23;
                NightStartHour = 23;
                NightStartMinute = 45;
            };
            BlueReductionEnabled = 1;
            BlueReductionMode = 2;
            BlueReductionSunScheduleAllowed = 1;
            Version = 1;
        };

時・分を分ける

まとめ

NSUserDefaultssetObject:forKey: をすると plist のルートに値が保存されるけど、Apple の実装としては(全体的に保存される設定が複雑な事もあってか、保存パラメータから簡潔にモデルのインスタンスを作るためか)ルートには値を保存せずに辞書でラップして分換算で時・分を保存するのがメジャーなのかなという感じ。
分換算するか時・分を分けて保存するかはどちらでも良い様な気がするけど、とりあえず NSDateNSDateComponents で時・分のみを管理するなんてのは冗長で、管理の面からも human-readble で分かりやすい形で保存するのがベターだと思った。

メモ

plist の内容表示は以下

defaults read foo.plist
plutil -p foo.plist
/usr/libexec/PlistBuddy -c print foo.plist

設定ファイルの捜索場所は以下のあたり
対象の plist ファイルが色々なところに散らばっていて探すのは結構大変だった、、

~/Library/Preferences/
~/Library/Preferences/ByHost/
/Library/Preferences/
/private/var/root/Library/Preferences/

Objective-C と Swift の App Store 配布時のファイルサイズの違い

概要

元々 Objective-C のみで作られたアプリでライブラリに Firebase と自前の Objective-C Framework を複数含む。
これに Swift クラスを3つ(3ファイル)追加しアーカイブしたものを iTunes Connect にアップロードして「App Store ファイルのサイズ」から推定 App Store ファイルサイズを表示。

結果

表の左は Xcode 9.3 でビルドして 2018年3月17日 14:18 にアップロード
表の右は Xcode 9.4 でビルドして 2018年6月1日 1:48 にアップロード
(バージョンが揃ってないため正確なベンチにはならないけどざっと比較確認したかったので、、)

デバイスの種類 ダウンロードサイズ インストールサイズ デバイスの種類 ダウンロードサイズ インストールサイズ
Universal 3.69MB 5.17MB Universal 6.05MB 11.8MB
iPod Touch Sixth Generation 3.63MB 5.02MB iPod Touch Sixth Generation 5.95MB 11.6MB
iPhone SE 3.63MB 5.02MB iPhone SE 5.95MB 11.6MB
iPhone 5S 3.63MB 5.02MB iPhone 5S 5.95MB 11.6MB
iPhone 6 3.63MB 5.02MB iPhone 6 5.95MB 11.6MB
iPhone 6 Plus 3.65MB 5.03MB iPhone 6 Plus 5.96MB 11.6MB
iPhone 6s 3.63MB 5.02MB iPhone 6s 5.95MB 11.6MB
iPhone 6s Plus 3.65MB 5.03MB iPhone 6s Plus 5.96MB 11.6MB
iPhone 7 3.63MB 5.02MB iPhone 7 5.95MB 11.6MB
iPhone 7 Plus 3.65MB 5.03MB iPhone 7 Plus 5.96MB 11.6MB
iPhone 8 3.63MB 5.02MB iPhone 8 Plus 5.96MB 11.6MB
iPhone 8 Plus 3.65MB 5.03MB iPhone 8 5.95MB 11.6MB
iPhone X 3.65MB 5.03MB iPhone X 5.96MB 11.6MB
iPad (5th generation) 3.63MB 5.02MB iPad (5th generation) 5.95MB 11.6MB
iPad Wi-Fi + Cellular (5th generation) 3.63MB 5.02MB iPad Wi-Fi + Cellular (5th generation) 5.95MB 11.6MB
iPad (6th generation) 5.95MB 11.6MB
iPad Wi-Fi + Cellular (6th generation) 5.95MB 11.6MB
iPad Air Wifi 3.63MB 5.02MB iPad Air Wifi 5.95MB 11.6MB
iPad Air Wifi + Cell 3.63MB 5.02MB iPad Air Wifi + Cell 5.95MB 11.6MB
iPad Air 2 Wifi 3.63MB 5.02MB iPad Air 2 Wifi 5.95MB 11.6MB
iPad Air 2 Wifi + Cell 3.63MB 5.02MB iPad Air 2 Wifi + Cell 5.95MB 11.6MB
iPad Mini 2 Wifi 3.63MB 5.02MB iPad Mini 2 Wifi 5.95MB 11.6MB
iPad Mini 2 Wifi + Cell 3.63MB 5.02MB iPad Mini 2 Wifi + Cell 5.95MB 11.6MB
iPad Mini 3 Wifi 3.63MB 5.02MB iPad Mini 3 Wifi 5.95MB 11.6MB
iPad Mini 3 Wifi + Cell 3.63MB 5.02MB iPad Mini 3 Wifi + Cell 5.95MB 11.6MB
iPad mini 4 WiFi 3.63MB 5.02MB iPad mini 4 WiFi 5.95MB 11.6MB
iPad mini 4 WiFi + Cellular 3.63MB 5.02MB iPad mini 4 WiFi + Cellular 5.95MB 11.6MB
iPad Pro WiFi 3.63MB 5.02MB iPad Pro WiFi 5.95MB 11.6MB
iPad Pro WiFi + Cellular 3.63MB 5.02MB iPad Pro WiFi + Cellular 5.95MB 11.6MB
9.7-inch iPad Pro 3.63MB 5.02MB 9.7-inch iPad Pro 5.95MB 11.6MB
9.7-inch iPad Pro Cellular 3.63MB 5.02MB 9.7-inch iPad Pro Cellular 5.95MB 11.6MB
10.5-inch iPad Pro 3.63MB 5.02MB 10.5-inch iPad Pro 5.95MB 11.6MB
10.5-inch iPad Pro Wi-Fi + Cellular 3.63MB 5.02MB 10.5-inch iPad Pro Wi-Fi + Cellular 5.95MB 11.6MB
12.9-inch iPad Pro (2nd generation) 3.63MB 5.02MB 12.9-inch iPad Pro (2nd generation) 5.95MB 11.6MB
12.9-inch iPad Pro Wi-Fi + Cellular (2nd generation) 3.63MB 5.02MB 12.9-inch iPad Pro Wi-Fi + Cellular (2nd generation) 5.95MB 11.6MB

まとめ

ダウンロードベースで2.3MB程度、インストールベースで6.5MB程度が増加した。
もし Swift ソースに複数の Swift バージョンが含まれている場合はランタイムを内包するためもうちょっと容量が増えるのかも知れない。(未確認)
2つのビルドの期間に新しい iPad が発表されているのでデバイスのリストに差異が出ている。
アップロード時にサーバ側で評価された値がそのまま表示される様だ。

等幅フォントのリスト iOS monospace font

iOS で使える等幅フォントどれだっけな?てのをたまに知りたくなる。
検索しても引っかからないなーと思ったら gist に放り込んだまま(4年も前に!)でブログに書いてなかった模様。てことで記事化しておく。

iOS UIKit, macOS AppKit, どちらでも利用可能な Core Text と3通りの実装を書いている。

List of monospace font

パラメータを変えれば等幅フォントの一覧の他にも縦書き対応フォントの一覧を取得したり応用できる。

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 のバージョンの違いを吸収をしてくれるという利点がある。
ただし 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 の実装を呼び出している。なくても良い。