読者です 読者をやめる 読者になる 読者になる

cockscomblog?

cockscomb on hatena blog

子供が生まれました

本日午前11時に、僕と妻の子供が生まれました。予定日を10日ほど過ぎた、3,700グラムの男の子です。日付が変わった頃から陣痛がきて、それからあれよあれよという間に産まれていました。今はただ妻と息子の無事をありがたく思います。

結婚してからおよそ1年半になりますが、新しい家族の存在は新鮮で、とても嬉しく、そして自分に息子がいるという事実がまだ不思議です。自覚に欠けているようにも感じますが、子供が僕と妻を少しずつ親にしてくれるのだろうと思っています。

家事にも育児にも仕事にも、ますますがんばって参りたいと思います。

今後ともよろしくおねがいいたします。

年末年始に飲み過ぎないために

諸君は酒が好きだろうか。筆者の場合は少しくらい、好きである。酒を飲んでもいいことなどない。僅かに楽しいような気分になるばかりで、だんだんと頭がボーッとしてきて身体もダルくなり、ひどいときには周囲の人間にまで迷惑をかける。しかし、よく冷えたビールの一口目の、あのなんとも言い難い美味さよ。あのたった一口分の美味さが脳神経に刻みつけられ、繰り返しくりかえし酒を飲んでしまう。なんと愚かなことだろう。

さて本稿では、種々の問題をはらむ飲酒について、特に二つの問題を取り上げ実践的なアドバイスを諸君らに授けたい。筆者のアドバイスは主に Swift によって記述される。

ビール1パイント問題

アイリッシュパブなどに行ってビールを頼むと、「中ジョッキ」のような曖昧な単位でなく、「パイント」という単位で量を指定しなければならない。諸君らも安易に1パイントで頼んでしまって思ったより多くて苦労する、といった経験をしたことがあるかもしれない。

パイントはヤード・ポンド法における体積の単位であり、イギリスでは1パイントがおよそ0.568リットルに相当する。アメリカでは0.473リットルであるが、ビールにおいてはイギリス式のパイントが用いられる。

Swift と Foundation.framework でこれを示すと、以下のようになる。

let onePint = Measurement(value: 1, unit: UnitVolume.imperialPints) // 1.0 pt
onePint.converted(to: .liters) // 0.568261 L

Measurement は Foundation に iOS 10 の世代から追加された、Double 型の値と Unit 型の単位を持った、単位付きの値を表す struct である。体積の単位を表す UnitVolume は抽象クラスである Dimension のサブクラスで、また DimensionUnit のサブクラスである。Unit は単に単位を表し、Dimension はそれに加えて単位変換を行うために baseUnit()UnitConverter が関連づけられている。

これらの仕組みにより、converted(to:) メソッドで単位変換を行うことができる。英国法定標準のパイントである UnitVolume.imperialPints から SI 併用単位の UnitVolume.liters へ変換できる。(ところで litre じゃなく liter なのはアメリカっぽいと思います。)

let halfPint = Measurement(value: 0.5, unit: UnitVolume.imperialPints) // 0.5 pt
halfPint.converted(to: .liters) // 0.2841305 L

(halfPint * 3).converted(to: .liters) // 0.8523915 L

Swift の Foundation インターフェースでは演算子が定義されているので、ハーフパイントのビールを3杯くらい飲めばおおよそいい感じになる、ということがわかる。

結論:ハーフパイントくらいでいい

さらに Measurement では、MeasurementFormatter を使って読みやすい文字列を得られる。

let onePint = Measurement(value: 1, unit: UnitVolume.imperialPints) // 1.0 pt

let formatter = MeasurementFormatter()
formatter.locale = Locale(identifier: "ja-JP")
formatter.unitStyle = .long

formatter.string(from: onePint) // "0.568リットル"

日本酒1合問題

日本酒のラインナップが充実した居酒屋でお気に入りの銘柄を1合注文したとき、後に続くおちょこ追加、おちょこ人数分によって、僅かに舐めるばかりの量しか残らない。諸君らも、このような事例で涙したことは一度や二度では済まないだろう。

1合がいかほどの量か、ビールのように調べてみたいが、生憎 Foundation の UnitVolume には「合」という単位は存在していない。しかし幸いなことに、自分で単位を追加することができる。

extension UnitVolume {
    static var: UnitVolume {
        return UnitVolume(symbol: "合", converter: UnitConverterLinear(coefficient: 0.18039))
    }
}

このように UnitConverterLinear を利用して新しい UnitVolume を作ることができる。UnitConverterLinearUnitConverter 抽象クラスのサブクラスであり、coefficient と constant によって単純な線形の変換を行うことができる。UnitVolumebaseUnit().liters であるから、尺貫法における1合が約0.18039リットルであることにより、このように書けるのである。

let oneGo = Measurement(value: 1, unit: UnitVolume.合) // 1.0 合
oneGo.converted(to: .liters) // 0.18039 L

これによって Measurement において合という単位が利用できるようになり、人数に応じて適切な量について考えられるようになった。

UnitConverter は他にも実装することができ、例えば逆数への変換などが存在し得るだろう。

結論:みんなで飲むなら多めにたのむ

総括

本論により、Foundation の Measurement 関連 API の利用方法を示した。その中で我々は、以下の結論を得た。

  • ハーフパイントくらいでいい
  • みんなで飲むなら多めにたのむ
  • 飲み過ぎないように気をつける

それでは皆さんよいお年を。


本稿の内容は関西モバイルアプリ研究会 #21において行われた筆者の研究発表から、その内容を再録したものである。

これは飲酒を勧めるものではなく、京都市清酒の普及の促進に関する条例とも関係しない。筆者の居住地である日本国では、未成年者飲酒禁止法により満20歳未満の者の飲酒は禁止されている。また酒気帯び及び酒酔い運転は犯罪である。飲酒は健康な成年に限って、自己の自由意志によってのみ行われるべきであり、何人も飲み過ぎてはいけない。

Swift 3 の private と fileprivate

おはようございます。バットマンです。PlayStation VR でバットマン:アーカム VRを遊んだところ、現実とゲームの区別がつかなくなりました。ヘッドセットを外した後も、手がオブジェクトにめり込むんじゃないかと錯覚するようになった。

ところで Swift ピープルの皆さんも、最近はプロジェクトを Swift 3 にマイグレーションしたりして暮らしていらっしゃることかと思います。御多分に洩れず、僕もマイグレーションして暮らしています。いろいろコツも掴めてきたところではありますが、それはともかく、privatefileprivate について割り切れないものを感じている昨今です。

privatefileprivate

SE-0025 Scoped Access Level で Swift 3 に導入された privatefileprivate は、Swift 2 までの private はファイル内で可視であるというのを fileprivate とした上で、定義内で可視であるという private を新たに導入したものである。これによって private の意味が他の言語などと揃い、一方で Swift の extension の仕組みと親和的な fileprivate が残された、ということになる。

Swift 2 から Swift 3 へ自動マイグレータを適用すると、Swift 2 の private はすべて Swift 3 の fileprivate へ書き換えられる。これらは全く同じなので正しい。しかし fileprivate は長ったらしいので、可能なら private へ置き換えていきたい。ひとまずすべて private にしてみて、コンパイルが通るように fileprivate へ戻す、といった方法で可能な限り private にしていく。extension で何らかの protocol に準拠していて、そのために stored property を fileprivate にしなければならない、というようなことがある。

そういうことをしていると、いまこの privatefileprivate の差が、いったい何の意味を持っているのかだんだん分からなくなる。このコードを読むとき、privatefileprivate の違いをどういう風に解釈したらよいのか。

同一ファイル上で extension によって実装を分けている場合、privatefileprivate を明確に分離できない。同一ファイル上の別な classstruct であれば、privatefileprivate の違いが役に立つかもしれない。しかしそれは、そもそも別なファイルに分けておけばよかったのかもしれない。

ということで、privatefileprivate の分離自体は正しいのだけど、実際にはうまく使い分けることが難しく、一筋縄にはいかない。

他方で internalpublicopen は明々白々に使い分けられて便利。

あなたの知らない UIKit の世界 — UITableView に UITextView を置きたい

f:id:cockscomb:20160906232707p:plain:h540:right

UITableView の見た目で様々な要素を表示しつつ、その一部として UITextView を使いたい、という需要があると思う。最も身近な例は標準のメールアプリである。

これは UIKit の API を駆使することで実現可能である。本記事ではこれを実現する方法を通して、あまり知られていないであろう UIKit の機能について紹介する。

UITextView の intrinsic content size

Auto Layout において、UIView は自身が表示されるためにちょうどよいサイズを知っている。具体的には var intrinsicContentSize: CGSize である(Swift 3.0 では property で、Swift 2.3 まではメソッドである)。このサイズは他の NSLayoutConstraint と関連して、デフォルトではプライオリティ 750 で縮みにくく、プライオリティ 250 で広がりにくい。

例えば UIImageView では画像のサイズがこれに相当するし、UILabel では文字列を描画するのに最低限必要なサイズと等しくなるはずである。

ここで UITextView の intrinsic content size はどうかというと、そのままでは widthheight-1CGSize が返る。UITextViewUIScrollView を継承しているので、表示する内容に関わらず、ちょうどよいサイズというものを持たないのである。しかしここで、UIScrollView 由来の var isScrollEnabled: Bool property に false を設定すると、こんどはちょうどよいサイズが返るようになる。

let textView = UITextView()
textView.text = "Think different"

textView.intrinsicContentSize() // {w -1 h -1}

textView.scrollEnabled = false

textView.intrinsicContentSize() // {w 86 h 30}

UITextView の intrinsic content size (Swift 2.3)

この挙動を利用して UITableViewCell の Auto Layout を設定することができる。

ところで UITextViewUIScrollView を継承しているのは正当で、内容となる文字列が非常に長い場合に、表示されていない部分のレイアウトを遅延させることができる。実際に、長大な内容の UITextViewvar contentSize: CGSize を取得すると、場合によっては本来のサイズが返ってこないはずだ。逆に言えばスクロールを無効にした UITextView では、長大な内容を持つ場合に明確なパフォーマンスの低下がみられることに留意するべきである。具体的な仕組みに関しては NSLayoutManager Class Reference の “Noncontiguous Layout” を参照すること。

UITableView の Auto Layout

UITableView の self-sizing cell について、詳細は “Working with Self-Sizing Table View Cells” を参照。

UITextView を置いた UITableViewCell に適切に NSLayoutConstraint を設定する。このとき最小の高さを設定しておく方が良いかもしれない。あとは UITableViewAutomaticDimension を高さとして返す。

UITableViewCell の高さの更新

一度表示された UITextView の内容が変更される場合、単に Auto Layout を利用していれば func invalidateIntrinsicContentSize() の効果で全体が更新されるはずである。一方で UITableViewCell の場合は、高さの再計算が必要であることを UITableView に伝える必要がある。ただし cell をリロードするといったことは避けたい。

UITableView では、func beginUpdates()func endUpdates() を連続して呼び出すことで、 cell の高さを再計算させることができる。これはドキュメンテーションされた挙動である。UITableView Class Reference の beginUpdates() から、discussions 部分を引用する。(強調は筆者によるもの)

Call this method if you want subsequent insertions, deletion, and selection operations (for example, cellForRowAtIndexPath: and indexPathsForVisibleRows) to be animated simultaneously. You can also use this method followed by the endUpdates method to animate the change in the row heights without reloading the cell. This group of methods must conclude with an invocation of endUpdates. These method pairs can be nested. If you do not make the insertion, deletion, and selection calls inside this block, table attributes such as row count might become invalid. You should not call reloadData within the group; if you call this method within the group, you must perform any animations yourself.

このように cell のリロードなしに高さのアニメーションを行えることが明記されている。

これを利用して、var text: String? property を変更したあとや、あるいは var isEditable: Bool が true であればユーザーの入力に合わせて、beginUpdates()endUpdates() を呼び出すことで、Auto Layout と合わせて適切に cell の高さが変化する。

extension ViewController: UITextViewDelegate {

    func textViewDidChange(textView: UITextView) {
        UIView.performWithoutAnimation {
            self.tableView.beginUpdates()
            self.tableView.endUpdates()
        }
    }

}

ユーザーの入力に合わせて高さを更新する。この場合は煩わしいのでアニメーションを無効にしている。(Swift 2.3)

まとめ

UITextView のスクロールを無効にすることで intrinsic content size が変化し Auto Layout でうまく扱えることを示した。これを UITableViewCell に置いて、UITableView の self-sizing cell として利用できることを明らかにした。そして UITableViewbeginUpdates()endUpdates() のドキュメントされた挙動によって、cell の高さを再計算させられることを確認した。

これらの UIKit の挙動はあまり知られていないように思う。ひとつひとつは些細だが、組み合わせることで強力なものとなる。UIKit についての知見を深めることは、すばらしいアプリを開発するための近道である。


以上の内容は8/31の関西モバイルアプリ研究会で発表したものです。

iPhone OS 2.0 とともに UIKit が公開されて、ついには iOS 10 の正式リリースがすぐそこまで迫っている昨今ですが、長年の経験を持つ私たちでさえまだまだ知らないことが山ほどあるように思います。UIKit は非常に巧妙に設計された GUI フレームワークですから、ユーザー体験を改善するために必要な様々な仕掛けが用意されていたりします。

特に近年では、Swift というリッチな言語や、モダンパラダイムやライブラリ、あるいは種々の開発手法などと、モバイルアプリに関する話題には事欠きません。しかしながら初心に立ち返って、慣れ親しんだはずのフレームワークを子細に観察してみると、意外な発見があるかもしれません。今後もライフワークとして学び続けて、API を通して Apple に近づいていきたいと思います。

Swift の Nil Coalescing Operator でコンパイルは遅くなるか

趣味のウェブブラウジングをしていると、Swift の ?? (nil coalescing operator) がコンパイルを遅くするのではないか、といった話題*1を見かけました。この演算子は、左辺の Optional<Wrapped> 型の値が Optional.none である場合に右辺の値を返すというもので、直感的にはこれがコンパイル時間を悪化させるとは思えません。経験から言えば、このようなケースでは大抵やや複雑な型推論が発生しており、それがコンパイル時間に支配的な影響を与えています。そうであるなら、人間が少し工夫して型のコンテキストを与えてやることで、計算機はずっとよいパフォーマンスを発揮できるはずです。

ごく簡単な例で実験してみましょう。以下のコードは、let view: UIView? があるとき、座標系における view のX座標を得ようとするものです。ただし viewnil を取る場合は 0 であることにします(nil coalescing operator を用います)。このとき得られる値 x の型は CGFloat になります。

let x = view?.frame.origin.x ?? 0

このように書いたとき、view?.frame.origin.xOptional<CGFloat> 型であるから、0 という数値リテラルCGFloat 型の値であり、結果として xCGFloat 型である、というような順で型推論は解決されるでしょう。これは実際に少し複雑なので、コンテキストを与えて緩和することができます。

let x: CGFloat = view?.frame.origin.x ?? 0

一例としてはこうです。これは先ほどの例に対して xCGFloat 型であることを明示しただけです。

さて、これが本当にコンパイル速度を改善するのか試験してみましょう。Swift コンパイラに少しオプション(-Xfrontend -debug-time-function-bodies)を渡すことで、関数毎にコンパイル時間を出力することができます。元の話題の記事にあるように、Xcode プラグインの形で公開されているツールを利用することもできます。

筆者がざっくりと試験した結果では、元のような例(testA())では実際にコンパイルに時間を要しました。その他の CGFloat 型であるというコンテキストを与えた場合 if let 文などを利用した場合はほとんど変わらず、その差はありませんでした。この傾向はキャッシュをクリアした上で数度同じ試験を繰り返しても変わりませんでした。(きちんと統計的に検定すれば有意差が得られるはずですが、横着して計算していません。)

また元の話題で遅いとされていたケースも、少し書き換えることで十分早くすることができました。

// 3580.1ms
return CGSize(width: size.width + (rightView?.bounds.width ?? 0) + (leftView?.bounds.width ?? 0) + 22, height: bounds.height)

// 5.5ms
return CGSize(width: size.width + (rightView?.bounds.width ?? 0) as CGFloat + (leftView?.bounds.width ?? 0) as CGFloat + 22, height: bounds.height)

現在のバージョンの Swift コンパイラでは、型推論がやや複雑になった場合に、明確なコンパイル速度の低下が見られ、その場合でもコンテキストを与えることにより型推論の範囲を限定して速度を改善できる、ということが言えるはずです。また nil coalescing operator がそのような状況を生み出しやすい可能性があります。

私見では、コンパイル時間に配慮して nil coalescing operator を使わないという判断をする必要はなく、必要に応じて型のコンテキストを与える程度で十分だと思います。それよりはコードが持つ本来の意図がより明確になるように記述するのがよいでしょう。

こちらからは以上になります。

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 と同様の仕組みがあり、アプリを起動し直したときでも前と同じ状態に戻ります。

参考