cockscomblog?

cockscomb on hatena blog

UIButtonの画像の位置を変えたい

UIButton の画像の位置を、タイトルの左側じゃなく上側や右側にしたい。画像が左側じゃないボタンのモックを見たとき、どうやって実現するかいつも少し悩む。ということでいくつかのアプローチを検討したい。

drawRect(_:) など

UIButtonvar titleLabel: UILabel? { get }var imageView: UIImageView? { get } といった view を内包していて、描画はそういった view の側で行われるので、drawRect(_:) などの描画関連のメソッドをオーバーライドするべきではない。

独自に UIImageView を足したり CALayer を足したりして、表示される画像を追加することはできる。

setBackgroundImage(_:forState:)

画像を背景にする。UIImageresizableImageWithCapInsets(_:) と組み合わせれば、わりと好きな位置に画像を表示できるはずである。不毛な感じはする。

titleEdgeInsetsimageEdgeInsets

タイトルの画像の UIEdgeInsets でチマチマ調整する方法。もともと画像が左にあるところからがんばって計算することになって厳しい。

titleRectForContentRect(_:)imageRectForContentRect(_:)

titleRectForContentRect(_:)imageRectForContentRect(_:) といったメソッドをオーバーライドしていい感じの CGRect を返す。

似たような backgroundRectForBounds(_:) メソッドのドキュメントには

Subclasses that provide custom background adornments can override this method and return a modified bounds rectangle to prevent the button from drawing over any custom content.

ということが書いてあって、オーバーライドしてよいことがわかる。しかし contentRectForBounds(_:) を含む同種の他のメソッドには書いていない。同種の他のメソッドには対応する UIEdgeInsets があるので、そういうのを考慮しているからオーバーライドしないようにという意味なのか、温度感がわからない。

UIControl のサブクラス

完全に独自の Button を作ることもできる。UIControl を継承して何もかも自分で作ることで、自由な内容を描画できる。特に不都合はないが、面倒ではある。


titleRectForContentRect(_:)imageRectForContentRect(_:) のオーバーライドがいちばん現実的ではないか。という結論に至った。

詳解 Swift 改訂版

詳解 Swift 改訂版

マツコの知らない State Restoration の世界

慣れない街へ出かけるとき、電車の中で目的地までの道のりを「地図アプリ」で検索しておいて、駅を出てから地図を見て移動したことはありますか。あるいは「ネットショッピングアプリ」で欲しいものが見つかって、衝動買いするにはちょっと高価だったときに、レビューを読んだりネット上での評判を確かめたりしたことはありませんか。もしそんなとき、地図アプリやネットショッピングアプリを離れて他のことをして、また戻ってきたときに、そのアプリは元の画面のままですか。それともアプリの最初の画面に戻ってしまっていますか。

iOSState Restoration という機能は、アプリを元の状態に復元するための仕組みを提供します。メモリが不足してバックグラウンドのアプリが終了した後、次に起動したときに、アプリを元の状態に戻すことができます。地図で見つけた道順や、検討中の商品、または入力中のメッセージや、さっき見ていた web ページなど、そういったものをそのまま残しておくことができます。もしこのような復元が行われなければ、ユーザーはそれまでのコンテキストを失い、せっかく見つけたものを見失ったり、大切な言葉を忘れてしまったり、とてもがっかりしてしまうに違いありません。

あなたのアプリのユーザーが戻ってきた時にがっかりしないよう、state restoration の力を借りてみましょう。

First Step

ここからは state restoration を実現するための実装方法を紹介します。

老婆心からはじめに注意しておきたいと思いますが、state restoration は UIKit の中では難しい部類の機能です。そして既存のアプリの設計では対応できないこともあります。どうかがっかりしないで、粘り強く取り組んでほしいと思います。

UIApplicationDelegate

optional func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool
optional func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool

UIApplicationDelegate で上記のふたつのメソッドを実装し、true を返します。

状況に応じて false を返すことで、保存されたり復元されたりするのを抑制できます。典型的には、保存された時と復元される時でアプリケーションのバージョンが異なり、うまく復元することができないような場合には、復元を諦めて false を返すことになります。そのような目的のために利用できる UIApplicationStateRestorationBundleVersionKey 定数があり、coder.decodeObjectForKey(UIApplicationStateRestorationBundleVersionKey) as? String の結果を Info.plistCFBundleVersion と比較することで、復元するか判断できます。

optional func application(_ application: UIApplication, willEncodeRestorableStateWithCoder coder: NSCoder)
optional func application(_ application: UIApplication, didDecodeRestorableStateWithCoder coder: NSCoder)

また上記のメソッドで、アプリケーション全体にわたる状態を保存・復元できます。

UIViewController, UIView

UIViewControllerUIView は具体的な保存と復元を担います。保存と復元の対象にするためには、インスタンスvar restorationIdentifier: String? プロパティに値を入れる必要があります。コード上で設定することも Storyboard 上で設定することもできます。

f:id:cockscomb:20160330163542p:plain:w260:h210

Storyboard から restoration identifier を設定する

このとき view controller のヒエラルキーの中で、UIWindow の root view controller から復元したい view controller インスタンスに至るまでの全ての上位の要素にも restorationIdentifier が設定されていなければなりません。途中で restorationIdentifiernil の要素があると、それ以降は復元の対象になりません。view の復元には、その view が保持されている view controller が復元できる必要があります。

また container view controller から見たとき、child view controller の restorationIdentifier はそれぞれユニークである必要があります。UINavigationController では child view controller に、位置を元にした識別子を付加していることがドキュメントで示されています。

UIViewController

restorationIdentifier が設定された view controller は、以下のふたつのメソッドで状態の保存と復元を行います。

func encodeRestorableStateWithCoder(_ coder: NSCoder)
func decodeRestorableStateWithCoder(_ coder: NSCoder)

つまり状態の保存や復元の対象とできるのは NSCoding プロトコルに適合したオブジェクトやプリミティブな値ということになります。

またアプリケーション全体の復元が終わった後に以下のメソッドが呼ばれます。

func applicationFinishedRestoringState()

View controller が Storyboard から生成される場合はこれで十分です。もし view controller をコードだけで生成しているような場合には、これに加えてもうひとつ準備が必要です。UIViewController.restorationClassUIViewControllerRestoration を実装したクラスを設定するか(典型的には view controller のクラス自身で実装する)、 UIApplicationDelegate.application(_:viewControllerWithRestorationIdentifierPath:coder:) を実装するかです。どちらにしても restorationIdentifier の値の配列と NSCoderインスタンスから view controller のインスタンスを生成して返すことになります。UIViewControllerRestoration の典型的な実装を以下に示しておきます。

class ViewController: UIViewController, UIViewControllerRestoration {

    override func viewDidLoad() {
        super.viewDidLoad()

        restorationIdentifier = "ViewController"
        restorationClass = ViewController.self
    }

    static func viewControllerWithRestorationIdentifierPath(identifierComponents: [AnyObject], coder: NSCoder) -> UIViewController? {
        return ViewController()
    }

}

UIView

View controller と同様に UIViewrestoratoinIdentifier を持つ場合に以下のメソッドが呼ばれます。

func encodeRestorableStateWithCoder(_ coder: NSCoder)
func decodeRestorableStateWithCoder(_ coder: NSCoder)

独自の view を作った際に、多くの場合は view controller から値を再設定される方が好ましいと言えますが、例えばユーザーの入力値を保持しておくといった機能を持っているなら、これらのメソッドで復元可能にするのがよいでしょう。

UIKit が提供する view のうち、UICollectionViewUIImageViewUIScrollViewUITableViewUITextFieldUITextViewUIWebView はデフォルトで復元機能を持っていることがドキュメントで示されています。さらに UICollectionViewUITableView のそれぞれの data source が UIDataSourceModelAssociation プロトコルを実装している場合に、要素の選択などといった状態を復元することができます。

復元のライフサイクル

よく注意しなければならないのは、復元されるタイミングです。多くの場合、新たに登場するライフサイクルの流れが、state restoration を実装する上での大きな障害に感じられるでしょう。

アプリケーション全体の中では、UIApplicationDelegateapplication(_:willFinishLaunchingWithOptions:) の後、application(_:didFinishLaunchingWithOptions:) の前に復元が行われます。application(_:didFinishLaunchingWithOptions:) で行う何らかの処理に依存するべきではありません。

View controller の復元では、当然ですが viewDidLoad() より後に復元が行われます。もし viewDidLoad() で view に値を設定していた場合、復元がうまくいかないことになります。

Container View Controller

Container view controller は、child view controller についても処理しなければなりません。UIKit が提供する container view controller は適切にハンドリングしてくれます。もし自分で container view controller を実装している場合は、child view controller を適切に管理する必要があります。

保存する際には、encodeRestorableStateWithCoder(_:) で child view controller を NSCoderencodeObject(_:forKey:) で保存します。UIViewControllerNSCoding に準拠していることに気がつくことでしょう。

復元は少しわかりにくいですが、Storyboard を利用しているときは、そのまま Storyboard から viewDidLoad() の中などで instantiateViewControllerWithIdentifier(_:) で生成します。Storyboard からではない場合は decodeRestorableStateWithCoder(_:) で取り出すのがよいでしょう。

また必要に応じて restorationIdentifier を調整しなければならないでしょう。

設計

State restoration をあなたのアプリで実装しようとすると、おそらく設計上の多くの問題にぶつかることになるでしょう。典型的な問題についてヒントを示そうと思います。

ディスクキャッシュ・データベース

多くのオブジェクトは、state restoration とともに再生成されればよいでしょう。しかしユーザーの入力や web から取ってきたデータは、どこかに保存しておく必要があります。

State restoration では、復元するオブジェクトが NSCoding に準拠していることを求めます。しかし現実のあなたのクラスの多くは NSCoding に準拠していないことでしょう。もしひとつでも NSCoding に準拠できない stored property を持っていたら、そのクラスは NSCoding に準拠できないかもしれません。そもそも Swift の struct は NSCoding に準拠する方法がありません。

ひとつのやり方はディスク上に保存してしまうことです。そのデータが高々ひとつの画面で表示されるようなもので、他のデータと関連性を持たないような場合、何らかの方法で単純に保存するとうまくいくかもしれません。

もしデータの構造が複雑な場合には、データベースシステムを導入すると簡単です。Core Dataや Realm、あるいは SQLite でもいいでしょう。もちろん他のものでも構いません。データはデータベースに格納してしまって、state restoration ではそのデータのプライマリーキーだけを保存・復元することにします。プライマリーキーは Core Data なら NSManagedObjectID.URIRepresentation() が利用でき、Realm であれば Object.primaryKey() をオーバーライドして指定します。その他のデータベースシステムでも類似の概念を得られるはずです。

データをディスク上に置くことで、state restoration のために保存されるデータの容量を減らすこともできます。

インスタンス管理・UIStateRestoring

State restoration が起きるとき、通常とは異なるフローで view controller が生成されます。例えばひとつ前の view controller から prepareForSegue(_:sender:) で何らかのオブジェクトが渡ってきているとき、state restoration 後にはこれは得られません。データとして state restoration で保存・復元することもできますが、view controller でそれを行うと、本来は同一のインスタンスへの参照だったものが、異なる複数インスタンスができてしまうことになります。

これを避けるために、インスタンス管理を行う方法があります。インスタンス管理の典型例は、シングルトンです。つまりアプリケーション中にインスタンスが単一であることを保証し、例えば static な stored property などでそのインスタンスへの参照を与えるやり方です。しかしシングルトンを乱用するべきとは思えないので、これを少し拡張して DI コンテナに相当する概念を導入することができます。

もう一つの方法として UIStateRestoring プロトコルに準拠させ、UIApplication.registerObjectForStateRestoration(_:restorationIdentifier:) メソッドで登録してしまうやり方があります。この方法では、view controller と独立したオブジェクトを復元できます。このことで view controller を跨ぐようなオブジェクトも復元可能です。

その他

スナップショット

State restoration を実装すると、状態が保存されるのと同時に画面のスナップショットが作成されます。次回の復元時にはこのスナップショットが先に表示されるので、ユーザーはシームレスに復元されたように感じます。しかし何らかの事情でこのスナップショットが不適切であると考えられる場合には、状態の保存中に UIApplicationignoreSnapshotOnNextApplicationLaunch() メソッドを呼び出すことで、スナップショットが表示されるのを抑制できます。

非同期の復元

復元は基本的にメインスレッド上で行われますが、場合によっては他のスレッドを活用して並列に処理したいかもしれません。そういった場合は UIApplicationextendStateRestoration() メソッドcompleteStateRestoration() メソッドが利用できます。前者を呼び出してから、例えば Grand Central Dispatch などを利用して非同期に復元処理を行い、後者のメソッドを呼び出します。このふたつのメソッドは必ずペアで呼び出さなければならないので注意してください。

デバッグ

State restoration のデバッグはやや困難です。メモリが十分あるとき、アプリはバックグラウンドで休眠状態に置かれるため、state restoration が発生しません。かといってマルチタスクスイッチャーから上スワイプなどで削除すると、やはり state restoration が発生しません。いちばん簡単なのは、ホームボタンでホーム画面に戻した後、Xcode などを使ってプロセスを kill することです。ホーム画面に戻る瞬間に状態が保存されているので、次回の起動時に復元が行われます。

さらに詳細にデバッグする必要があるときは、Apple の提供するツールを利用できます。Downloads for Apple Developers から restorationArchiveTool for iOS というのを探してダウンロードすると、保存された状態をコマンドラインで覗き見ることができるツールが得られます。また他にも同梱されている .mobileconfig ファイルを利用することで、デバッグログを出力するようにしたり、あるはマルチタスクスイッチャーでアプリを終了したり復元中にクラッシュしたりしても、次の起動時に復元しようとするように設定できます。

どの画面を復元するか

State restoration は一般に複雑で、全ての画面で実装するのは高コストです。しかし必ずしも全ての画面を復元しなくても問題ありません。ユーザーにとってメリットのある部分だけ復元できるようにしておけばよいのです。restorationIdentifier を設定しなければその view controller は復元されないので、ごく一時的にしか表示されないような画面では復元を諦めることができます。ただしその view controller より下の階層の view controller も全て復元できなくなるので注意してください。

まとめ

State restoration は iOS 6 や iOS 7 で追加された機能です。うまく実装することで、ユーザーはがっかりせずに済み、あなたのアプリを何度も使ってくれることでしょう。しかし実装するのが比較的難しいからか、適切に state restoration されるアプリは少ないです。しかしこれを読んだあなたは state restoration について幅広い知識を得たはずです。どうか安心して仕事に取りかかってください。

もしユーザーがあなたのアプリを1分以上使うなら、state restoration に対応する価値があります。反面で、例えばそのアプリが20秒程度で利用するようなものであれば、state restoration はそれほど役立たないかもしれません。State restoration が難しい場合、例えば検索フォームに履歴が残っているとか、そういった機能でいくらか代替できるかもしれません。いずれにせよ、ユーザーがアプリに戻ってきたときに、なるべく混乱が起きないようにするのが望ましいでしょう。

余談ですが Android においては、画面の遷移に必要な情報が serialize (parcelable) なオブジェクトでやりとりされることや、画面の回転でも画面が再生成されるために、state restoration のような仕組みがフレームワークと密に融合しています。このため多くの場合で、特別なことをしなくても同じような結果になります。

またさらに余談ですが OS X にも iOS と同様の仕組みがあり、アプリを起動し直したときでも前と同じ状態に戻ります。

参考

UIControlにOptionSetTypeで状態を追加する

新成人のみなさん、おめでとうございます。大人になるのは難しいことだけど、自由に自分の人生を歩んでほしいと思います。

UIControlState

UIControlにはvar state: UIControlState { get }というプロパティがある。これはすべてのUIControlの状態を示す。UIControlStateは同時に複数の状態を保持できるbit-maskであり、SwiftにおいてはOptionSetTypeとして表現される。つまり例えば、.Disabledでありかつ.Selectedであることが起こり得る。

struct UIControlState : OptionSetType {
    init(rawValue rawValue: UInt)
    static var Normal: UIControlState { get }
    static var Highlighted: UIControlState { get }
    static var Disabled: UIControlState { get }
    static var Selected: UIControlState { get }
    static var Focused: UIControlState { get }
    static var Application: UIControlState { get }
    static var Reserved: UIControlState { get }
}

UIControlStateの様々な状態

このvar state: UIControlState { get }は、読み込みしかできない。ヘッダには以下のようにある。

could be more than one state (e.g. disabled|selected). synthesized from other flags.

すなわち、var enabled: Boolvar selected: Boolvar highlighted: Boolといったプロパティから合成されることになっている。

新しく状態を作る

ここでUIControlState.Applicationというものがあることに気付いただろうか。ドキュメントには以下のように書かれている。

Additional control-state flags available for application use.

つまりこの.Applicationは、私たちのアプリケーションで自由に使ってよいことになっている。Objective-Cのヘッダを見ると以下のように定義されている。

UIControlStateApplication = 0x00FF0000

Objective-Cで見たUIControlStateApplicationの値

Swiftでは隠蔽されてしまっているが、実際の値は0x00FF0000であり、bit-maskにおいて8 bit分の幅が用意されていることがわかる(いうまでもないが、16進数で二桁分であるから、2進数で表現すれば8桁分となる)。この範囲を自由に割り当てて、新しく状態を増やしてみよう。

一度でも押されたことが記録されるボタンを作る

実際の例を示す。

extension UIControlState {
    static var Visited = UIControlState(rawValue: 1 << 16)
}

class VisitableButton: UIButton {

    override var state: UIControlState {
        if visited {
            return super.state.union(.Visited)
        } else {
            return super.state
        }
    }

    var visited: Bool = false {
        didSet {
            setNeedsDisplay()
            titleLabel?.setNeedsDisplay()
            imageView?.setNeedsDisplay()
        }
    }

}

VisitableButton

このUIButtonを継承したVisitableButtonには、新しくvar visited: Bool というプロパティを追加してある。そしてこのvisitedを元に、var state: UIControlState { get }をオーバーライドして新しい状態であるUIControlState.Visitedを追加する。(visitedが設定された後に表示を更新する必要があるだろうから、didSetでなるべく自身を更新するようにしている。)

UIControlState.Visited1 << 16であり、これは0x00FF0000UIControlState.Applicationの範囲に入っている(これを確かめるにはprint(0x00FF0000 as UInt & (1 << 16) as UInt)とするか、print(UIControlState.Application.contains(.Visited))とする)。オーバーライドしているstateプロパティでは、OptionSetTypeunionメソッドを利用して、元の値との論理和を作っている。

class ViewController: UIViewController {

    @IBOutlet weak var button: VisitableButton! {
        didSet {
            button.setTitleColor(UIColor(red: 0x00 / 0xFF, green: 0x00 / 0xFF, blue: 0xEE / 0xFF, alpha: 1),
                forState: .Normal)
            button.setTitleColor(UIColor(red: 0x55 / 0xFF, green: 0x1A / 0xFF, blue: 0x8B / 0xFF, alpha: 1),
                forState: .Visited)
        }
    }

    @IBAction func visit(sender: VisitableButton) {
        sender.visited = true
    }

}

VisitableButtonを利用するコード例

利用する側は、単にvisitedプロパティを操作するだけでよい。この場合は事前に色を設定している。Webのリンクのように、一度でも訪れたページへのリンクが青から紫になる、というのを模している。

UIButtonsetXxx(_:forState:)といったメソッドは、このようにUIControlState.Applicationの範囲に定義された独自の状態に対しても機能する。すなわち、事前に状態毎に設定しておくだけで、自動的にそれが反映されるのである。

まとめ

UIControlvar state: UIControlState { get }プロパティを、新たな状態を作って拡張する方法を示した。この方法によって、UIControlの状態を統一的に表すことができる。

もし既存のコードで、UIControlやそのサブクラスに何らかの状態を増やしたというつもりで、何度setXxx(_:forState:)のようなメソッドを呼び出していたら、それはおおよそ間違いであるとみられる。

同様にUIControlEventsにも.ApplicationReservedという値があり、独自のイベントを作ることができる。

UIControlEventApplicationReserved = 0x0F000000

UIControlEvents.ApplicationReservedは4 bitの範囲を持つ

参考


詳解 Swift 改訂版

詳解 Swift 改訂版

Swiftを使って型付けされた画面遷移がしたい

皆さん今朝はどんな夢をみましたか。今日は私の初夢を紹介します。

prepareForSegue

iOSアプリを開発していて、特にStoryboardを利用して遷移を作るとき、往々にしてそのコードは不完全になる。

ViewControllerの遷移をするときには、次に表示されるViewControllerに必要な情報を受け渡して初期化しなければならない。Storyboardを使った遷移は、すでにインスタンス化されたViewControllerオブジェクトに対して行うことになる。このためViewControllerのイニシャライザを利用できない。従ってUIViewControllerprepareForSegue(_:sender:)メソッドをオーバーライドして、UIStoryboardSegueオブジェクトのdestinationViewController: UIViewController propertyから、次に表示される予定のViewControllerオブジェクトにアクセスして、必要と思われる情報を渡す。これが一般的な実装である。

destinationViewControllerを初期化する

destinationViewController propertyは単にUIViewController型であるから、ここでは目的の型にキャストする必要があるだろう。そしてインスタンス化されたオブジェクトに対して、必ず必要なだけの情報を渡さなければならない。この際に型による保護は得られない。遷移元のViewControllerが遷移先のViewControllerについてよく知っている必要がある。例えば後から必要な情報が増えたときには、コンパイルは通るが実行時にエラーになる、といった状態が発生しやすい。あるいはsegueの接続が変わった場合に、もともと想定していた初期化が行われずに実行時エラーが起きることが頻繁にある。もちろんこれらの問題が気付かれぬままリリースされることはないだろうが、それでも型による保護がないというのは不便である。

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if let toViewController = segue.destinationViewController as? ToViewController {
        toViewController.entry = Entry(title: "Title")
    } else {
        fatalError()
    }
}

典型的な prepareForSegue の実装では、遷移先のViewControllerの型をキャストし、必要な情報を受け渡す

ViewControllerの初期化

そもそもViewControllerの初期化について考えておく必要がある。インスタンスは、本来であればイニシャライザで初期化されるべきであるが、Storyboardを使う以上それは叶わない(Nibであればイニシャライザを利用することもできる)。仮にイニシャライザが利用できる場合、そのシグネチャそのものが初期化に必要な情報を表すことになる。Storyboardによってインスタンス化されるViewControllerにおいても、どのように初期化されるべきか知っているのはそのViewController自身である方が好ましい。

ところで遷移を表すUIStoryboardSegueは、3つの情報を持っている。var identifier: String? { get }var sourceViewController: UIViewController { get }var destinationViewController: UIViewController { get }である。identifierは一意にsegueを指す。同じsegueは必ず同じ型のsourceViewControllerdestinationViewControllerを持つ。このことを利用して、この情報の組を型として表現することができるのではないか。

SegueInitiator

試しにこの遷移の情報を元にViewControllerを初期化する型を作ってみる。

protocol SegueInitiator: class {
    static var identifier: String? { get }
    func prepare(segue: UIStoryboardSegue)
}

segueのidentifierに対応して、prepare(_:)メソッドを持つというSegueInitiator型を定義した。

extension SegueInitiator {
    static func match(segue: UIStoryboardSegue) -> Bool {
        return identifier == segue.identifier
    }
    func performFrom(viewController: UIViewController) {
        if let identifier = Self.identifier {
            viewController.performSegueWithIdentifier(identifier, sender: self)
        } else {
            fatalError("can't perform nil identifier segue")
        }
    }
}

ふたつのデフォルト実装も用意しておく。ひとつはUIStoryboardSegueが対応するものかどうか。そしてもうひとつは、UIViewControllerperformSegueWithIdentifier(_:sender:)を呼び出すことができるものである。

あとはこのprotocolに準拠するclassを定義すればよく、特にprepare(_:)メソッドを遷移先のViewControllerに合わせて正しく実装すればよいはずである。しかし実際的にはsegueのsourceViewControllerdestinationViewControllerのそれぞれの型を、より具体的なViewControllerの型に束縛したいはずだ。そこで以下のように具体的な型を持てるprotocolを作って元のprotocolを継承させる。

protocol TypedSegueInitiator: SegueInitiator {
    typealias Source: UIViewController
    typealias Destination: UIViewController
    func prepareWithSource(source: Source, destination: Destination)
}

extension TypedSegueInitiator {
    static func match(segue: UIStoryboardSegue) -> Bool {
        return identifier == segue.identifier &&
            segue.sourceViewController is Source &&
            segue.destinationViewController is Destination
    }
    func prepare(segue: UIStoryboardSegue) {
        if let source = segue.sourceViewController as? Source,
            let destination = segue.destinationViewController as? Destination {
                prepareWithSource(source, destination: destination)
        } else {
            fatalError("Given segue '\(segue.dynamicType) - Identifier: \(segue.identifier ?? ""), Source: \(segue.sourceViewController.dynamicType), Destination: \(segue.destinationViewController.dynamicType)' does not match this SegueInitiator '\(self.dynamicType) - Identifier: \(self.dynamicType.identifier), Source: \(Source.self), Destination: \(Destination.self)'")
        }
    }
}

ここではtypealiasSourceDestinationのふたつのassociated typeを定義した。デフォルト実装でprepare(_:)を実装し、型が合っているか検証した後に、新たなprepareWithSource(_:destination:)メソッドを呼び出すことにする。型が合わない場合にはその時点でわかりやすいメッセージと共に実行時エラーを引き起こす。Storyboard上での設定と実装で定義が合わない場合にはもうどうしようもない。

これらを用いることで以下のように書ける。

struct Entry {
    let title: String
}

class FromViewController: UIViewController {

    @IBAction func executeToSegue(sender: AnyObject) {
        ToSegue(entry: Entry(title: "To Segue")).performFrom(self)
    }

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if let initiator = sender as? SegueInitiator {
            initiator.prepare(segue)
        } else {
            fatalError()
        }
    }

}

class ToViewController: UIViewController {

    @IBOutlet weak var titleLabel: UILabel!

    var entry: Entry!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        titleLabel.text = entry.title
    }
}

class ToSegue: TypedSegueInitiator {
    let entry: Entry

    init(entry: Entry) {
        self.entry = entry
    }

    static let identifier: String? = "ToSegue"
    typealias Source = UIViewController
    typealias Destination = ToViewController
    func prepareWithSource(source: Source, destination: Destination) {
        destination.entry = entry
    }
}

最後のToSegueが、遷移先のViewControllerを実際に初期化する役割を持ったclassである。prepareWithSource(_:destination:)メソッドの実装はごく単純であり、この段階においては型による安全性が保たれている。この初期化オブジェクトはイニシャライザで初期化に必要なインスタンスを受け取ることにしている。初期化オブジェクトのシグネチャが、初期化に必要な情報を表しているのである。

もちろんこのようにしても、ViewControllerを不適切なやり方で初期化しようとすることはできる。しかし原則的にSegueInitiatorを利用して遷移先の初期化を行うというルールを作り、ViewControllerのコードと同じところにSegueInitiatorのコードを置いておくことで、いくらかの助けにはなるだろう。

まとめ

実際に動作するコードをGitHub上に公開している。この例では、例えばUINavigationControllerが間に挟まっているようなケースにも対応したNavigationSegueInitiatorというclassも用意してある。

本記事の内容は筆者が思いつくままに書いてみたという段階であり、実際に試されたことはない。この方法が現実のアプリケーションでうまく働くかは未知数である。

SwiftからUIKitをはじめとしたObjective-C時代のAPIを利用するときに感じるミスマッチは、現段階においては受け入れるしかないものと思われる。この遷移のインターフェースはiOSのUIKitにおいてもOS XのAppKitにおいてもそう変わらない。*1決して悪いとは思わないが、それでもやはりSwiftに期待される安全性からは程遠く感じられる。

次世代のiOSOS Xではフレームワーク側が進歩して、このような問題が一挙に解決されるかもしれない。そういった期待を持ちつつも、いまのところは様々な工夫を試していくのがよいだろう。


詳解 Swift 改訂版

詳解 Swift 改訂版

*1:watchOSのWatchKitでは、contextオブジェクトを返すメソッドを実装すると、contextオブジェクトは遷移先のWKInterfaceControllerのawakeWithContext(_:)メソッドに渡り、ここで自身を適切に初期化するということになっている。

Swift 2 Error Handling in Practice #swift2symposium

Swift 2 から新たに導入されたエラーハンドリングに関する機能を実際のアプリで利用しようとすると、いくつか悩みどころがあることに気付く。これらの問題について議論を深め、実践的な解を求めていきたいと思う。

Which is better? — Optional type or throws

func parseInt(string: String) -> Int?

func parseInt(string: String) throws -> Int

func parseInt(string: String) throws -> Int?

文字列を整数型としてパースする関数のシグネチャ

文字列を整数型にパースする関数があったとして、そのシグネチャはどのようにあるべきだろうか。明らかに、nil を返すかまたはエラーを throw するような、つまり一番下のシグネチャは不適切である。残るふたつ、文字列が整数としてパースできないとき、nil を返すべきか、あるいは何らかのエラーを throw するべきか。整数に nil という状態はあり得ないので nil が返るのは好ましいとは言えない、文字列を整数値にパースしようとしてできないのは明らかな異常系である、といった理由で throw するべきという意見。あるいは、文字列を整数値にするような取るに足りない処理でいちいちエラーハンドリングを強制するのは過剰である、こういったことでエラーが投げられることは相対的にエラーの重みを軽くしてしまう、などの意見。

func parseJSON(data: NSData) -> JSONObject?

func parseJSON(data: NSData) throws -> JSONObject

func parseJSON(data: NSData) throws -> JSONObject?

バイナリを JSON のオブジェクトとしてパースする関数のシグネチャ

一方で、バイナリを JSON としてパースする関数はどうだろう。これについても一番下のシグネチャは明らかに適切でない。JSON がパースできなかった場合はその原因がわかった方がよいだろうから、throws する真ん中のシグネチャがよいのではないか。

このように、正しい値を返せない場合に nil を返すようにするのか、あるいはエラーを throw するのか、どちらのインターフェースが好ましいのだろう。現実的にはコンテキストに依存することもあるだろうし、現在のところ一定した結論はない。

try? 構文の追加によって、インターフェースのユーザーから見れば throws 関数であってもエラーを無視するのが容易になった。このためインターフェースを作る側は気軽に throws にできるようになったこともまたこの議論に影響する。

ErrorType を丁寧に作る

enum JSONDecodeError: ErrorType {
    case MissingRequiredKey(String)
    case UnexpectedType(key: String, expected: Any.Type, actual: Any.Type)
}

struct JSONObject {
    let raw: [String : AnyObject]
    func getValue<T>(key: String) throws -> T {
        guard let value = raw[key] else {
            throw JSONDecodeError.MissingRequiredKey(key)
        }
        guard let typedValue = value as? T else {
            throw JSONDecodeError.UnexpectedType(key: key, expected: T.self, actual: value.dynamicType)
        }
        return typedValue
    }
}

JSON のオブジェクトから何らかの型の値にマッピングしようというときに投げられるエラー

JSON のオブジェクトから何らかの型の値にマッピングするとき、本来存在するべきキーが存在しなかったり、予期しているものと違う型の値が入っている場合に、それをエラーとしてレポートしたい。どういった ErrorType を定義するのが良いのだろうか。

エラーを見たときすぐに問題に気付けるように、ErrorType は丁寧に作られるべきである。加えて CustomDebugStringConvertible などを実装し、エラーを読みやすくする工夫もあると良いだろう。

enum JSONDecodeError: ErrorType, CustomDebugStringConvertible {
    case MissingRequiredKey(String)
    case UnexpectedType(key: String, expected: Any.Type, actual: Any.Type)

    var debugDescription: String {
        switch self {
        case .MissingRequiredKey(let key):
            return "Required key '\(key)' missing"
        case let .UnexpectedType(key: key, expected: expected, actual: actual):
            return "Unexpected type '\(actual)' was supplied for '\(key): \(expected)'"
        }
    }
}

CustomDebugStringConvertible に準拠することで debugPrint した際の可読性を改善できる

できることは他にもある。エラーを投げる関数の側に、ドキュメンテーションコメントを利用してどのようなエラーが投げられ得るか記述しておくことができる。これはその関数の利用者にとって有益な情報になるはずだ。

struct JSONObject {
    let raw: [NSString : AnyObject]

    /**
    Get typed value for the key

    - parameters:
      - key: JSON key
    - returns: Typed value
    - throws: JSONDecodeError
    */
    func getValue<T>(key: String) throws -> T {
        guard let value = raw[key] else {
            throw JSONDecodeError.MissingRequiredKey(key)
        }
    ...

ドキュメンテーションコメントの throws セクションで投げられるエラーの種類を明示できる

ErrorType と NSError

enum MyError: ErrorType {
    case SuperError
}

let error = MyError.SuperError as NSError

print(error.localizedDescription)
// The operation couldn’t be completed.
       (Module.MyError error 0.)

ErrorTypeNSError 型にキャストすることができる。NSErrorErrorType に準拠することになっているので、この逆のキャストは自然であるが、ErrorType から NSError へのキャストができるのは不自然である。おそらく暗黙的な変換が内部で行われている。

NSErrorErrorType よりも役割の広い型であった。localizedDescriptionlocalizedRecoverySuggestion といった、ユーザーインターフェイスに表示されることを意図した機能が NSError には存在している。ErrorType はそのような機能を規定しておらず、つまり ErrorTypeNSError にキャストした場合には、localizedDescription などに NSError のデフォルトの実装が使われる。しかしデフォルトの実装はユーザーインターフェイスとの連携にあまり適さず、これまでのようにエラーをユーザーインターフェイスに反映させるのが難しくなった。

NSErroriOS 9 や OS X 10.11 から新たに、public class func setUserInfoValueProviderForDomain(errorDomain: String, provider: ((NSError, String) -> AnyObject?)?) というクラスメソッドを持つ。これは localizedDescription などに対応するキーが userInfo 辞書に存在しない場合に利用される。つまり、事前に NSError にユーザーインターフェイス向けの情報が設定されていなくても、オンデマンドにそれらを設定できるのである。しかし、ErrorTypeNSError になったとき(内部的に _SwiftNativeNSError になっていて)、本来 ErrorType であったときに持っていた情報は全て失われ、domaincode だけが残っている。この状態から元のエラーの原因を識別し、意味のある情報を得るのは難しい場合も多い。

これらのことから、ErrorTypeNSError として取り扱うのはあまり筋が良くない。元々持っていた情報や機能が失われてしまう。

議論の結果、例えば ErrorDescriptable のような protocol を定義し、var localizedDescription: String { get } のようなプロパティから情報を得られるようにしておき、ErrorDescriptable の protocol extension に var toNSError: NSError { get } といった機能をつけるのが良いのではないか。これを利用して NSError にキャストせずに直接 NSError を作ることで、有用な情報を保ったまま NSError オブジェクトを得られるのではないか。


Swift の言語機能を実際のソフトウェアの中でどのように活かすかというのは、Swift でソフトウェアを書こうとする私たちが直面する大きなテーマである。エラーハンドリングだけを取り上げたとしても、ここまでに述べたような多様な問題や選択肢が存在している(それに加えて非同期のエラーハンドリングというさらに大きな問題がある)。こういったテーマについて考え、それを実践し、そしてさらなる議論を深めていくことで、Swift 時代における iOS アプリ開発の定石を発見していきたい。

この記事は Swift 2シンポジウム #2 - 2015/08/30(日) - dots. [ドッツ] で発表し、議論した内容を簡単にまとめたものです。

Swift 1.2

第61回 Cocoa勉強会関西で“Swift 1.2 The long-awaited language updates”と題して発表した、Swift 1.2の主だった(おもしろい)変更点の紹介です。

if let

Swift 1.2で最も改善されたのはif文です。if letでOptionalをunwrapできる機能が大きく向上し、複数のOptionalを同時にunwrapできるほか、unwrapされた値について条件を加えることができるようになりました。

例えばcondition: Boolが真でふたつのOptional<Int>nilではなく、大小関係にも条件がある、という条件を表してみます。

Swift 1.1

let condition = true
let aNumber: Int? = 3
let anotherNumber: Int? = 7
if condition {
    if let a = aNumber {
        if let b = anotherNumber {
            if a < b {
                println(a + b)
            }
        }
    }
}

Swift 1.2

let condition = true
let aNumber: Int? = 3
let anotherNumber: Int? = 7
if condition, let a = aNumber, let b = anotherNumber where a < b {
        println(a + b)
}

極端な例ではありますが、Swift 1.1ではifを4回重ねていたものを、Swift 1.2では1つのifで表現できます。

let constant must be initialized before use

let定数は再代入できません。Swift 1.1ではそのために、宣言と同時に値を決める必要がありました。Swift 1.2ではその制限が少し緩和され、値が使われる前に確実に初期化されるなら、宣言の後で値が決まってもよいことになりました。

Swift 1.1

let hour = NSCalendar.currentCalendar().component(.CalendarUnitHour, fromDate: NSDate())

var meridiem: String
if hour < 12 {
    meridiem = "AM"
} else {
    meridiem = "PM"
}

println(meridiem)

Swift 1.2

let hour = NSCalendar.currentCalendar().component(.CalendarUnitHour, fromDate: NSDate())

let meridiem: String
if hour < 12 {
    meridiem = "AM"
} else {
    meridiem = "PM"
}

println(meridiem)

Swift 1.1ではvarにしなければならなかったmeridiemが、Swift 1.2ではletにできます。ifに限らずswitchなども利用できますが、全ての条件で初期化されることが保証されている必要があります。

Set

CollectionTypeArrayDictionaryなどに加えてSetが追加されました。Setは集合を表し、Foundation.frameworkのNSSetに対応します。Setの導入に合わせて、Objective-CでNSSetを利用していたAPISwiftから利用する場合にはSwiftのSetを使うことになります。

Swift 1.1

let abc: NSSet = NSSet(objects: "A", "B", "C")
if NSSet(objects: "A").isSubsetOfSet(abc) {
    println(abc)
}

Swift 1.2

let abc: Set<String> = ["A", "B", "C"]
if abc.isSupersetOf(["A"]) {
    println(abc)
}

NSSetと違って内包する値の型を指定でき、またArrayリテラルで表現できます。

Static methods and properties

Swift 1.2からclassにもstaticな関数やプロパティを持てるようになりました。これまでもclass funcがありましたが、static funcclassかつfinalという意味になります。またstatic letで宣言した定数は最初にアクセスされた時点で評価されるという特徴を持ちます。

class StaticPropertiesAndMethods {
    static func printDate() {
        println(date)
    }
    static let date = NSDate()
}

StaticPropertiesAndMethods.printDate() // => "2015-04-10 07:01:00 +0000"

クラス変数はObjective-Cには存在せず代替的な手法で実現していましたが、Swiftでついに用意された文法と言えます。

Non-Void return types in Void contexts

Swift 1.1でクロージャを使うときに、返り値としてVoidを要求するところに1行だけ、Voidではない値を返す式があると、コンパイルエラーになっていました。これはクロージャが1行だけの式を持つとき、その式の結果がクロージャ全体の返り値として取り扱われ、型がミスマッチだと判定される結果です。このために無意味なreturnを付け加えるなどして回避していました。

Swift 1.2ではこのようなときにVoidではない値が返っても許容されるようになりました。これで意味上は不要なreturnを書く必要がなくなり、すっきりします。

Swift 1.1

let wantsVoid: (() -> Void) = {
    "non-Void here"
    return
}

Swift 1.2

let wantsVoid: (() -> Void) = {
    "non-Void here"
}

zip

Swift 1.2ではzip関数が導入されました。ふたつのSequenceTypeの組み合わせを簡単に作ることができます。要素数が異なる場合には短い方に合わせられます。

let ordinals = ["first", "second", "third"]
let values = [1, 2, 3]

var ordinalsDict: [String: Int] = [:]

for (key, value) in zip(ordinals, values) {
    ordinalsDict[key] = value
}

println(ordinalsDict) // => ["first": 1, "second": 2, "third": 3]

Type casting

キャスト関連でも非互換な変更などがあります。

let any: AnyObject = 3
let number: Int = any as! Int

危険な操作であるときの一貫性のため、ダウンキャストするときas!を使うことになりました。

protocol SomeProtocol {
    func something() -> String
}

class SomeClass: SomeProtocol {
    func something() -> String {
        return "Something awesome"
    }
}

let some: AnyObject = SomeClass()
if some is SomeProtocol {
    println((some as! SomeProtocol).something())
}

@objcではない通常のprotocolにもキャスト関連の操作ができるようになっています。

@noescape

関数が引数としてクロージャを受け取るとき、クロージャの内部で利用される変数はキャプチャされ、それぞれ強参照された状態となります。またこのためにクロージャの外のオブジェクトのプロパティなどを利用する場合にはself.で修飾する必要があります。これによってクロージャを実際に評価するタイミングはいつでもよいことになっています。

しかしクロージャを受け取ってすぐに評価することが決まっている関数の場合はどうでしょう。そういうときはキャプチャする必要が無いし、self.も不要です。キャプチャしない方がパフォーマンス上でも有利でしょうし、最適化しやすいかもしれません。このようにすぐに評価されることがわかっているクロージャのために追加された新しいアノテーション@noescapeです。

func map<U>(f: @noescape (T) -> U) -> U?

Optionalmapでは上記のように@noescapeされています。このためクロージャで使う変数がプロパティであってもself.は不要です。

このようにSwiftではこれらの挙動をescapeという言葉で表しています。

func map<U>(transform: (T) -> U) -> [U]

ところでArraymapを見てみると、上記のように@noescapeアノテーションがないことが分かります。一貫性がないように思われるかもしれませんが、これには理由があるように思えます。lazy()関数などを用いて遅延評価する場合にはescapeせざるを得ません。これは簡単な実験で確かめられます。

let start = NSDate()
let array = Array(0..<3)

let result = lazy(array).map { (number: Int) -> Int in sleep(1); return 100 * number }

println(NSDate().timeIntervalSinceDate(start))

このようにlazy()関数を使えばmap中でsleep(1)していても一瞬で実行が終わり、result[2]などresultの要素にアクセスしたとき初めてmapクロージャが評価されます。このlazy()を消すと、このスニペットは少なくとも3秒の実行時間を必要とします。

これがArray.mapがescapeを必要とする理由です。

この@noescapeが追加されたのに合わせてデフォルトでは@autoclosureもescapeされなくなりました。escapeしてほしい場合には@autoclosure(escaping)と書く必要があります。

flatMap

新しいflatMap関数を使うとmapではできないちょっとしたことが簡単にできるようになります。

let mapped = ["A B C", "D E", "F"].map {
    $0.componentsSeparatedByString(" ")
}
println(mapped) // => [[A, B, C], [D, E], [F]]

let flatMapped = ["A B C", "D E", "F"].flatMap {
    $0.componentsSeparatedByString(" ")
}
println(flatMapped) // => [A, B, C, D, E, F]

上記の例のように、Arrayの要素に何らかの変換を行うとき、flatMapなら要素毎に新たなArrayを返しても2次のArrayになりません。

func toInt(value: String) -> Int? {
    return value.toInt()
}

let one: String? = "1"

let mapOne = one.map { toInt($0) }
println(mapOne) // => Optional(Optional(1))

let flatMapOne = one.flatMap { toInt($0) }
println(flatMapOne) // => Optional(1)

また上記の例では、OptionalmapflatMapの違いを示しています。mapの中でさらにOptionalな値を返すとOptional<Optional<Int>>を得ることになります。flatMapではOptional<Int>です。これは非常に有用であることがわかるでしょう。

このようにflatMapmapとよく似ていますが、最初のArrayと要素数が変わったArrayが返ったり、多重のOptionalを避けられたりといった特徴があります。

let intLike = ["12.3", "45", "6-7-8"]
let results = intLike.flatMap {
    $0.toInt().map { [ $0 ] } ?? []
}

println(results) // => [45]

上記はこれの応用例で、ふつうのmapであればArray<Int?>が返るところを、nilを除去してArray<Int>にしています。

NSEnumerator.generate() -> NSFastGenerator

Swift 1.2ではFoundation.frameworkにも少し拡張が増えています。

let fonts = "/System/Library/Fonts"
let files = NSFileManager().enumeratorAtPath(fonts)!
for path in files {
    println(path)
}

enumeratorAtPathNSDirectoryEnumeratorというNSEnumeratorのサブクラスを返します。Swift 1.2より前では、NSEnumeratorSequenceTypeではなく、for inが使えませんでした。Swift 1.2からはgenerator()メソッドが加わり、protocol SequenceTypeを満たすようになったためfor inできるようになりました。

Swiftはこのように、Foundation.frameworkについてSwiftから利用しやすいように多少の拡張を加えており、今後もこれらが拡充されていく可能性があります。

その他の非互換な変更

Remove implicit conversions from NSString/NSArray/NSDictionary to String/Array/Dictionary

NSStringなどのFoundation.frameworkのオブジェクトから対応するSwiftStringなどへの暗黙的な変換が行われなくなりました。明示的にas Stringなどと書いてキャストしておく必要があります。逆のStringからNSStringといった変換はこれまで通り暗黙的に行われます。

SwiftからObjective-Cのヘッダを見たときにはNSStringStringになっており、Swiftからはほとんど常にStringを利用していることになるので、実際的にこれが問題になるケースは少ないでしょう。ただしCoreFoundation.frameworkに関するオブジェクトを利用するときは別です。

countElements → count

素数を数える関数の名前が変わりました。分かりやすいですね。

パフォーマンスに関する変更

Swift 1.2と共に様々なパフォーマンスが改善されました。Swiftで書かれたプログラムのパフォーマンスが改善されたほかにも興味深い変更があります。

Incremental build

Swiftコンパイルがインクリメンタルになりました。これまでは、ひとつのファイルを変更したときにも全てのファイルのコンパイルをやり直す必要がありましたが、これからは必要最低限のファイルをコンパイルし直すだけでよくなりました。ただし今後も、変更したファイルだけをコンパイルし直すのではなく、変更が影響するであろうファイル全てをコンパイルし直す必要があります。

Swift 1.2でも思ったよりコンパイルが遅いということがあるかもしれませんが、もしかするとXcode 6.3.1の問題かもしれません。

Whole Module Optimization

オブジェクト指向パラダイムにおいて、クラスを継承してメソッドをオーバーライドするというのがあります。すなわち、メソッドを呼び出す際にはオーバーライドされた後の実装を呼び出す必要があるということです。プログラムの実行中、メソッドの呼び出しやプロパティを操作する時に、実際に呼び出すべき対象を動的に解決します。これをdynamic dicpatchと呼んでいます。

ここで、例えばfinal修飾されているメソッドは、ぜったいにオーバーライドできません。そういった場合にはdynamic dispatchする必要がないので、Swiftコンパイラは静的に解決してくれます。dynamic dispatchが行われない分、実行時のパフォーマンスは改善します。

さらにアクセス制御のprivateで修飾されていれば、そのファイルの外からはオーバーライドできません。つまりそのファイル中でオーバーライドされているか検査することで不要なdynamic dispatchをしないようにコンパイルできます。private修飾はパフォーマンスにも役立っていることがわかります。

さらにinternalでもこれを可能にするのがWhole Module Optimizationです。internalで修飾されていればそのモジュールの外からオーバーライドされません。つまり、モジュール全体を検査することで、不要なdynamic dispatchを避けることができます。この最適化は比較的時間がかかるため、現在のところオプションになっています。

これらの話題はSwiftのブログにも書かれています。

Objective-CのNullability

Swift 1.2に合わせてObjective-Cにも変更があります。変数や引数、返り値などがnilを取り得るかをアノテートできるようになりました。nullablenonnullで修飾することでこれが可能です。また全てにこれらのアノテーションを付けて回るのは現実的ではないので、NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_ENDで囲われている部分はnonnullということになっています。基本的にはこれで囲んでおいて、nullableだけ明示的に書くのがよいでしょう。

これらのアノテーションを書いておくと、Swiftから見たときにImplicitly Unwrapped OptionalではなくOptionalかそうでないかはっきりします。さらにObjective-Cにおいても、nonnullとアノテートされたポインタにnilを入れようとしていれば静的解析で検出されます。ただし実行時には影響しません。


Swiftはまだ変わり続ける生きた言語です。Swift 1.2はSwiftの正式なリリース以来もっとも大きな変化で、開発者にとっても非常に歓迎すべき改善が数多くあります。今後のSwiftの進化に期待しながら、Swift 1.2でバリバリ開発していきたいですね。

詳解 Swift

詳解 Swift


いっしょにSwiftでバリバリ開発しませんか。

iOS/OS X 用ライブラリ UTIKit を OSS でリリースしました

UTI

UTI とは Uniform Type Identifier のことで、iOSOS X の世界で、ファイルなどの種類を表すために用いられる文字列です。

UTI の実例として、画像一般を表す public.image や、そのサブタイプで JPEG 画像を表す public.jpeg などがあります。iOSOS X で利用されるほとんどのファイル形式には対応する UTI があり、例えば Keynote ファイルは com.apple.iwork.keynote.key などとなります。もちろんサードパーティも例外ではなく、Adobe Photoshop のファイルは com.adobe.photoshop-image となり、Microsoft Word の docxorg.openxmlformats.wordprocessingml.document です。

iOSOS Xフレームワークは、ファイルの種類を表すのにこの UTI を利用することが多くあります。例えば Photos.framework から画像データを得たいというとき、PHImageManagerrequestImageDataForAsset(_:options:resultHandler:) メソッドを利用します。この resultHandler((NSData!, String!, UIImageOrientation, [NSObject : AnyObject]!) -> Void)! という型のクロージャで、クロージャの第2引数はドキュメントを読むと dataUTI となっています。すなわち、画像データの種類が UTI として得られます。

ところで、この UTI から拡張子MIME type や拡張子を取得するにはどうすればよいのでしょうか。画像をサーバーにアップロードするときには MIME type が必要かもしれません。例えば public.jpeg だったら image/jpeg という風な対応を丁寧に処理しても良いかもしれませんが、そもそも iOSOS X にはそういった関数が備わっています。func UTTypeCopyPreferredTagWithClass(inUTI: CFString!, inTagClass: CFString!) -> Unmanaged<CFString>! を利用することで UTI から MIME type や拡張子が得られます。

let mime = UTTypeCopyPreferredTagWithClass("public.jpeg", kUTTagClassMIMEType).takeRetainedValue() // => "image/jpeg"

let ext = UTTypeCopyPreferredTagWithClass("public.jpeg", kUTTagClassFilenameExtension).takeRetainedValue() // => "jpeg"

またここでは紹介しませんが、逆に MIME type や拡張子から UTI を得る関数もあります。非常に便利ですが、ごくたまにしか使わないので忘れがちです。筆者は毎回のように忘れて、過去のプロジェクトや IRC のログを検索して思い出します。

UTIKit

ということで、このような UTI に関連した処理を利用しやすくするラッパーライブラリが、UTIKit です。これを利用すると上記の処理が以下のようになります。

UTI("public.jpeg").MIMEType // => "image/jpeg"

UTI("public.jpeg").filenameExtension // => "jpeg"

ごく短くなる上に、インターフェースもわかりやすいですね。

もちろん MIME type や拡張子から UTI を得るのも簡単です。

let jpeg = UTI(MIMEType: "image/jpeg")

let jpeg = UTI(filenameExtension: "jpeg")

Equatable を実装しているためこれらふたつは同値です。

UTI(MIMEType: "image/jpeg") == UTI(filenameExtension: "jpeg") // => true

また、public.jpegpublic.image に従っているので、switch を利用して以下のように書けます。

switch UTI("public.jpeg") {
case UTI("public.image"):
    println("JPEG is a kind of images")
default:
    fatalError("JPEG must be a image")
}

これらのように、UTI が Swift ライクに扱えることがわかります。

ご利用方法

CocoaPods 0.36 以降でインストールできます。Podfile に以下の行を書き足しましょう。

pod "UTIKit"

また Carthage でもインストールできるかもしれません(試してない)。

MIT ライセンスです。どうぞご利用ください。

To Do