ObjecTips

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

iOS 13 Core Graphics で Tagged PDF の書き出しをサポート

CGPDF 周りの新APIを発見

func CGPDFContextBeginTag(_ context: CGContext, 
                        _ tagType: CGPDFTagType, 
                        _ tagProperties: CFDictionary)

CGPDFContextBeginTag(_:_:_:) - Core Graphics | Apple Developer Documentation

func CGPDFContextEndTag(_ context: CGContext)

CGPDFContextEndTag(_:) - Core Graphics | Apple Developer Documentation

enum CGPDFTagType : Int32

CGPDFTagType - Core Graphics | Apple Developer Documentation

相変わらずオンラインドキュメントが空なので Xcode でSwiftファイルを確認してみたところ以下の記載が!

/* Tagged PDF Authoring */

この後何十行とコメントが記載されており、PDF 1.7 の仕様で定義されている事やドキュメントのリンクも記載されているので詳細が気になる人はコメントやリンクを参照。

http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/pdf_reference_1-7.pdf


Tagged PDF(タグ付きPDF) は Accessibility 対応のPDFで、PDFの中にコンテンツの構造をHTMLに類似したタグ(マークアップ)で示した付加データが入っている。

例えば通常 PDF で表示される文字の情報はフォント、フォントサイズ、描画位置、描画テキストなどの情報が記録されているが、一見1センテンスのまとまった文章に見えても文字毎にバラバラの順序でデータが入っている可能性もある(オーサリングソフトによる)。
また、フォントやフォントサイズの違いによる書類内での意味の違い、例えば表示されているボールド体の文字が文章内での強調なのか、文章の見出しなのか、フォントサイズの小さな文字が注釈なのか、ルビなのか、ページ番号なのかといった情報は記録されていないため、ビューア側でPDFをパースする際にコンテンツ内容をどの様に解釈するかというのはかなり難しくなってくる。

Tagged PDF では描画するコンテンツに意味付けがされるので、スクリーンリーダーでのコンテンツの読み上げ順序やコントロールが改善される。
Accessibility 対応だけではなく、独自ビューアの開発においても Tagged PDF のタグ情報がある事によって同様に読み上げやコントロールの改善が可能になる。

Tagged PDF のタグ情報の読み取りについては相変わらず自前でゴリゴリ実装するしかなさそうだが、PDF の書き出し時にタグ情報を追加できる様になっただけでも前進ではないだろうか。


参考

helpx.adobe.com

helpx.adobe.com

macOS 10.15 AppKit の変更点

AppKit で気になった追加API

NSScreen
var localizedName: String { get }

localizedName - NSScreen | Apple Developer Documentation

これまではスクリーン(macOS ではディスプレイ)に関する情報の取得は CGDisplay 系のAPIで取得していた気がする。
もうちょっと上のレイヤーで簡単にリストアップする事ができる様になりそう。
ちなみに MacBook Pro + Sidecar の環境で実行してみたところ

Build-in Retina Display
AirPlay Display

と表示された。

NSColorSampler

NSColorSampler - AppKit | Apple Developer Documentation

使い方は簡単

NSColorSampler().show { color in
}

これでスポイトツールが表示される。
既存では NSColorPanel を表示してそこからユーザがスポイトツールを選択し使用する事はできたが、NSColorSampler を使うと直接スポイトツールを起動する事ができる。面白い。

NSResponder, NSEvent 周りの changeMode
func changeMode(with event: NSEvent)

changeMode(with:) - NSResponder | Apple Developer Documentation

static var changeMode: NSEvent.EventTypeMask { get }

changeMode - NSEvent.EventTypeMask | Apple Developer Documentation

case changeMode = 38

NSEvent.EventType.changeMode - NSEvent.EventType | Apple Developer Documentation

オンラインドキュメントを見ても Xcode でSwiftファイルを見ても何も載っていないなーと思って探していたら NSResponder の Objective-C header ファイルにコメントが載っていた。

f:id:Koze:20190613122703p:plain

/* Issued in response to a double-tap on the side of the Apple Pencil */ - (void)changeModeWithEvent:(NSEvent *)event API_AVAILABLE(macos(10.15));

macOS で Apple Pencil?
Sidecar 利用時にMacアプリで Apple Pencil のサイドダブルタップが取得できるのだろうかと思い以下の様に、firstRespoder のインスタンスで変更を受け取る実装と NSEvent のモニターメソッドで変更受け取る実装を試してみたけど、Apple Pencil のダブルタップのアクションは受け取れなかった。

    override func changeMode(with event: NSEvent) {
        // not called
        print(event)
    }
    NSEvent.addGlobalMonitorForEvents(matching: .changeMode) { event in
        // not called
        print(event)
    }
    NSEvent.addLocalMonitorForEvents(matching: .changeMode) { event -> NSEvent? in
        // not called
        print(event)
        return event
    }

MonitorForEvents(matching: .any) にするとApple Pencilのタッチ、ドラッグ、圧力などのイベント情報は取れた。ペンサイドのダブルタップのみ反応無し。
まだ beta でちゃんと動いていないのか、何か他の方法での実装が必要なのかも知れない。

Xcode 11 Core Data の変更点

こっちの記事の派生で Core Data の更新箇所だけ切り出し

koze.hatenablog.jp

Core Data

Default Value オプション
  • Core Data のモデルの String attribute で Default Value の項目が空の場合に Null String を使うか Empty String "" を使うかのチェックボックスオプションが付いた

Xcode 10 では Attribute の Optional 設定のオンオフに関わらず、デフォルト値の設定が空の場合はモデルの生成時に自動で初期値に Empty String が入る仕様になっている。
Xcode 11 ではデフォルト値の設定が空の場合、新しいオプション設定に基づいて初期値に Null String か Empty Stirng が設定される。

ただし試してみたところ、Attribute 設定を Non Optional にして、Default Value を Null String にした場合、モデルの初期化後に何も値を設定しないままDBを保存するとクラッシュしてしまうので、初期値が Null String の場合は Attribute の Optional を強制にしてしまう方が良いのではないかと思った。
ここはまだ初期βなのでフィードバックの余地有り。

なお、Core Data にはモデルクラスの自動コード生成の機能があって、Attribute が Optional かどうかに関わらず Swift 上では以下の様な Optional なプロパティのコードが生成されてしまうため、プロパティに nil を入れて保存するとクラッシュというややこしい事になってしまっている。

extension Event {
    @NSManaged public var title: String?
}

これも生成されるモデルのコードはDBのモデルと揃う様にフィードバックした方が良いと思う。
(Miguration まで考えるとなかなか大変そうではある)

CloudKit
  • Core Data の Configuration に CloudKit を使うかどうかのオプションが付いた

Xcode 11, iOS 13 の Core Data の大きなトピックに CloudKit の同期サポートがある。
詳しくは以下

Using Core Data With CloudKit - WWDC 2019 - Videos - Apple Developer

CloudKit を利用するにチェックを入れるとデフォルト値の設定が必須になったりモデルに変更が必要になるが、おそらく Lightweight Migration 可能(Migration の実装不要)な範囲の変更になると思う。

Xcode 11 の変更点

気になるところだけざっくり

Xcode 11 Beta Release Notes | Apple Developer Documentation

New Features
  • SwiftUI(書くまでもなく)

  • Application Loader が Xcode に同梱されなくなった

Asset Catalog
  • Asset Catalog でキーボードショートカットでのコピペと複製が可能に
Core Data
  • 待ち望んでいた CloudKit 連携

別記事へ

koze.hatenablog.jp

Debugging
  • Environment overrides で Xcode での Debug の際にデバッグメニューから Dark Mode, Dynamic Type と Accessibility 関連の設定変更が可能になった

Accessibility Inspector を起動しなくても Xcode 上で素早くアクセスできる様になったのは便利。Environment overrides では目視ベースでの確認に留まりそうで、さらなるデバッグはこれまで通り Accessibility Inspector を別途起動して行う事ができる。

Deprecations
  • Quartz Composer framework の廃止

framework が deprecated になる事によって QuartzComposer.framework を利用したアプリの作成が将来的にできなくなる。
開発ツールとしての Quartz Composer.app が廃止になるわけではないのでQCアプリは引き続き Core Image のプロトタイピングツールとして利用可能。

Interface Builder
  • UITaleViewCell で Auto Layout を設定した時に、IB上でセルの高さが自動で調整される様になった。
  • IBで var translatesAutoresizingMaskIntoConstraints: Bool を設定可能。
  • IBで UIScrollView のコンテンツとその Auto Layout を設定した際にスクロールプレビューが可能。
  • watchOS への WKInterfaceTextField の追加
  • SF Symbols の追加

Introducing SF Symbols - WWDC 2019 - Videos - Apple Developer

  • IBSegueAction の追加

別記事へ

koze.hatenablog.jp

IB周りはかなり良くなった。

Localization
  • Asset Catalog をローカライズ可能に(アイコンファイル以外)
Project Navigator
  • 検索と置き換えで Asset Catalog のリネームを可能に
Signing and Distribution
  • iOS と macOS どちらにも使える Certificate が使える様に(これまでは iOS と macOS は別々の Certificate が必要だった)
Simulator
  • Simulator が Metal に対応。ただし macOS 10.15 で動かした時のみらしい。古いOSの場合はソフトウェアレンダーの OpenGL が使われる。
    Metal は Mac の GPU を使って動く。複数の GPU が利用可能な場合は使用する方を選択可能。
Source Editor
  • スペルチェック機能の追加
Known Issues
  • iOS Simulator で iCloud sync が使えない
  • iOS Simulator と Mac とのペーストボードの自動同期が使えない。メニューからマニュアルでペーストボードの操作を行う必要がある。
  • Swift 5.1 の新機能 Opaque Result Type some Protocol は Swift 5.1, iOS 13 が必要で古いOSでは利用できないが、コンパイラがこれを強制しない(エラーを出してくれない)ため古いアプリ向けでもビルド自体は通り、実際にアプリを実行して該当箇所を呼び出した時にクラッシュする。

シミュレータでの実行時エラーは以下だった

dyld: lazy symbol binding failed: Symbol not found: _swift_getOpaqueTypeMetadata
  Referenced from: /Users/<UserName>/Library/Developer/CoreSimulator/Devices/<DeviceUUID>/data/Containers/Bundle/Application/<AppUUID>/<BundleName>.app/<ExecutableName>
  Expected in: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/swift/libswiftCore.dylib

What's New in Swift - WWDC 2019 - Videos - Apple Developer


とりあえず Xcode の変更点からのピックアップはこんなところ。
個人的には Core Data + CloudKit は最高のアップデートで、IBが色々と良くなっているのも嬉しい。
SwiftUI は iOS 13 以降必須になってしまうため、折を見てぼちぼちやっていく予定。

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 にも追加されていた。

instantiateInitialViewController(creator:) - UIStoryboard | Apple Developer Documentation

instantiateViewController(identifier:creator:) - UIStoryboard | Apple Developer Documentation

使い方は以下の様になる

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 でエラーを確認して対応に当たる事が出来る。