以下のブログ記事の翻訳です1。
Dependency Injection Framework for Swift - Simple Weather App Example with Swinject Part 2/2
前回のブログ記事では、シンプルな天気アプリのモデル部分を実装し、dependency injection (依存性の注入) とSwinjectを利用して、密結合になった依存性を排除する方法について学びました。密結合だった部分を疎結合にすることにより、ユニットテストが書きやすくなることがわかりました。今回の記事ではアプリのUI部分を実装し、疎結合にしたコンポーネントをSwinjectでどのように接続していくか見ていきます。
このブログ記事で使用するソースコードはGitHubのリポジトリからダウンロードできます。
UIの基本構成
最初に、テーブルビューで天気情報を表示するための基本的なUI構成を作っていきます。UIのコンポーネントはストーリーボードからインスタンス化しますが、ストーリーボード自体のインスタンス化は手で書いて、UIStoryboard
を継承したSwinjectStoryboard
を使えるようにします。
Info.plist
を開いて"Main storyboard file base name"
キーを削除してください。もし生のキーを表示している状態であれば、"UIMainStoryboardFile"
と表示されているかもしれません。
ViewController.swift
を削除してWeatherTableViewController.swift
を追加してください。その中身は空のWeatherTableViewController
の定義があるだけとします。後でdependency injectionパターンを用いてこのクラスを実装します。
WeatherTableViewController.swift
import UIKit
class WeatherTableViewController: UITableViewController {
}
Main.storyboard
を開いて、デフォルトで作成されているビューコントローラを削除してください。その後、オブジェクトライブラリから新しくナビゲーションコントローラをストーリーボードに追加してください。
そのナビゲーションコントローラを選択し、アトリビュートインスペクタで"Is Initial View Controller"
をチェックしてください。
ナビゲーションコントローラのルートビューコントローラとなっているテーブルビューコントローラを選択し、そのカスタムクラスをWeatherTableViewController
に設定してください。そのテーブルビューのプロトタイプセルを選択し、そのスタイルをRight Detail
に変更し、Identifierを"Cell"
に設定してください。テーブルビューコントローラのナビゲーションアイテムを選択し、そのタイトルを"Weather Now"
に設定してください。
AppDelegate.swift
を修正し、イニシャルビューコントローラをストーリーボードから手動でインスタンス化するように書き換えます。ここで、UIStoryboard
の代わりにSwinjectStoryboard
を使用し、後でdependency injectionを追加できるようにしておきます。SwinjectStoryboard
のインスタンス化にはイニシャライザを使わず、create
関数を使用します2。
AppDelegate.swift
import UIKit
import Swinject
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(
application: UIApplication,
didFinishLaunchingWithOptions
launchOptions: [NSObject: AnyObject]?) -> Bool
{
let window = UIWindow(frame: UIScreen.mainScreen().bounds)
window.backgroundColor = UIColor.whiteColor()
window.makeKeyAndVisible()
self.window = window
let storyboard = SwinjectStoryboard.create(name: "Main", bundle: nil)
window.rootViewController = storyboard.instantiateInitialViewController()
return true
}
...
}
これでアプリを実行する準備ができました。Command-R
を入力して実行しましょう。下の画像のような空のテーブルビューが表示されると思います。
ビューコントローラへのDependency Injection
それでは、空っぽだったテーブルビューコントローラを実装し、dependency injectionを追加しましょう。
WeatherTableViewController
にweatherFetcher
プロパティを追加します。
WeatherTableViewController.swift
class WeatherTableViewController: UITableViewController {
var weatherFetcher: WeatherFetcher?
}
AppDelegate
を修正し、設定を済ませたContainer
をSwinjectStoryboard
に渡してインスタンス化するようにしましょう。
AppDelegate.swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(
application: UIApplication,
didFinishLaunchingWithOptions
launchOptions: [NSObject: AnyObject]?) -> Bool
{
let window = UIWindow(frame: UIScreen.mainScreen().bounds)
window.backgroundColor = UIColor.whiteColor()
window.makeKeyAndVisible()
self.window = window
let container = createContainer()
let storyboard = SwinjectStoryboard.create(
name: "Main",
bundle: nil,
container: container)
window.rootViewController = storyboard.instantiateInitialViewController()
return true
}
private func createContainer() -> Container {
let container = Container()
container.registerForStoryboard(WeatherTableViewController.self) { r, c in
c.weatherFetcher = r.resolve(WeatherFetcher.self)
}
container.register(Networking.self) { _ in Network() }
container.register(WeatherFetcher.self) { r in
WeatherFetcher(networking: r.resolve(Networking.self)!)
}
return container
}
...
}
createContainer
メソッドの中で、最初にContainer
のインスタンスを生成し、次にその設定をしています。ビューコントローラの依存関係の設定のため、registerForStoryboard
を使用しています。ここでは、依存関係が解決されたWeatherFetcher
のインスタンスでweatherFetcher
プロパティがセットされるように設定しています。この方法はプロパティ注入 (property injection) と呼ばれます。前回のブログ記事で定義したNetworking
プロトコルは、Alamofireをカプセル化しているNetwork
のインスタンスになるように設定しています。WeatherFetcher
は、Networking
の該当するインスタンスをイニシャライズ時に受け取るように設定しています。これはイニシャライザ注入 (initializer injection) と呼ばれます。最後に、createContainer
メソッドは設定を済ませたcontainer
を返しています。
application:didFinishLaunchingWithOptions:
メソッドの中で、上記のように設定したcontainer
をSwinjectStoryboard
のイニシャライザに渡しています。以上です。シンプルな使い方ですね。Container
を設定して渡すだけでいいのです。
それでは次に、WeatherTableViewController
の実装に取り掛かりましょう。
WeatherTableViewController.swift
class WeatherTableViewController: UITableViewController {
var weatherFetcher: WeatherFetcher?
private var cities = [City]() {
didSet {
tableView.reloadData()
}
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
weatherFetcher?.fetch {
if let cities = $0 {
self.cities = cities
}
else {
// Show an error message.
}
}
}
// MARK: UITableViewDataSource
override func tableView(
tableView: UITableView,
numberOfRowsInSection section: Int) -> Int
{
return cities.count
}
override func tableView(
tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCellWithIdentifier(
"Cell", forIndexPath: indexPath)
let city = cities[indexPath.row]
cell.textLabel?.text = city.name
cell.detailTextLabel?.text = city.weather
return cell
}
}
最初に、cities
プロパティを追加し、City
の空配列で初期化しています。そのプロパティをセットした時にテーブルビューが更新されるようにしてあります。
次に、viewWillAppear
をオーバーライドし、fetch
メソッドで天気情報の取得を開始するようにしています3。fetch
は引数としてクロージャをとり、天気情報を取得できた時にCity
の配列を渡してそのクロージャを実行します。そのクロージャの中では、self.cities
がセットされ、その結果テーブルビューが更新されます。fetch
が失敗した場合には、nil
をクロージャに渡します。このブログ記事ではエラー処理は省略してありますが、GitHubのレポジトリにあるソースコードではエラーメッセージを表示するように実装してありますのでご確認ください。
最後に、tableView:numberOfRowsInSection:
を実装して行数を返し、tableView:cellForRowAtIndexPath:
を実装してセルに都市名と天気をセットするようにしています。
これでUIの実装が完了しました。アプリを実行してみましょう。現在の天気情報がテーブルビューに表示されましたね。
ビューコントローラのテスト
ここまででアプリが動作することを見ましたが、WeatherTableViewController
のユニットテストを追加させてください。ビューが表示されるタイミングでビューコントローラが天気情報を取得し始めるか確認します。このテストでモックのコンセプトが分かります。
SwinjectSimpleExampleTests
に以下の内容のWeatherTableViewControllerSpec.swift
を追加してください。
WeatherTableViewControllerSpec.swift
import Quick
import Nimble
import Swinject
@testable import SwinjectSimpleExample
class WeatherTableViewControllerSpec: QuickSpec {
class MockNetwork: Networking {
var requestCount = 0
func request(response: NSData? -> ()) {
requestCount++
}
}
override func spec() {
var container: Container!
beforeEach {
container = Container()
container.register(Networking.self) { _ in MockNetwork() }
.inObjectScope(.Container)
container.register(WeatherFetcher.self) { r in
WeatherFetcher(networking: r.resolve(Networking.self)!)
}
container.register(WeatherTableViewController.self) { r in
let controller = WeatherTableViewController()
controller.weatherFetcher = r.resolve(WeatherFetcher.self)
return controller
}
}
it("starts fetching weather information when the view is about appearing.") {
let network = container.resolve(Networking.self) as! MockNetwork
let controller = container.resolve(WeatherTableViewController.self)!
expect(network.requestCount) == 0
controller.viewWillAppear(true)
expect(network.requestCount).toEventually(equal(1))
}
}
}
最初に、Networking
のモックとしてMockNetwork
を定義しています。request
メソッドを実装していますが、レスポンスは返していません。その代わり、requestCount
という名前のカウンタをインクリメントしています。モックは、あるインスタンスのメソッドやプロパティが正しく呼ばれているか確認するために使われます。スタブのようにダミーのデータを返すこともできますが、メソッドやプロパティの呼び出しをチェックできるところがスタブと異なります。
spec
については、まず先にit
の中を見てみましょう。MockNetwork
とWeatherTableViewController
のインスタンスを設定済みのcontainer
から取得しています。Networking
はMockNetwork
に結び付けられていることをプログラマの私達は知っているので、container
から返ってきたインスタンスをMockNetwork
にキャストしています。次に、ビューコントローラのviewWillAppear
が呼ばれた後で、モックのrequest
メソッドが1回だけ呼び出されることをrequestCount
カウンタによってチェックしています。WeatherTableViewController
はNetworking
のインスタンスを直接保持しているわけではありませんが、モックのメソッド呼び出しを確認することにより、関連するインスタンスが正しく接続されていることが確認できます。
それでは、container
の設定部分に戻りましょう。1番目に、Networking
がMockNetwork
に決定されるよう設定し、container
の中でそのインスタンスが共有されるように設定しています。このようにオブジェクトスコープを設定することにより、カウンタをチェックするためのMockNetwork
インスタンスが、WeatherTableViewController
によって間接的に保持されているインスタンスと同一になることを保証できます。2番目に、WeatherFetcher
の依存性をイニシャライザ注入するように設定しています。3番目に、WeatherTableViewController
の依存性をプロパティ注入するように設定しています。
それではユニットテストを実行してみましょう。パスしましたね。たとえばの話ですが、この天気アプリの開発を続け、機能をどんどん追加していくと考えてみてください。このユニットテストがあれば、UIとモデルの接続を誤って壊してしまう心配がなくなります。
まとめ
シンプルな天気アプリのUI部分を実装し、dependency injectionのコンテナを使ってどのようにコンポーネントを結びつけていくか学びました。SwinjectStoryboard
があると、ストーリーボードで定義されたビューコントローラに依存性を簡単に注入できることがわかりました。最後に、モックを利用して、依存関係の末端にあるインスタンスのメソッド呼び出しをチェックすることにより、意図したとおりに各コンポーネントが接続されていると確認できることがわかりました。
次回の記事では、MVVMアーキテクチャを用いてより大きな例題アプリを開発します。現在人気でエレガントなSwiftのリアクティブプログラミングフレームワークであるReactiveCococaを使用します。もちろん、dependency injectionとSwinjectを活用してMVVMの各コンポーネントを疎結合なまま結びつけていきます。