cockscomblog?

cockscomb on hatena blog

CoreImage.CIFilterBuiltins

iOSmacOSで画像を加工しようとした場合、Core Image frameworkを使うのが簡単だ。画像をハードウェアの支援で高速に処理でき、内蔵された様々なフィルターを使うこともできる。Mac OS X 10.4で初めて搭載されて以来、しっかりとアップデートされ続けている。

2019年のiOS 13やmacOS 10.15では、Core Imageの色々なフィルターをSwiftから扱いやすくなった。

CIFilter APIの変遷

最初期は以下のように、Key-Value Codingを活用したAPIだった。もともとCore Image frameworkは、GUIでユーザーに操作させることを想定しており、文字列などから動的に扱えるように作られている。(あるいは組み込みではないフィルターがインストールされている場合もあり、いずれにしてもKVCは有用である。)反面で、フィルター名やフィルターとパラメータの対応に気を配る必要がある。

import CoreImage

let image: CIImage = ...

guard let filter = CIFilter(name: "CIGaussianBlur") else {
    fatalError()
}
filter.setValue(20, forKey: kCIInputRadiusKey)
filter.setValue(image, forKey: kCIInputImageKey)
guard let processed = filter.value(forKey: kCIOutputImageKey) as? CIImage else {
    fatalError()
}

let context = CIContext()
context.createCGImage(processed, from: processed.extent)

iOS 8のタイミングで、KVCではないAPIも整備され、CIFilter.init(name:parameters:)CIImage.outputImageを使って、以下のように書けるようになった。

guard let filter = CIFilter(name: "CIGaussianBlur", parameters: [
    kCIInputRadiusKey: 20,
    kCIInputImageKey: image,
]) else {
    fatalError()
}
guard let processed = filter.outputImage else {
    fatalError()
}

さらにこれのショートカットで、CIImage.applyingFilter(_:parameters:)メソッドを使い、以下のようにも書ける。見た目はかなりよくなったが、ガウシアンぼかしのために"CIGaussianBlur"フィルターを使って、パラメータとしてkCIInputRadiusKeyを与える、ということは知らなければならない。

let processed = image.applyingFilter("CIGaussianBlur", parameters: [
    kCIInputRadiusKey: 20,
])

iOS 10の頃にはCIImage.applyingGaussianBlur(sigma:)メソッドなどが追加され、いくつかの基本的なフィルターが容易に利用可能になった。

let processed = image.applyingGaussianBlur(sigma: 20)

CIFilterBuiltins

iOS 13やmacOS 10.15から、新たにCIFilterにたくさんのクラスメソッドが追加された。このうちCIFilter.gaussianBlur()を使えば、これまでと同じ処理を以下のように書ける。

import CoreImage
import CoreImage.CIFilterBuiltins

let image: CIImage = ...

let filter = CIFilter.gaussianBlur()
filter.radius = 20
filter.inputImage = image
guard let processed = filter.outputImage else {
    fatalError()
}

let context = CIContext()
context.createCGImage(processed, from: processed.extent)

クラスメソッドのシグネチャclass func gaussianBlur() -> CIFilter & CIGaussianBlurになっているので、型を利用して簡単に扱えるようになっている。このようなクラスメソッドが、組み込みのフィルターそれぞれに用意されている。

import CoreImage.CIFIlterBuiltinsを忘れやすいので注意が必要だ。Core Imageのmodulemapをみると、以下のようになっている。

framework module CoreImage [extern_c] {
  umbrella header "CoreImage.h"
  export *
  module * { export * }
  
  explicit module CIFilterBuiltins {
      header "CIFilterBuiltins.h"
      export *
  }
}

まとめ

Core ImageAPIは徐々に改良が加えられている。

ユースケースに応じて異なるAPIを使うのがよいだろう。単純にガウシアンぼかしを掛けたいのであれば、CIImage.applyingGaussianBlur(sigma:)が最も簡単である。こういったショートカットが用意されていない組み込みのフィルターを利用するなら、CIFilterBuiltinsがよい。組み込みではないフィルターを利用するような場合は、必然的にCIImage.applyingFilter(_:parameters:)メソッドやKVCを使うことになる。

Chromeが影響を受けたmacOS Catalinaの「Hiragino Kaku Gothic ProN」フォントファミリーに関する変更

macOS Catalinaにアップグレードすると、Google Chromeのフォントの表示が変わる、ということがあった。CSSでフォントファミリーとして「Hiragino Kaku Gothic ProN」を指定していても、この指定が効かずにフォールバックされてしまうというものである。macOS Catalinaから、「Hiragino Kaku Gothic ProN(ヒラギノ角ゴ ProN)」ファミリーが削除され、「Hiragino Sans(ヒラギノ角ゴシック)」だけになったのが原因とされている。

同僚のid:polamjagが教えてくれたのだが、Chromiumのbug trackerにissueが登録されている。

内容を読むと、単に「Hiragino Kaku Gothic ProN」ファミリーが削除されたというだけでなく、NSFontManager APIの呼び出し方によっても結果が異なるようだ。

NSFontManager.availableFontFamiliesの結果が「Hiragino Kaku Gothic ProN」を含まなくなり、NSFontManager.availableMembers(ofFontFamily:)を呼べば「Hiragino Kaku Gothic ProN」が見つかる。

import Cocoa

let hiraginoKakuGothicProN = "Hiragino Kaku Gothic ProN"

let fontManager = NSFontManager.shared

print(fontManager.availableFontFamilies.filter { $0 == hiraginoKakuGothicProN }) // []

if let hiragino = fontManager.availableMembers(ofFontFamily: hiraginoKakuGothicProN) {
    print(hiragino) // [[HiraKakuProN-W3, W3, 4, 0], [HiraKakuProN-W6, W6, 8, 2]]
}

NSFontManager.availableMembers(ofFontFamily:)でフォントが見つかるのは、Appleが互換性を保とうとしたからだろう。

Chromeはフォントファミリーをcase insensitiveで見つけるため、敢えてNSFontManager.availableFontFamiliesの方を使っていたということだったが、修正コミットとなるTest for font family on mac using NSFontManager::availableFontsForFamilyの該当部分をみると、NSFontManager.availableMembers(ofFontFamily:)を呼び出すようになった。説明によれば、NSFontManager APIは、case insensitiveにフォントを見つけてくれるようだ。

あなたの知らない 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 に近づいていきたいと思います。

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

参考

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 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でバリバリ開発しませんか。