Edited at

UIViewControllerからfatalError("init(coder:) has not been implemented") をなくす

UIViewControllerを継承したViewControllerを作成し、独自のinitメソッドを作成した場合、

required init?(coder: NSCoder) の実装を強制されますが、

誰も正しい説明をしていない気がするので書きました。


よくある誤解

required init?(coder: NSCoder) はStoryboardでのみ使われる

というような記述を、色々なサイトで見かけることがあります。

その結果、Storyboardを利用しない場合、

自動生成されたfatalError("init(coder:) has not been implemented")のまま

required init?(coder: NSCoder)メソッドが放置される場合がありますが、それは誤りです。

Storyboard使用の有無に限らずinit?(coder: NSCoder)を使うパターンがあります。

ですのでソースからの生成だろうがinit?(coder: NSCoder)は適切に実装されるべきですし、

Storyboardからの生成の場合、多くはinit?(coder: NSCoder)の記述は省かれますが、

ちゃんとinit?(coder: NSCoder)を用意して内部実装を書かなければならない場合があります。

Storyboardの利用有無と、init?(coder: NSCoder)を実装しなければならないことは無関係です。

NSCoding適合クラスのサブクラスを作って新しいイニシャライザを用意した場合、強制的にrequired init?(coder: NSCoder)の実装を求められるため、

UIViewControllerのサブクラスで独自のイニシャライザを用意した場合も、強制的にrequired init?(coder: NSCoder)の実装を求められます。


init?(coder: NSCoder) が使われるパターン

以下に3つのパターンをあげましたが、これ以外にもあるかもしれませんし、将来使われる可能性もあるかと思います。


Unarchive

例えばお絵描きするViewControllerがあった場合、途中まで絵を描いておいて、

途中の状態をスナップショットとして保存しておきたい、という要望はあるかと思います。

お絵描きViewControllerをNSKeyedArchiverで保存しておいて、

然るべき時にNSKeyedUnarchiverでお絵描きViewControllerを取り出す、という仕組みを考えた場合、

Archiveは見かけ上できるものの、Unarchive時に

fatalError("init(coder:) has not been implemented")が発動します。

実装者としては、UIViewControllerはNSCodingに対応している、というiOSとしての正しい知識のもとスナップショット保存を実装したつもりがコケるわけです。


Restoration

https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/PreservingandRestoringState.html

自分のアプリをホームに戻した後、地図とか重めのアプリを動かして、

いざ自分のアプリに戻ってくると最初の画面からやり直しになる場合がありますよね?

そうしたことを防ぐためにiOSにはリストアの機能が備わっています。

こちらを正しく実装すれば、ユーザーは以前使っていた画面状況から復帰できるようになるんですが、

この復帰の起点は init?(coder: NSCoder) になるので、

リストア対応を下手に行うと、起動するたびに

fatalError("init(coder:) has not been implemented") にぶち当たって

一生起動できないアプリが出来上がります。


Storyboardからの読み込み

Storyboardファイルからの読み込みの場合、ファイルの内容はUINibDecoderというNSCoderで提供されます。

UINibDecoderは非公開クラスなので中身については謎ですが、このCoderをsuper.init(coder: coder) という形でUIViewControllerに渡してあげればStoryboardの内容でViewControllerが構築されるわけです。

ただ、当然そればスーパークラスがUIViewControllerの場合ですので、間にfatalError("init(coder:) has not been implemented") なサブクラスがいるとコケます。


init(coder:) を実装する

公式ドキュメントがあります。(Objective-Cです)

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Archiving/Articles/codingobjects.html#//apple_ref/doc/uid/20000948-97254

UIViewControllerはNSCodingに適合しているので、super.init(coder: coder)を呼んだ後、

サブクラスを動作させるためのパラメータをcoder.decodeXXXで取得する作業を行います。

当然パラメータのエンコード部分も必要になりますので、func encode(with coder: NSCoder)func encodeRestorableState(with coder: NSCoder)でエンコードを追加します。


code

例えば独自パラメータidが必要なViewControllerの場合、init?(coder: NSCoder)でもidの初期化が必要になるかと思いますので、idのデコードおよびエンコード処理を追加しておく必要があります。

class ViewController: UIViewController {

let id:String?
init(id:String?) {
self.id = id
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
self.id = coder.decodeObject(forKey: "id") as? String
super.init(coder: coder)
}

override func encode(with coder: NSCoder) {
super.encode(with: coder)
coder.encode(id, forKey: "id")
}

override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
coder.encode(id, forKey: "id")
}
}


Storyboard

StoryboardでViewControllerの実装を行うとき、prepareForSegue:sender:などでパラメータの設定をすることが多いかと思います。

Storyboardの場合、required init?(coder: NSCoder) を強制実装する必要がないので忘れがちですが、外からパラメータをもらうときは忘れずにエンコードしましょう。

class IBViewController: UIViewController {

var id:String? // prepareForSegue:sender:で設定してもらうパラメータ
required init?(coder: NSCoder) {
self.id = coder.decodeObject(forKey: "id") as? String
super.init(coder: coder)
}

override func encode(with coder: NSCoder) {
super.encode(with: coder)
coder.encode(id, forKey: "id")
}

override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
coder.encode(id, forKey: "id")
}
}


Storyboard for IBSegueAction

IBSegueActionの場合はcoder付きのメソッドが実装できるので、そちらを使います。

つかおう!IBSegueAction!

class IBViewController: UIViewController {

var id:String?
init?(coder: NSCoder,id:String) {
self.id = id
super.init(coder: coder)
}

required init?(coder: NSCoder) {
self.id = coder.decodeObject(forKey: "id") as? String
super.init(coder: coder)
}

override func encode(with coder: NSCoder) {
super.encode(with: coder)
coder.encode(id, forKey: "id")
}

override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
coder.encode(id, forKey: "id")
}
}


まとめ


  • UIViewControllerはConforms To NSCodingなのでUIViewControllerのサブクラスもinit?(coder: NSCoder)を実装している必要があります


  • init?(coder: NSCoder)はStoryboard/Xib専用のメソッドではありません OSがViewControllerをNSCodingに適合しているとみなして呼び出すルートがあります

  • init?(coder: NSCoder)では、エンコードしていたパラメータをデコードする機能を実装します。同時に、initに必要なパラメータ及び保存しておきたいものを各種encodeメソッドでエンコードしておきましょう。