ObjecTips

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

CaseIterable を使って case の index を取得する

前段

Swift 4.2 の CaseIterableallCaeses を使って enum の case の一覧や個数が取得可能になり随分便利になった。
表題の enum の index の取得について、まず Int 型の場合は以下の様に CaseIterable を使わなくても rawValue を使ってシンプルに取得出来る。

enum Animal: Int {
    case dog
    case cat
    case rabbit
}

let index: Int = Animal.cat.rawValue

CaseIterable を使って case の index を取得する

では Int 型の enum が様々な事情により以下の様に定義されている場合どうだろう。

enum Animal: Int {
    case dog = 1
    case cat = 3
    case rabbit = 4
}

この場合 CaseIterable を適用すれば下記の様に allCases のコレクションを使って index の取得が出来る。
firstIndex(of:) が返す型は Optional の Int? 型だが .cat は確実に allCases に存在しているので Optional を unwrap している。

enum Animal: Int, CaseIterable {
    case dog = 1
    case cat = 3
    case rabbit = 4
}

let index: Int = Animal.allCases.firstIndex(of: .cat)!

rawValue に依存しないで index が取得出来る様になったので型も Int 型に縛られる事がなくなる。
例えば String だったり

enum Animal: String, CaseIterable {
    case dog = "イヌ"
    case cat = "ネコ"
    case rabbit = "うさぎ"
}

let index: Int = Animal.allCases.firstIndex(of: .cat)!

型自体を外してしまって RawRepresentable プロトコルに適合しなくしても良い。

enum Animal: CaseIterable {
    case dog
    case cat
    case rabbit
}

let index: Int = Animal.allCases.firstIndex(of: .cat)!

Extension にする

色々汎用化を試みて CaseIterable の Extension とする実装に落ち着いた。
これを Swift のプロジェクトに入れておけば CaseIterable な enum から一発で index を取得する事が出来る。

extension Hashable where Self : CaseIterable {
    var index: Self.AllCases.Index {
        return type(of: self).allCases.firstIndex(of: self)!
    }
}

以下の様に簡単に書けるので UITableView 周りで section と row から IndexPath を作る時などに便利に使ってる。

enum Animal: CaseIterable {
    case dog
    case cat
    case rabbit
}

let index: Int = Animal.cat.index

*1


ソースファイル

https://gist.github.com/Koze/59a1b31c45217b9f46c11737ba905534#file-caseiterable-index-swift

index of CaseIterable enum

*1:毎回 allCases を呼ぶためリソースの無駄がある事は頭の隅に置いておくべきかも。それをシビアに気にしなくちゃいけない様な巨大な enum はあまり無いとは思うけど。

Swift の enum の型の明記とビルド速度

調査の動機

Swift って結構省略して書けるけどその分 Xcode が脳内補完するからビルドが遅くなるんじゃないの?
だったら省略表記無しでコードが長くなってもビルド速度が早い方がいい。
エビデンスが無いので一応確認してみよう。

ビルド環境

ビルド環境
本体 iMac (Retina 5K, 27-inch, Late 2014)
プロセッサ 3.5 GHz Intel Core i5
メモリ 24 GB 1600 MHz DDR3
Xcode Xcode 10.0

比較コード

enum の型を明記するパターン

cell.accessoryType = UITableViewCell.AccessoryType.checkmark

enum の型を省略するパターン(こちらの記述が一般的)

cell.accessoryType = .checkmark

比較手順

let cell: UITableViewCell = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: "")
cell.accessoryType = UITableViewCell.AccessoryType.checkmark
cell.accessoryType = UITableViewCell.AccessoryType.checkmark
cell.accessoryType = UITableViewCell.AccessoryType.checkmark
// ... repeat 100,000 lines

という感じでコードを10万行並べる。
*1 *2

手順1

ビルド時間が Xcode のウィンドのタイトルバーに表示される様にしておく。

Terminal で
defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES

手順2

Xcode で
Option+Shift+Command+K で Clean Build Folder を実行

手順3

~Library/Developer/Xcode/DerivedData/ 以下を削除

Terminal で
rm -rdf Library/Developer/Xcode/DerivedData/*

手順4

Xcode で
Command+B でビルドを実行
Destination は iOS Simulator

結果

手順2~4を何回か試したところ以下の結果

コード 時間
cell.accessoryType = UITableViewCell.AccessoryType.checkmark 約90sec
cell.accessoryType = .checkmark 約50sec

意外にも型明記した方が遅かった!
もしかすると型の省略パターンでは左辺により型のツリーの2階層目まで決定していて*3その下の3階層目をチェックするだけのところを、型の明記パターンではまた1階層目から順に型のチェックが走っているという事なのかも知れない。

let a = ... みたいな一般的な変数の型推論パターンでは型指定した方がビルドが早いというのはよく言われる事だけど、今回のケースでは途中まで確定している型を打ち消して記述する事で型チェックの回数が増えてしまい遅くなるという事なんだと思う。

という事で型明記の有無どちらがビルドが早いかはケースバイケースで一概には言えないけど、今回の様な引数やプロパティで型情報が与えられている場合の enum の値の記述については指定の型情報を生かして値だけを書く様にしていこうと思う。

// ex.) 引数
UITableViewCell(style: .default, reuseIdentifier: "Cell")

// ex.) プロパティ
cell.selectionStyle = .blue

*1:やってみた結果1万行じゃ大した差が出なかった

*2:Xcode 上で1万行単位のコピペをやると固まるので TextEdit.app でソースを開いてコピペ作業を行った

*3:これを「推論」というのか単に「型情報が与えられている」というのか

Swift + Core Data Code Generation で Objective-C からの呼び出しでクラッシュするケース

部分的に Swift で書き直しているプロジェクトで起きたケース。
.xcdatamodeld のエンティティ設定で Core Data の Code Generation を Objective-C から Swift に変更した後 Objective-C からの呼び出しでクラッシュ

まず Objective-C と Swift の Core Data Code Generation については以下を参照

koze.hatenablog.jp

Swift ソースを自動生成した場合、class func fetchRequest@nonobjc になっているのに注意。
クラスメソッドとしては以下の様にiOS 10以降で +fetchRequest が利用可能になっているが、

// Objective-C
@interface NSManagedObject : NSObject
+ (NSFetchRequest*)fetchRequest API_AVAILABLE(macosx(10.12),ios(10.0),tvos(10.0),watchos(3.0));
@end
// Swift
open class NSManagedObject : NSObject {
    @available(iOS 10.0, *)
    open class func fetchRequest() -> NSFetchRequest<NSFetchRequestResult>
}

呼ばれる実装の実態として自動生成によりサブクラス(モデルクラス)に実装されている fetchRequest メソッドが必要となるため、以下の様に Objective-C でメソッド呼び出しを行った場合に自動生成されたメソッドが呼ばれず意図した NSFetchRequest が取得出来ずクラッシュする模様。

// Objective-C
NSFetchRequest<Model *> *request = [Model fetchRequest];

対応パターン

1.

Objective-C ソースを生成する様に設定し Objective-C と Swift から +fetchRequest を利用する。

// Objective-C
NSFetchRequest<Model *> *request = [Model fetchRequest];
// Swift
let request: NSFetchRequest<Model> = Model.fetchRequest
2.

Swift ソースを生成する様に設定した場合、Objective-C からの呼び出しは
-fetchRequestWithEntityName: メソッドを使いエンティティ名を指定して fetchRequest を作成する様に実装する。

// Objective-C
NSFetchRequest<Model *> *request = [NSFetchRequest fetchRequestWithEntityName:@"Model"];
// Swift
let request: NSFetchRequest<Model> = Model.fetchRequest
3.

Swift ソースを生成する様に設定し Swift からしか呼び出さない様にする。

// Swift
let request: NSFetchRequest<Model> = Model.fetchRequest

Swift への移行が進めば3番になっていくかも知れないがまずは1番か2番だろう。

Core Data Code Generation の Objective-C と Swift の違い

環境 Xcode 10.0

Core Data Code Generation

Code Generation は Xcode が Core Data のモデルクラスの基本実装を自動で行ってくれる機能で <ProductName>.build/Debug-iphonesimulator/<ProductName>.build/DerivedSources/CoreDataGenerated/<FileName> の中にファイルが自動で生成される。

FileName は .xcdatamodeld のファイル名

Objective-C + Code Generation

Objective-C の場合以下の様な4ファイルが生成される。
(Model は .xcdatamodeld で設定したエンティティ名、もしくはクラス指定をしていればクラス名が入る)

Model+CoreDataClass.h

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

NS_ASSUME_NONNULL_BEGIN

@interface Model : NSManagedObject

@end

NS_ASSUME_NONNULL_END

#import "Model+CoreDataProperties.h"

Model+CoreDataClass.m

#import "Model+CoreDataClass.h"

@implementation Model

@end

Model+CoreDataProperties.h

#import "Model+CoreDataClass.h"


NS_ASSUME_NONNULL_BEGIN

@interface Model (CoreDataProperties)

+ (NSFetchRequest<Model *> *)fetchRequest;

@property (nullable, nonatomic, copy) NSDate *creationDate;
@property (nullable, nonatomic, copy) NSDate *modificationDate;

@end

NS_ASSUME_NONNULL_END

Model+CoreDataProperties.m

#import "Model+CoreDataProperties.h"

@implementation Model (CoreDataProperties)

+ (NSFetchRequest< Model *> *)fetchRequest {
    return [NSFetchRequest fetchRequestWithEntityName:@"Model"];
}

@dynamic creationDate;
@dynamic modificationDate;

@end
Swift + Code Generation

Swift の場合以下の2ファイルが生成される。
Model+CoreDataClass.swift

import Foundation
import CoreData

@objc(Model)
public class Model: NSManagedObject {

}

Model+CoreDataProperties.swift

import Foundation
import CoreData


extension Model {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Model> {
        return NSFetchRequest<Model>(entityName: "Model")
    }

    @NSManaged public var creationDate: Date?
    @NSManaged public var modificationDate: Date?

}

これらのソースが自動で生成され利用する事が出来る。

Objective-C + Code Generation 利用編

Objective-C で Code Generation した場合は追加で以下の2つのファイルも生成され、利用にはこれを import する必要がある。

FileName+CoreDataModel.h

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

#import "Model+CoreDataClass.h"

FileName+CoreDataModel.m

#import "FileName+CoreDataModel.h"

Objective-C で利用するにはクラスのヘッダか実装部に import する

#import "ProductName+CoreDataModel.h"

Swift から利用するには Objective-C to Swift の橋渡しとして ProductName-Bridging-Header.h に import する

#import "ProductName+CoreDataModel.h"

*1

Swift + Code Generation 利用編

Swift で Code Generation した場合は追加で以下のファイルが作成されるが、それぞれのクラスのスコープがデフォルトの internal になっているため Objective-C と比べてソースの中身は特に何も実装されていない。

FileName-CoreDataModel.swift

import Foundation
import CoreData

Swift で Code Generation して Swift で使う場合、プロジェクト上にはヘッダもクラス定義も存在しない(見えていない)がクラスを呼び出して使用する事が出来る状態になる。

Objective-C から利用するには Swift to Objective-C の橋渡しとして ヘッダか実装部に以下を import する

#import "ProductName-Swift.h"

*2

*1:Bridging-Header は正確にはビルド設定の Objective-C Bridging Header の値

*2:正確にはビルド設定の Objective-C Generated Interface Header Name の値でデフォルト値は $(SWIFT_MODULE_NAME)-Swift.h

アプリの譲渡 App Transfer の注意点1

App Transfer の手順については以下

koze.hatenablog.jp

譲渡作業で確認できた注意点

ダウンロード等のレポート情報が見られなくなる

アプリの譲渡を行うと譲渡側の App Store Connect からアプリ自体が削除されてしまい、これまでのダウンロード数などのレポート画面を表示する事が出来なくなる。
アプリの受取側のレポートには譲渡後の数字のみが反映され譲渡前の数字を表示する事(知る事)は出来ない。
アプリを譲渡したからといってこれまでのマーケットに関するデータまで全て受取側に提供する必要がないのは分かるが、譲渡側がこれまでのレポートにアクセス出来なくなるのはちょっと問題かも。Apple に要望を出してもいいかも知れない。

アプリの譲渡前後を合算した総ダウンロード数などの情報が必要になる事はままあると思うので、譲渡の際には譲渡側で譲渡直前の最新の各種レポートをCSVなりXLS形式でダウンロードしておく事をオススメする。
今回のケースでは総ダウンロード数、全体の月別ダウンロード数、地域別の月別ダウンロード数、地域を掘り下げた各国の月別ダウンロード数、バージョン毎のダウンロード数、月別アップデート数を事前に取得しておいた。

例えば3月に譲渡を行なった場合に譲渡側では3月の途中までのデータ、受取側では3月の途中からのデータを取得可能なので、集計レポートを作成する際に3月分については各レポートを自前で合算してやる必要がある。

1月 2月 3月 譲渡 3月 4月 5月
ダウンロード数 10 10 4 6 10 10

手動で合算 ↓

1月 2月 3月 4月 5月
ダウンロード数 10 10 10 10 10

もし月別ではなく日別でレポートを取得した場合にきっちりと譲渡前後の日付でレポートが分割されるのであれば集計もセルを並べるだけでいいので楽だったかも知れない。(もし譲渡日のレポートが譲渡側と受取側で分割してレポートが取れるのであれば譲渡日のレポートをやはり自前で集計する必要がある)

App Store Connect のレポートは時間帯によっては前日や2日前分までしか取得出来ないので、レポートのタイムラグと譲渡切り替えのタイミングの関係で1日分譲渡側と受取側のどちらもアクセス出来ない日が存在するのではという懸念もある。

いずれにしても、もし次の機会があれば日別のレポートをダウンロードしておこうと思う。

アプリの譲渡 App Transfer の手順

App Transfer の概要

App Transfer は App Store でのアプリの譲渡機能で、レビューを維持したまま他の Developer アカウントへアプリを移管する事が出来る。ユーザはアプリを別途ダウンロードする必要はなく同一のアプリとしてアップデートを行う事が出来る。
現在は日本語ドキュメントが用意されているので詳しくはそちらを参照のこと
App の譲渡の概要
https://help.apple.com/app-store-connect/#/deved688524f

アプリの引き継ぎ、アップデートが可能とは言え、すべてが完璧に引き継がれるわけではない。
iCloudを利用しているとアプリを譲渡出来ない、Keychain共有を使用していない場合 Keychain に保存済みの情報にはアクセス出来なくなるなどいくつかの制約もある。
上記の「App の譲渡の概要」と合わせて以下も参照のこと
App の譲渡の条件
https://help.apple.com/app-store-connect/#/devaf27784ff

手順

*1

譲渡側

App Store Connect (iTunes Connect) でアプリを選択して「App 情報>追加情報>App の譲渡」を選択

f:id:Koze:20180725095837p:plain

App の譲渡の承諾画面が表示される

f:id:Koze:20180725100307p:plain f:id:Koze:20180725100314p:plain

TestFlight ベータ版テスト 譲渡する App からすべてのビルドおよびテスターを削除し、テスト情報の各フィールドのデータを消去する必要があります。

上記項目が譲渡の条件を満たしていないと表示される。

TestFlight のビルドを見ると TestFlight 配信済みのアプリが確認出来る。

f:id:Koze:20180725101425p:plain

これを全て削除

f:id:Koze:20180725101605p:plain

App Store Connect (iTunes Connect) ユーザと、すべてのテスター(外部テスター)を確認。

f:id:Koze:20180725101934p:plain f:id:Koze:20180725101942p:plain

これらも全て削除

f:id:Koze:20180725102103p:plain

もう一度「App の譲渡」画面を表示

f:id:Koze:20180725102357p:plain

TestFlight ベータ版テスト 譲渡する App からすべてのビルドおよびテスターを削除し、テスト情報の各フィールドのデータを消去する必要があります。

まだ上記の条件を満たしていないと表示される。
「TestFlight>APP 情報>テスト情報」を表示する。

f:id:Koze:20180725102802p:plain f:id:Koze:20180725102809p:plain

上記の記入欄を全て削除する。

f:id:Koze:20180725103256p:plain

再度「App の譲渡」画面を表示

f:id:Koze:20180725103421p:plain f:id:Koze:20180725103429p:plain

「続ける」を選択

f:id:Koze:20180725103550p:plain

受取側の Agent の Apple ID と Team ID を入力して「続ける」を選択すると同意画面が表示される。

f:id:Koze:20180725103854p:plain f:id:Koze:20180725103902p:plain

これで譲渡リクエスト完了

f:id:Koze:20180726180850p:plain

契約画面の Contract In Process からリクエストの取り消しも可能

f:id:Koze:20180725104311p:plain

受取側

Apple から Agent 宛にメールが届く

f:id:Koze:20180725104825p:plain

App Store Connect にログインすると譲渡リクエストに関するメッセージが表示される。

f:id:Koze:20180725104945p:plain

契約画面を表示すると Contract In Process に譲渡リクエストが表示される。

f:id:Koze:20180725105325p:plain

Review を選択すると App Transfer についての画面が表示される。

f:id:Koze:20180725114242p:plain

そのまま次の画面に進もうとしてもエラーで弾かれるので、適宜必要な情報を入力する。

f:id:Koze:20180725114655p:plain

完了すると譲渡リクエストが Contract In Effects に移る。

f:id:Koze:20180725110055p:plain

3時間ぐらいで処理が完了するとメールが届く。

f:id:Koze:20180725111224p:plain

これにて完了。

*1:スクリーンショットが iTunes Connect と App Store Connect と混在しているのは App Transfer を行なった時期が2018年3月で、記事を書いたのが App Store Connect 発表後の2018年7月のため。ご了承を。

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/