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

cockscomblog?

cockscomb on hatena blog

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

Swift Development

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

さて本稿では、種々の問題をはらむ飲酒について、特に二つの問題を取り上げ実践的なアドバイスを諸君らに授けたい。筆者のアドバイスは主に 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

Swift Development

おはようございます。バットマンです。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 を置きたい

iOS Cocoa Development

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 Development

趣味のウェブブラウジングをしていると、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の画像の位置を変えたい

iOS Cocoa Development

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 の世界

iOS Cocoa Development Swift

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

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

参考

Swift 2.2

Swift

寒さも和らぎ、日によっては春の訪れを感じさせる今日この頃、いかがお過ごしでしょうか。春といえば Swift です。Swift は春と秋に、まるで衣替えのように大きなリリースがあります。2016年の春と予告されていた Swift 2.2 は、おそらく来週には正式にリリースされるものと思われます3月22日にリリースされました

Swift 2.2 は、バグの修正や警告や診断の改善、コンパイル時間や実行速度の向上が主目的であるとされ、それに加えて Swift 2.0 以来のちょっとした機能向上を図ってのリリースとなります。Swift2.2 は OSS となった Swift の初めてのバージョンアップでもあります。すなわちコミュニティからの直接的なフィードバックを経た、最初の Swift と言えるでしょう。そんな Swift 2.2 の変更から主だった(おもしろい)部分を紹介します。

春に備えて準備をはじめましょう!

class の failable initializer でより早く失敗できる

Swift 2.1 では、class の failable initializer (init?()init() throws) に制限がありました。

For classes, however, a failable initializer can trigger an initialization failure only after all stored properties introduced by that class have been set to an initial value and any initializer delegation has taken place.

Swift 2.1 までの The Swift Programming LanguageFailable Initializers に記載されていたのこの制限により、stored property を全て初期化した後じゃなければ return nilthrow ができませんでした。

Swift 2.2 ではこれが解消され、より早いタイミングで失敗できるようになりました。struct など value type ではできていたことではありますが、class で利用できると便利なことが多いでしょう。

Ref.

#if swift(>=2.2)

#ifSwift のバージョンによって分岐させられるようになりました。当てはまらない方はコンパイラに解釈されないので、エラーや警告も発生しません。この機能により、複数Swift のバージョンに対応したコードを書けるようになります。特にライブラリの開発で役に立つでしょう。

#if swift(>=2.2)
    print("Swift 2.2!")
#else
    // コンパイラによって解釈されない
#endif

Swift 2.2 以降で使えることで、例えば Swift 3.0 と 2.2 のふたつのバージョンでコンパイルできるコードが書けるようになります。しかし Swift 3.0 では大きく API が変更される予定なので、分岐によって分けることがどれくらい現実的かは未知数ではあります。

Ref.

関数を参照する際に引数ラベルまで指定できる

Swift では関数を簡単に参照することができますが、引数ラベルの名前が違う以外はすべて同じシグネチャを持つ関数があった場合でも、引数ラベルまで利用して参照できるようになりました。

class NiceClass {
    func awesomeMethod(x: String, some: String) {}
    func awesomeMethod(x: String, another: String) {}
}
let nice = NiceClass()

let some = nice.awesomeMethod(_:some:)
let another = nice.awesomeMethod(_:another:)

この例のように書くことができます。Swift 2.1 までは nice.awesomeMethod としか書けなかったので、このような状況では曖昧さが解決できませんでした。UIView.insertSubview(_:aboveSubview:)UIView.insertSubview(_:belowSubview:) のように、現実的にもこのようなメソッドが存在していたので、嬉しい変更と言えるでしょう。もしオーバーロードによって型が違うだけのシグネチャがあるときは、型コンテキストを作って型推論に頼らずに書くことで、曖昧さが解決できます。

また関数のほかイニシャライザでも同様です。

Ref.

#selector

Selector というのは Objective-Cメソッドを指すデータで、target-action パターンなどの動的なメッセージパッシングに利用されてきました。Swift においても Objective-C の言語機能を利用して設計されたフレームワークを使用する都合上、この selector を作ることができ、Selector 型のインスタンスとして表現されています。SelectorStringLiteralConvertible として定義されていたため、多くの場合は Selector 型のコンテキストで単なる文字列リテラルとして書かれてきました。

Swift 2.2 からは、Selector の作り方が変更され、#selector 式が導入されます。この式の導入により、Swift の関数を参照するように Selector を生成できます。

let sel = #selector(MyViewController.doSomething(_:))

このとき MyViewController.doSomething(_:) は、@objcObjective-C 用の別名をつけられていても関係なく、Swift 内での表現が利用できます。また SE-0021 と合わせて、引数ラベルまで指定できます。さらにオーバーロードによって引数ラベルまで指定しても曖昧な場合は、as を使って関数の型を記述することができ、例えば #selector(MyViewController.doSomething(_:) as (Int) -> Void) のようにも記述できます。

メソッドのレシーバ (例における MyViewController に当たる部分) はクラスでもインスタンスでもよく、あるいは self でも問題ありません。self の場合は省略もできます。

新しい #selector 式では、独自の構文の導入によってコンパイラによるチェックが可能になりました。存在しないメソッドを指そうとしているとコンパイラがエラーを出します。あるいは Objective-C 側から参照できないメソッドの場合にもエラーとなります。このことでより安全に利用できるようになりました。

Ref.

associatedtype

protocol の associated type を作るとき、Swift 2.1 までは typealias キーワードを使うことになっていましたが、Swift 2.2 からは廃止予定となり、新たに associatedtype キーワードを利用することになります。Swift 3.0 からは associated type を作る目的での typealias は廃止されます。

protocol CollectionType : Indexable, SequenceType {
    associatedtype Generator : GeneratorType = IndexingGenerator<Self>
    func generate() -> Self.Generator
    ...
}

typealias キーワードには他にも用途があり、紛らわしいので新たにキーワードが用意された格好です。protocol に準拠する型で associatedtype に対応する型を与えるときは、これまでと変わらず typealias キーワードを利用します。

Ref.

#file, #line, #column, #function

Swift にはソースコードの現在地を示す識別子 (magic identifier) があり、Swift 2.1 までは __FILE__, __LINE__, __COLUMN__, __FUNCTION__ というように、C のマクロと似たフォーマットが利用されてきました。Swift 2.2 からはこれらは廃止予定とされ、それぞれ #file, #line, #column, #function という # でプリフィックスされた新たな識別子が与えられました。Swift 3.0 では旧来のスタイルは完全に廃止されます。

Swift 2.2 以降 Swift 2.1 以前 説明
#file __FILE__ ファイル名
#line __LINE__ 行番号
#column __COLUMN__ 列番号
#function __FUNCTION__ 関数宣言名

ところで、以前からある line control statement としての #line はどうなるのか、気になることかと思います。残念ながら Swift 2.2 の時点では、どちらも #line で表現され、コンテキストによって意味が変わります。

Line control statement では、#line [line-number] [file-name] というフォーマットで #line#file の結果を変えることができ、#line でリセットできます。特にリセットするための #line と、行番号の #line は非常に曖昧です。現在の所、行の最初のトークンとして現れる #line は line control statement として解釈されるようになっています (apple/swift)。

この曖昧な状態は長くは続かず、line control statement を #sourceLocation(file: "foo", line: 42) のようにリネームすることが決定されています。リセットするときは #sourceLocation() となります。余談ですが、当初は #setline となることに決まっていましたが、継続的な議論の末にさらに変更されました (swift-evolution-announce)。

Ref.

Other Improvements

引数ラベルに予約語が使用可能になる

SE-0001

関数の引数ラベルに、これまでは予約語が使用できませんでした。Swift 2.2 ではこの制限が緩和され、inout, var, let を除くほとんどの予約語が使えるようになります。これまでもバッククオート (“`”) を用いることで予約語を使うことができましたが、それが必要なくなりました。

forin など前置詞として使われる単語も予約語として扱われているので、この変更によって書きやすくなる場面があるでしょう。

tuple が比較できるようになる

SE-0015

要素が比較可能なふたつの tuple を比較できるようになります。これは標準ライブラリに tuple の比較演算子が実装されたことによるものです。比較するためには要素数が同じであることと、個々の要素が Equatable であることや Comparable であることが必要です。

ただし tuple の要素数が6以下である必要があります。それ以上の要素数について比較したい場合には、自分で実装を追加しなければなりません。これはそれぞれの要素数の tuple 毎に実装を用意しなければならない都合上、どこかに限度を設ける必要があり、大抵は要素数が6以下であることや標準ライブラリのバイナリサイズの増加分とのバランスにより決定されています。

@objcenum につけられるようになる

Declaration Attributes

If you apply the objc attribute to an enumeration, each enumeration case is exposed to Objective-C code as the concatenation of the enumeration name and the case name. For example, a case named Venus in a Swift Planet enumeration is exposed to Objective-C code as a case named PlanetVenus.

Raw type が Intenum@objc 属性で Objective-C から見えるようになります。Objective-C から見たときは enum の名前と case の名前が結合されます。

Deprecated

Swift 2.2 では、これまで利用できたいくつかの機能が廃止予定になります。Swift という言語をより洗練させるために、非互換であってもきちんと整理していこうという姿勢ですね。これらは Swift 3.0 で実際に削除されます。代替的な書き方が存在するはずなので、順次置き換えていきましょう。

カリー化関数構文

SE-0002

カリー化関数を作るための func curried(x: Int)(y: Int) という構文は、それほど便利に利用されておらず、連鎖的に言語や実装を複雑にしていることから、Swift 3.0 で削除され、Swift 2.2 でも警告されます。

func curried(x: Int)(y: Int) -> Int {
    return x * y
}

func closure(x: Int) -> (y: Int) -> Int {
    return { y in x * y }
}

例のふたつの関数は同等の働きをするので、直接クロージャを用いた下の形式に書き換えることになるでしょう。

関数の引数の var

SE-0003

func increment(var x: Int) -> Int {
    x += 1
    return x
}
increment(1)

関数の引数の var は、便利であることが少なく、inout などとも紛らわしいことから、Swift 3.0 で削除される予定です。Swift 2.2 でも警告されます。

++, -- 演算子

SE-0004

インクリメントやデクリメントのための ++-- 演算子は、前置と後置で返り値が異なるなど複雑であり、また for-in でループすることがふつうである現在となっては利用頻度も低いなどの理由から、Swift 3.0 で削除される予定です。Swift 2.2 でも警告されます。

x += 1x = x.successor() といった代替的な方法に書き換える必要があります。

C スタイルの for ループ

SE-0007

C スタイルの for ループ (for init; comparison; increment {}) は、Swift において使用頻度が非常に低く、簡潔な for-in に較べて複雑であることから、Swift 3.0 で削除され、Swift 2.2 から警告されます。

ほとんどのケースでは for-in で書き換えられ、あるいは while でも同様に記述できるはずです。for-in では収まりの悪いようなケースについては標準ライブラリの改善によって解決される予定であるほか、パフォーマンス上の違いについても最適化によって解決が図られることになっています (swift-evolution-announce)。

?

Optional の Sequence での lazy な flatMap

SE-0008

lazy を使って LazyCollection を作り、それを flatMap した場合について、flatMapOptional を返す場合についても LazyCollection が保たれるべきである (遅延評価されるべきである) という提案です。Optional を返す場合は LazyCollectionTypeflatMap ではなく SequenceTypeflatMap が利用されるためにこの問題が起きているというわけです。

この提案は Swift 2.2 の変更として承認されていますが、実装された様子がありません (SR-361)。

Xcode 7.3

Xcode も合わせてアップデートされます。オートコンプリートが fuzzy な入力にも対応するのは、誰もが嬉しいことでしょう。つまり例えば、UITableViewdequeueReusableCellWithIdentifier(_:forIndexPath:) メソッドを呼び出すのに、deqcellwi などとタイプしてもよい、ということです。これまでは前方一致でしかオートコンプリートされなかったことを考えると、信じられない進歩です。

Interactive Playgrounds によって、Playground で表示されるビューを操作できるようになるのも、とてもおもしろいことです。UI のプロトタイピングにはもちろん、何かを説明するのにも便利そうですね。

また OSS となった Swift の toolchain を利用することもできるようになります。

Xcode のリリースノートを見ると興味深い変更がもう少しあります。

The static analyzer checks for missing localizability. This check is off by default and can be enabled by selecting Yes for “Missing localizability” in the “Static Analyzer – Generic Issues” build settings. (23414217)

デフォルトでは無効になっているものの、NSLocalizedString を使っていないことを静的解析でチェックできるようになっていそうです。

Simulator.app supports delivering touch pressure to iOS and watchOS by using a Force Touch trackpad.

シミュレータ利用時に、Force Touch トラックパッドで 3D touch できるようになりました。

View debugger も全体的に強化されているようです。

Ref.

Xcode Release Notes


Swift 2.2 ではこのように小規模な改善が行われました。それほど大きな変更ではありませんが、しかし現実の問題に対する具体的な解決策と言えるようなものが多いように思われます。これはまさにコミュニティからのフィードバックの効果と言えるでしょう。

これらに加えて、コンパイル時間が改善しパフォーマンスも向上するほか、コンパイラ自体のクラッシュも減り、より正確に警告やエラーを発するようになります。これらが日々の生産性に寄与することに疑いの余地はありません。

そしてもちろん秋には Swift 3.0 が控えています。Swift 3.0 では新しい API ガイドラインによって API のネーミングに大きな変化が起き、まるで別物のように感じられるかもしれません。enum の個々の case が lower case になる (Optional であれば .some.none になる) と知ったら驚くことでしょう (SE-0023)。それ以外にも、ABI の安定に向けて generics を完全にするための議論が始められています (swift-evolution)。Swift 3.0 と共にリリースされる Swift Package Manager は、Xcode のプロジェクトファイルを生成するようになり、これまでより簡単に Swift のライブラリを扱えるようになるでしょう (apple/swift-package-manager, swift-evolution)。そして Swift 3.0 の開発はまだまだ続きます。今はまだわからない、素晴らしい機能が追加されることでしょう。

詳解 Swift 改訂版

詳解 Swift 改訂版