はじめに
これまでのiOS13未満のUIKitのAPIでは、Storyboardで定義したUIViewControllerではinitializerでのDI(Dependency Injection|依存性の注入)ができないという課題がありました。
※initializerでのDIはコンストラクタインジェクションとも呼ばれますが、Swiftでは慣習的にコンストラクタという用語はあまり使わないので、本記事では「initializerでのDI」とします。
UIViewControllerの実装方法とinitializerでのDIの可否をまとめると以下のようになります。
※黒魔術的なライブラリなどを利用しない前提
レイアウト実装方法 | initializer DI可否(〜iOS12) | initializer DI可否(iOS13〜) |
---|---|---|
コードベース | ○ | ○ |
XIB | ○ | ○ |
Storyboard | × | ○ |
iOS13未満での実装方法の詳細については以下の記事で説明しています。
Qiita - iOSとコードベースレイアウト
勿論、プロパティインジェクションならばiOS12以下でもStoryboardで定義したUIViewControllerで可能ですが、initializerでのDIをする設計にすることで以下の恩恵が得られます。
- DIする値をプロパティで保持する場合に、varでなくletで定義できるのでより安全
- DIする値をプロパティで保持する場合に、Optional型でなく非Optional型で定義できるので無駄なUnwrapが不要
- UIViewControllerの生成を行うために、必ずDIが必要になるので、プロパティインジェクションのように設定忘れが起こり得ない
iOS13でStoryboardでイニシャライザでDIする
iOS13では地味にこんなAPIが加わっています。
instantiateInitialViewController(creator:)
func instantiateInitialViewController<ViewController>(creator: ((NSCoder) -> ViewController?)? = nil)
-> ViewController? where ViewController : UIViewController
このAPIでは、creatorというクロージャーを引数として渡すことで、これまで隠蔽されていたStoryboardからのUIViewControllerの生成過程に介入することができます。
以下のようにUIViewControllerの継承クラスで以下のようにNSCoderを引数にとるinitializerを実装します。
class ViewController: UIViewController {
@IBOutlet fileprivate weak var textView: UITextView!
private let dependency: Int
// DI用のinitializer
init?(coder: NSCoder, dependency: Int) {
self.dependency = dependency
super.init(coder: coder)
}
// 独自のinitializerを実装するときにrequiredとして実装が要求されるinitializer
// 利用しないので、fatalError()として実装を省略する
required init?(coder: NSCoder) {
fatalError()
}
}
上記のように定義したinitializerをcreatorのクロージャーで利用します。
let storyboard = UIStoryboard(name: "ViewController", bundle: nil)
let viewController = storyboard.instantiateInitialViewController { coder in
ViewController(coder: coder, dependency: 10)
}
以上のようにすることで、Storyboardで定義したUIViewControllerでもinitializerでのDIが可能です。