ObjecTips

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

IBSegueAction の使い方

Xcode 11から IBSegueAction が追加された。利用可能なOSは iOS 13, macOS 10.15以降

この新機能は Segue で接続し呼び出す ViewController への必須パラメータ渡しの点でメリットがある。

既存実装

prepare(for:sender:) だと

func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if let viewController = segue.destination as? MyViewController {
        viewController.text = "example"
    }
}

という様に、渡ってくる ViewController は init?(coder: NSCoder) で既に初期化済のインスタンスで、プロパティ経由でパラメータ渡しを行うしかなかった。
そのためパラメータは private にする事ができず internalpublic にする必要があり、また外部から変更可能な var にする必要があった。

Optional の場合は以下

class DestinationViewController: UIViewController {
    var text: String?
}

Non Optional の必須パラメータの場合は以下の様に ImplicitlyUnwrappedOptional で実装する事になる。

class DestinationViewController: UIViewController {
   var text: String!
}

これに加えて ViewController 側での値変更がどのタイミングで呼ばれても良い様に考慮すると(一例としては)以下の様な実装になる。

class DestinationViewController: UIViewController {
    
    @IBOutlet weak var label: UILabel!
    var text: String? {
        didSet {
            updateLabel()
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        updateLabel()
    }
    
    private func updateLabel() {
        if isViewLoaded {
            label.text = text
        }
    }
IBSegueAction

@IBSegueAction attributes を使うと、IBで接続と呼び出しが可能な ViewController を返すメソッドを定義でき、任意のイニシャライザを使って ViewController を作成する事ができる。

class SourceViewController: UIViewController {

    @IBSegueAction
    func makeDestinationViewController(coder: NSCoder, sender: Any?, segueIdentifier: String?) -> DestinationViewController? {
        return DestinationViewController(coder: coder, text: "example")
    }
}

ViewController 側のパラメータは private にする事が可能で let にする事もできる。
初期化に必要なパラメータが変更不要で外部公開不要なプライベートな変数な場合、その役割通りに正しく宣言できる。

class DestinationViewController: UIViewController {

    @IBOutlet weak var label: UILabel!
    private let text: String
    
    init?(coder: NSCoder, text: String) {
        self.text = text
        super.init(coder: coder)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        label.text = text
    }
}
利用方法

IBでの設定はちょっと分かり辛い。
まずこれまでのIBと同じ様に Button などから別の ViewController へ Segue での画面遷移を設定する。
作成した Segue を選択しインスペクタの一番右のタブに切り替えると instantiation という項目がXcode 11では増えており、ここから Control+Drag で ViewController に線を伸ばすとコードで定義した makeDestinationViewControllerWithCoder を選択し接続する事ができる様になっている。

まとめ

外部に出さない変数や変更しない変数をその通りに正しく宣言する事ができるのでGood。
大した事ではないけど独自イニシャライザの宣言によって、既存のイニシャライザ required init?(coder: NSCoder) の空実装が必要な点だけちょっと面倒。


追記

ViewController をカスタムイニシャライザで作成可能にするためのメソッドが UIStoryboard にも追加されていた。

https://developer.apple.com/documentation/uikit/uistoryboard/3213988-instantiateinitialviewcontroller

https://developer.apple.com/documentation/uikit/uistoryboard/3213989-instantiateviewcontroller

使い方は以下の様になる

let viewController = storyboard.instantiateInitialViewController { (coder) -> DestinationViewController? in
    return DestinationViewController(coder: coder, text: "example")
}
let viewController = storyboard.instantiateViewController(identifier: "identifier") { (coder) -> DestinationViewController? in
    return DestinationViewController(coder: coder, text: "example")
}

UIStoryboard からインスタンス化する場合も同じく外部に出さない変数や変更しない変数をその通りに正しく宣言する事ができるのが肝だと思う。

iOS 10 + UIDocumentPickerViewController でクラッシュ

クラッシュ時のコンソールログは以下

*** Assertion failure in -[UIDocumentPickerViewController _commonInitWithCompletion:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3599.6/UIDocumentPickerViewController.m:91
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Application initializing document picker is missing the iCloud entitlement. Is com.apple.developer.icloud-container-identifiers set?'

アプリで iCloud の機能は使っていなくても iOS 10 で UIDocumentPickerViewController を利用する場合は Capabilities で iCloud をオンに設定しておく必要があるらしい。
かつ Files app の導入された iOS 11 以降だと Capabilities での設定不要で問題なく動作するので見落としがち。*1
今回は2019年の iOS 12 時代になってからようやく UIDocumentPickerViewController を機能として初導入したアプリだったので iOS 10 での動作チェック漏れでクラッシュが起きた。

選択肢としては iOS 11 以降のみで UIDocumentPickerViewController の呼び出しボタンを表示する様に分岐する、またはアプリでは iCloud は使ってないけど Capabilities で iCloud をオンにしておくという2つがあると思う。
ちなみに iCloud をオンにして Key-value storage のみを使う様にチェックして、iCloud Documents はチェックしないでおけば iCloud のコンテナ等の設定無しに UIDocumentPickerViewController での iCloud Drive へのアクセスが可能な様なので、Key-value storage にチェックだけしておいてアプリでは Key-value storage の機能を使用しないでおくのが一番影響が少ないと思われる。

f:id:Koze:20190522230217p:plain

*1:iOS 11 以降では UIDocumentPickerViewController イコール Files へのアクセスなのに対して、iOS 10 では UIDocumentPickerViewController イコール iCloud Drive へのアクセスとなるためだと思われる。

This app could not be installed at this time. のエラー対応

Xcode + iOS Simulator ででたまに遭遇するアプリが起動しない以下のエラー。

This app could not be installed at this time.

クリーンビルドするとか Xcode や Mac を再起動するなどいくつか紹介されているけど他の方法も見つけたので1つの解決の例としてメモしておく。
ヒントは Stack Overflow の回答にあった。

stackoverflow.com

Stack Overflow によれば ~/Library/Logs/CoreSimulator/ にあるログファイル内でエラーを見つけたとの事。
でこのディレクトリを見てみると CoreSimulator.log というファイルを発見。
自分の場合ログファイルには以下のエラーが大量に出ていた。

Apr 13 12:44:13 iMac CoreSimulatorService[12122] <Error>: Error Domain=com.apple.CoreSimulator.SimError Code=164 "Unable to shutdown device in current state: Shutdown" UserInfo={NSLocalizedDescription=Unable to shutdown device in current state: Shutdown}

Simulator がシャットダウン出来ないという事らしい。
そこで iOS Simulator を Quit した状態で Activity Monitor.app でプロセスを見てみたところ com.apple.CoreSimulator.CoreSimulatorServiceSimulator というプロセスが残っているのが確認できた。しかも com.apple.CoreSimulator.CoreSimulatorService がややCPUを利用している状態。
こいつが怪しそうだったのでこのプロセスを Activity Monitor で強制終了して Xcode で再度 Run してみたところ見事にエラーが解消された。

まとめ

  • This app could not be installed at this time. に遭遇したら ~/Library/Logs/CoreSimulator/CoreSimulator.log でエラーを確認して対応に当たる事が出来る。

縦書き対応した iWorks アプリの新レイアウトエンジン

縦書き対応した iWorks についてMacお宝鑑定団にて
http://www.macotakara.jp/blog/category-60/entry-37209.html

今回の縦書き表示は、Core Textではなく、iWorks専用レイアウトエンジンが搭載されていて、

との事で気になったので調べてみる。

otool -L /Applications/Pages.app/Contents/MacOS/Pages

上記コマンドでアプリにリンクされているライブラリを確認。
そしてアプリをアップデートしてから再度確認。
アップデート前後のバージョン 7.3 と 8.0 の otool の結果は以下

https://gist.github.com/727c02194f2d9402dd80de7a1620e69b

これを diff ツールで確認した追加ライブラリと削除ライブラリのリストは以下

https://gist.github.com/24ea90e725b6c8a03e6c7fde2994a014

ざっとみて分かるのは

  • ClassKit 対応
  • AddressBook, AudioToolbox, CFNetwork などの古くからの Framework は不使用に
  • CloudKit, Contacts などは使われなくなった訳ではなくアプリ同梱版に完全置き換え。前はアプリ同梱版とシステム版の両方にリンクされていた。
  • EquationKit の追加。Equation の訳は「方程式」なので LaTeX と MathML 関連のコードが切り出されたのではないかと予想される。

そして特筆すべきは Prefix TS から始まる TSKit などのフレームワーク群。いずれもアプリ内に同梱されている。

 @rpath/TSKit.framework/Versions/A/TSKit (compatibility version 1.0.0, current version 1.0.0)
    @rpath/TSStyles.framework/Versions/A/TSStyles (compatibility version 1.0.0, current version 1.0.0)
    @rpath/TSUtility.framework/Versions/A/TSUtility (compatibility version 1.0.0, current version 1.0.0)
    @rpath/TSCoreSOS.framework/Versions/A/TSCoreSOS (compatibility version 1.0.0, current version 1.0.0)
    @rpath/TSPersistence.framework/Versions/A/TSPersistence (compatibility version 1.0.0, current version 1.0.0)
    @rpath/TSTables.framework/Versions/A/TSTables (compatibility version 1.0.0, current version 1.0.0)
    @rpath/TSApplication.framework/Versions/A/TSApplication (compatibility version 1.0.0, current version 1.0.0)
    @rpath/TSAccessibility.framework/Versions/A/TSAccessibility (compatibility version 1.0.0, current version 1.0.0)
    @rpath/TSCalculationEngine.framework/Versions/A/TSCalculationEngine (compatibility version 1.0.0, current version 1.0.0)
    @rpath/TSCharts.framework/Versions/A/TSCharts (compatibility version 1.0.0, current version 1.0.0)
    @rpath/TSDrawables.framework/Versions/A/TSDrawables (compatibility version 1.0.0, current version 1.0.0)
    @rpath/TSText.framework/Versions/A/TSText (compatibility version 1.0.0, current version 1.0.0)

いくつか class-dump してみたところ、TSText の中に以下のプロトコルを発見。これがテキストボックス単位の縦書き横書きの切り替えかと思われる。

@protocol TSWPVerticalTextCommand <NSObject>
- (BOOL)addsOrRemovesVerticalText;
@end

またいくつかのクラスには以下のメソッドが存在しており、こちらはテキスト内の部分的な縦書きに関するメソッドかなと。

- (BOOL)textIsVerticalAtCharIndex:(unsigned long long)arg1;

とりあえず調査はここまでで深くは追わないけど、TS系の Framework が大量に追加されており新たなテキストレイアウトエンジン、テキスト描画エンジンが搭載されているというのは間違いなさそう。

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番だろう。