iOS13ではStoryboardでもDIができる件について


はじめに

これまでの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では、createrというクロージャーを引数として渡すことで、これまで隠蔽されていた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を実装するときにrequiedとして実装が要求されるinitializer
// 利用しないので、fatalError()として実装を省略する
required init?(coder: NSCoder) {
fatalError()
}
}

上記のように定義したinitializerをcreaterのクロージャーで利用します。

let storyboard = UIStoryboard(name: "ViewController", bundle: nil)

let viewController = storyboard.instantiateInitialViewController { coder in
ViewController(coder: coder, dependency: 10)
}

以上のようにすることで、Storyboardで定義したUIViewControllerでもinitializerでのDIが可能です。