Dependency Injection in MVVM Architecture with ReactiveCocoa Part 5: 非同期での画像の読み込み

以下のブログ記事の翻訳です。

[Dependency Injection in MVVM Architecture with ReactiveCocoa Part 5: Asynchronous Image Load](https://yoichitgy.github.io/post/dependency-injection-in-mvvm-architecture-with-reactivecocoa-part-5-asynchronous-image-load/)

* * *

[前回の記事](http://qiita.com/yoichitgy/items/2857b98807e6c4b3fec1)までで、MVVMアーキテクチャに基いて、[Pixabay](https://pixabay.com/)のサーバから返された画像のメタデータを表示するところまで例題アプリを実装しました。今回のブログ記事では、非同期で画像を読み込む機能を追加します。非同期のイベントハンドリングのため、これまでと同様に[ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa)を使用します。この開発を通して、ユニットテストとDependency InjectionをアップデートしながらMVVMアーキテクチャで機能を追加する方法について学びます。

ソースコードはGitHubのリポジトリからダウンロードできます。

- [SwinjectMVVMExample](https://github.com/Swinject/SwinjectMVVMExample): 発展形を含むプロジェクト
- [SwinjectMVVMExample_ForBlog](https://github.com/yoichitgy/SwinjectMVVMExample_ForBlog): (Xcodeや外部フレームワークの更新を除き) ブログ記事に沿った説明のための簡略化したプロジェクト

## Model

最初に、画像をリクエストする機能をModelに追加します。`Networking`プロトコルに`requestImage`メソッドを追加してください。このメソッドは画像のURLを引数にとり、画像をイベントとして送る`SignalProducer`を返します。

**Networking.swift**

    import ReactiveCocoa

    public protocol Networking {
        // 省略

        func requestImage(url: String) -> SignalProducer<UIImage, NetworkError>
    }

`requestImage`メソッドを実装するように`Network`クラスを修正しましょう。このメソッドの中では、`SignalProducer`のイニシャライザに与えるトレーリングクロージャを使い、Alamofireからの非同期なレスポンスを`ReactiveCocoa`の`Signal`に変換しています。Alamofireからのレスポンスが成功して正常なデータを受け取った場合、`.Next`と`.Completed`イベントを`observer`に送出します。それ以外の場合は`.Failed`イベントを送出します。Alamofireのデフォルトではメインスレッドでレスポンスを返すため、Alamofireにシリアルキューを渡してバックグラウンドでレスポンスを返すようにしています。

**Network.swift**

```
import ReactiveCocoa
import Alamofire

public final class Network: Networking {
    private let queue = dispatch_queue_create(
        "SwinjectMMVMExample.ExampleModel.Network.Queue",
        DISPATCH_QUEUE_SERIAL)

    // 省略

    public func requestImage(url: String) -> SignalProducer<UIImage, NetworkError> {
        return SignalProducer { observer, disposable in
            let serializer = Alamofire.Request.dataResponseSerializer()
            Alamofire.request(.GET, url)
                .response(queue: self.queue, responseSerializer: serializer) {
                    response in
                    switch response.result {
                    case .Success(let data):
                        guard let image = UIImage(data: data) else {
                            observer.sendFailed(.IncorrectDataReturned)
                            return
                        }
                        observer.sendNext(image)
                        observer.sendCompleted()
                    case .Failure(let error):
                        observer.sendFailed(NetworkError(error: error))
                    }
            }
        }
    }
}
```

`Networking`プロトコルに`requestImage`メソッドを追加したので、`ImageSearchSpec`で使っているスタブを更新します。そのユニットテストでは特に`requestImage`メソッドを使っていないので、空の`SignalProducer`をスタブが返すようにしています。

**ImageSearchSpec.swift**

    import Quick
    import Nimble
    import ReactiveCocoa
    @testable import ExampleModel

    class ImageSearchSpec: QuickSpec {
        // MARK: Stub
        class GoodStubNetwork: Networking {
            // 省略

            func requestImage(url: String) -> SignalProducer<UIImage, NetworkError> {
                return SignalProducer.empty
            }
        }

        class BadStubNetwork: Networking {
            // 省略

            func requestImage(url: String) -> SignalProducer<UIImage, NetworkError> {
                return SignalProducer.empty
            }
        }

        class ErrorStubNetwork: Networking {
            // 省略

            func requestImage(url: String) -> SignalProducer<UIImage, NetworkError> {
                return SignalProducer.empty
            }
        }

        // 省略
    }

それでは、`requestImage`メソッドをチェックするためのユニットテストを`NetworkSpec`に追加しましょう。テストのための安定したサーバとして[httpbin.org](http://httpbin.org)を使います。

**NetworkSpec.swift**

    import Quick
    import Nimble
    @testable import ExampleModel

    class NetworkSpec: QuickSpec {
        override func spec() {
            var network: Network!
            beforeEach {
                network = Network()
            }

            // 省略

            describe("Image") {
                it("eventually gets an image.") {
                    var image: UIImage?
                    network.requestImage("https://httpbin.org/image/jpeg")
                        .on(next: { image = $0 })
                        .start()

                    expect(image).toEventuallyNot(beNil(), timeout: 5)
                }
                it("eventually gets an error if incorrect data for an image is returned.") {
                    var error: NetworkError?
                    network.requestImage("https://httpbin.org/get")
                        .on(failed: { error = $0 })
                        .start()

                    expect(error).toEventually(
                        equal(NetworkError.IncorrectDataReturned), timeout: 5)
                }
                it("eventually gets an error if the network has a problem.") {
                    var error: NetworkError? = nil
                    network.requestImage("https://not.existing.server.comm/image/jpeg")
                        .on(failed: { error = $0 })
                        .start()

                    expect(error).toEventually(
                        equal(NetworkError.NotReachedServer), timeout: 5)
                }
            }
        }
    }

1番目のテストでは、成功するケースとして`Network`が非同期に画像を返すことを確認しています。2番目のテストでは、サーバから画像でないデータが返ってきた場合に`Network`が`NetworkError.IncorrectDataReturned`エラーを送出することを確認しています。3番目のテストでは、Alamofireからのエラーが対応する`NetworkError`に変換されて`Network`から送出されることを確認しています。

`Command-U`を入力してテストを実行しましょう。

## ViewModel

それでは、Modelから画像を受け取りViewのためにハンドリングするViewModelへと移りましょう。始めに、`ExampleViewModel`グループに以下の内容の`RACUtil.swift`を追加してください。その際、`ExampleViewModel`ターゲットにファイルを追加するよう注意してください。

**RACUtil.swift**

    import Foundation
    import ReactiveCocoa

    internal extension NSObject {
        internal var racutil_willDeallocProducer: SignalProducer<(), NoError>  {
            return self.rac_willDeallocSignal()
                .toSignalProducer()
                .map { _ in }
                .flatMapError { _ in SignalProducer(value: ()) }
        }
    }

`NSObject`のエクステンションの中で`rac_willDeallocSignal`を変換し、オブジェクトが破棄されたタイミングで空のタプルを送出する`SignalProducer`を簡単に作れるようにしています。このようにエクステンションを追加したのは、ReactiveCocoaのSwift APIではObjective-C APIにある`rac_willDeallocSignal`に相当するエクステンションがまだないためです。`toSignalProducer`によりObjective-Cの`Signal`をSwiftの`SignalProducer`に変換し、`map`と`flatMapError`でイベントとエラーの型を変換しています。

`ImageSearchTableViewCellModeling`プロトコルと`ImageSearchTableViewCellModel`クラスに`getPreviewImage`メソッドを以下のように追加してください。

**ImageSearchTableViewCellModeling.swift**

    import ReactiveCocoa

    public protocol ImageSearchTableViewCellModeling {
        var id: UInt64 { get }
        var pageImageSizeText: String { get }
        var tagText: String { get }

        func getPreviewImage() -> SignalProducer<UIImage?, NoError>
    }

**ImageSearchTableViewCellModel.swift**

    import ReactiveCocoa
    import ExampleModel

    public final class ImageSearchTableViewCellModel
        : NSObject, ImageSearchTableViewCellModeling
    {
        public let id: UInt64
        public let pageImageSizeText: String
        public let tagText: String

        private let network: Networking
        private let previewURL: String
        private var previewImage: UIImage?

        internal init(image: ImageEntity, network: Networking) {
            id = image.id
            pageImageSizeText = "\(image.pageImageWidth) x \(image.pageImageHeight)"
            tagText = image.tags.joinWithSeparator(", ")

            self.network = network
            previewURL = image.previewURL

            super.init()
        }

        public func getPreviewImage() -> SignalProducer<UIImage?, NoError> {
            if let previewImage = self.previewImage {
                return SignalProducer(value: previewImage).observeOn(UIScheduler())
            }
            else {
                let imageProducer = network.requestImage(previewURL)
                    .takeUntil(self.racutil_willDeallocProducer)
                    .on(next: { self.previewImage = $0 })
                    .map { $0 as UIImage? }
                    .flatMapError { _ in SignalProducer<UIImage?, NoError>(value: nil) }

                return SignalProducer(value: nil)
                    .concat(imageProducer)
                    .observeOn(UIScheduler())
            }
        }
    }

`getPreviewImage`メソッドは`UIImage`を送出する`SignalProducer`のインスタンスを返します。画像のキャッシュが`previewImage`プロパティに存在したらキャッシュ画像を使う`SignalProducer`で、そうでなければ`Networking`に画像をリクエストする`SignalProducer`となっています。

後者の場合の`SignalProducer`は`concat`で繋がれた2つの部分で構成されています。1つ目は、すぐに`nil`を送出して終了する`SignalProducer(value: nil)`です。最初に`nil`を送出するのは、再利用されたセルの`UIImageView`に表示されている古い画像を取り除くためです。2つ目は、`Networking`に画像をリクエストする`imageProducer`です。ここでは、テーブルビューの各セルでエラーメッセージを表示すべきではないので、`flatMapError`を用いてエラーを`nil`に変換して無視しています。`ImageSearchTableViewCellModel`のインスタンスが破棄された時に`SignalProducer`を停止するため、`racutil_willDeallocProducer`を引数にして`takeUntil`メソッドを使っています。`NSObject`のエクステンションで定義したメソッドを使うため、`ImageSearchTableViewCellModel`は`NSObject`を継承しています[^訳注1]。

以下のように`Networking`を渡すことができるよう`ImageSearchTableViewModel`を修正してください。`Networking`のインスタンスを注入できるようにイニシャライザに引数を追加しています。`startSearch`メソッドの中で、`Networking`のインスタンスを`ImageSearchTableViewCellModel`のイニシャライザに渡します。

**ImageSearchTableViewModel.swift**

```
import ReactiveCocoa
import ExampleModel

public final class ImageSearchTableViewModel: ImageSearchTableViewModeling {
    public var cellModels: AnyProperty<[ImageSearchTableViewCellModeling]> {
        return AnyProperty(_cellModels)
    }
    private let _cellModels = MutableProperty<[ImageSearchTableViewCellModeling]>([])
    private let imageSearch: ImageSearching
    private let network: Networking

    public init(imageSearch: ImageSearching, network: Networking) {
        self.imageSearch = imageSearch
        self.network = network
    }

    public func startSearch() {
        imageSearch.searchImages()
            .map { response in
                response.images.map {
                    ImageSearchTableViewCellModel(image: $0, network: self.network)
                        as ImageSearchTableViewCellModeling
                }
            }
            .observeOn(UIScheduler())
            .on(next: { cellModels in
                self._cellModels.value = cellModels
            })
            .start()
    }
}
```

最後に、Dependency Injectionを追加するために`AppDelegate`を修正します。以下のように`Networking`を`ImageSearchTableViewModel`に注入してください。

**AppDelegate.swift**

    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
        var window: UIWindow?
        let container = Container() { container in
            // Models
            container.register(Networking.self) { _ in Network() }
            container.register(ImageSearching.self) { r in
                ImageSearch(network: r.resolve(Networking.self)!)
            }

            // View models
            container.register(ImageSearchTableViewModeling.self) { r in
                ImageSearchTableViewModel(
                    imageSearch: r.resolve(ImageSearching.self)!,
                    network: r.resolve(Networking.self)!)
            }

            // Views
            container.registerForStoryboard(ImageSearchTableViewController.self) { r, c in
                c.viewModel = r.resolve(ImageSearchTableViewModeling.self)!
            }
        }

        // 省略
    }

それでは、ViewModelのアップデートに合わせてユニットテストを修正・追加しましょう。最初に`ImageSearchTableViewModelSpec`を修正し、`StubNetwork`を追加します。そのインスタンスは、`ImageSearchTableViewModel`の修正したイニシャライザに渡すために使います。

**ImageSearchTableViewModelSpec.swift**

    class ImageSearchTableViewModelSpec: QuickSpec {
        // MARK: Stub
        class StubImageSearch: ImageSearching {
            func searchImages() -> SignalProducer<ResponseEntity, NetworkError> {
                return SignalProducer { observer, disposable in
                    observer.sendNext(dummyResponse)
                    observer.sendCompleted()
                }
                .observeOn(QueueScheduler())
            }
        }

        class StubNetwork: Networking {
            func requestJSON(url: String, parameters: [String : AnyObject]?)
                -> SignalProducer<AnyObject, NetworkError>
            {
                return SignalProducer.empty
            }

            func requestImage(url: String) -> SignalProducer<UIImage, NetworkError> {
                return SignalProducer.empty
            }
        }

        // MARK: Spec
        override func spec() {
            var viewModel: ImageSearchTableViewModel!
            beforeEach {
                viewModel = ImageSearchTableViewModel(
                    imageSearch: StubImageSearch(),
                    network: StubNetwork())
            }

            // 省略
        }
    }

後で使うため、`DummyResponse.swift`にダミーの画像のインスタンスを追加してください。

**DummyResponse.swift**

    let image1x1: UIImage = {
        UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), true, 0)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image
    }()

以下のようにスタブとユニットテストを`ImageSearchTableViewCellModelSpec`に追加してください。

**ImageSearchTableViewCellModelSpec.swift**

    import Foundation
    import Quick
    import Nimble
    import ReactiveCocoa
    @testable import ExampleModel
    @testable import ExampleViewModel

    class ImageSearchTableViewCellModelSpec: QuickSpec {
        // MARK: Stubs
        class StubNetwork: Networking {
            func requestJSON(url: String, parameters: [String : AnyObject]?)
                -> SignalProducer<AnyObject, NetworkError>
            {
                return SignalProducer.empty
            }

            func requestImage(url: String) -> SignalProducer<UIImage, NetworkError> {
                return SignalProducer(value: image1x1).observeOn(QueueScheduler())
            }
        }

        class ErrorStubNetwork: Networking {
            func requestJSON(url: String, parameters: [String : AnyObject]?)
                -> SignalProducer<AnyObject, NetworkError>
            {
                return SignalProducer.empty
            }

            func requestImage(url: String) -> SignalProducer<UIImage, NetworkError> {
                return SignalProducer(error: .NotConnectedToInternet)
            }
        }

        // MARK: Spec
        override func spec() {
            var viewModel: ImageSearchTableViewCellModel!
            beforeEach {
                viewModel = ImageSearchTableViewCellModel(
                    image: dummyResponse.images[0],
                    network: StubNetwork())
            }

            describe("Constant values") {
                it("sets id.") {
                    expect(viewModel.id).toEventually(equal(10000))
                }
                it("formats tag and page image size texts.") {
                    expect(viewModel.pageImageSizeText)
                        .toEventually(equal("1000 x 2000"))
                    expect(viewModel.tagText).toEventually(equal("a, b"))
                }
            }
            describe("Preview image") {
                it("returns nil at the first time.") {
                    var image: UIImage? = image1x1
                    viewModel.getPreviewImage()
                        .take(1)
                        .on(next: { image = $0 })
                        .start()

                    expect(image).toEventually(beNil())
                }
                it("eventually returns an image.") {
                    var image: UIImage? = nil
                    viewModel.getPreviewImage()
                        .on(next: { image = $0 })
                        .start()

                    expect(image).toEventuallyNot(beNil())
                }
                it("returns an image on the main thread.") {
                    var onMainThread = false
                    viewModel.getPreviewImage()
                        .skip(1) // Skips the first nil.
                        .on(next: { _ in onMainThread = NSThread.isMainThread() })
                        .start()

                    expect(onMainThread).toEventually(beTrue())
                }
                context("with an image already downloaded") {
                    it("immediately returns the image omitting the first nil.") {
                        var image: UIImage? = nil
                        viewModel.getPreviewImage().start(completed: {
                            viewModel.getPreviewImage()
                                .take(1)
                                .on(next: { image = $0 })
                                .start()
                        })

                        expect(image).toEventuallyNot(beNil())
                    }
                }
                context("on error") {
                    it("returns nil.") {
                        var image: UIImage? = image1x1
                        let viewModel = ImageSearchTableViewCellModel(
                            image: dummyResponse.images[0],
                            network: ErrorStubNetwork())
                        viewModel.getPreviewImage()
                            .skip(1) // Skips the first nil.
                            .on(next: { image = $0 })
                            .start()

                        expect(image).toEventually(beNil())
                    }
                }
            }
        }
    }

`StubNetwork`の`requestImage`メソッドは、先ほどのダミー画像を送出する`SignalProducer`を返します。`ErrorStubNetwork`の同メソッドは、エラーを送出する`SignalProducer`を返します。新しいユニットテストを追加する前に、`spec`をリファクタリングしてあります。既存のテストを`describe("Constant values")`にグルーピングしました。

`describe("Preview image")`グルーピングには、`getPreviewImage`メソッドのための新しいユニットテストを5つ追加しました。1番目のテストでは、`SignalProducerが最初のイベントとして`nil`を送出することを確認しています。2番目のテストでは、成功時のイベントとして画像を送出することを確認しています。3番目のテストでは、画像のイベントをメインスレッドで送出することを確認しています。4番目のテストでは、キャッシュが存在する場合にはキャッシュした画像を即座に送出することを確認しています。このテストは特定の条件下での確認のため、`context`でさらにグループ分けしてあります。5番目のテストでは、`Networking`インスタンスからのエラーを`nil`に変換して送出することを確認しています。このテストも`context`でグループ分けしてあります。

`Command-U`を入力してユニットテストを実行しましょう。それでは、次のセクションに移り、Viewを実装していきます。

## View

始めに、`NSObject`の時と同様に`UITableViewCell`にエクステンションを追加します[^訳注2]。`ExampleView`グルーブに以下の内容の`RACUtil.swift`を追加してください。このエクステンションの中で、ReactiveCocoaのObjective-C APIの`rac_prepareForReuseSignal`をSwiftの型に変換しています。`UITableViewCell`の`prepareForReuse`が呼ばれたタイミングで、空のタプルのイベントが送出されるようになっています。

**RACUtil.swift**

    import UIKit
    import ReactiveCocoa

    internal extension UITableViewCell {
        internal var racutil_prepareForReuseProducer: SignalProducer<(), NoError>  {
            return self.rac_prepareForReuseSignal
                .toSignalProducer()
                .map { _ in }
                .flatMapError { _ in SignalProducer(value: ()) }
        }
    }

次に、`ImageSearchTableViewCell`を修正し、`viewModel`プロパティがセットされた時にイメージビューを更新する処理を入れます。過ぎ去ったRowのための画像で間違ってセルが更新されないよう、セルが他のRowのために再利用されるタイミングで`getPreviewImage`のシグナルを停止するようにしてあります。

**ImageSearchTableViewCell.swift**

    import UIKit
    import ExampleViewModel
    import ReactiveCocoa

    internal final class ImageSearchTableViewCell: UITableViewCell {
        internal var viewModel: ImageSearchTableViewCellModeling? {
            didSet {
                tagLabel.text = viewModel?.tagText
                imageSizeLabel.text = viewModel?.pageImageSizeText

                if let viewModel = viewModel {
                    viewModel.getPreviewImage()
                        .takeUntil(self.racutil_prepareForReuseProducer)
                        .on(next: { self.previewImageView.image = $0 })
                        .start()
                }
                else {
                    previewImageView.image = nil
                }
            }
        }

        @IBOutlet weak var previewImageView: UIImageView!
        @IBOutlet weak var tagLabel: UILabel!
        @IBOutlet weak var imageSizeLabel: UILabel!
    }

`Command-R`を入力してアプリを実行してみましょう。下のように、各イメージビューに画像が表示されていると思います。

![SwinjectMVVMExample Images Displayed in Table View Cells](https://qiita-image-store.s3.amazonaws.com/0/86677/946522bb-a2ac-128b-5191-3ff22893f7b6.png "SwinjectMVVMExampleCellsWithImagesScreenshot.png")

では最後に、`ExampleViewTests`グルーブに以下の内容の`ImageSearchTableViewCellSpec.swift`を追加してください。`ExampleViewTests`ターゲットにファイルを追加するように注意しましょう。

**ImageSearchTableViewCellSpec.swift**

    import Quick
    import Nimble
    import ReactiveCocoa
    import ExampleViewModel
    @testable import ExampleView

    class ImageSearchTableViewCellSpec: QuickSpec {
        class MockViewModel: ImageSearchTableViewCellModeling {
            let id: UInt64 = 0
            let pageImageSizeText = ""
            let tagText = ""

            var getPreviewImageStarted = false

            func getPreviewImage() -> SignalProducer<UIImage?, NoError> {
                return SignalProducer<UIImage?, NoError> { observer, _ in
                    self.getPreviewImageStarted = true
                    observer.sendCompleted()
                }
            }
        }

        override func spec() {
            it("starts getPreviewImage signal producer when its view model is set.") {
                let viewModel = MockViewModel()
                let view = createTableViewCell()

                expect(viewModel.getPreviewImageStarted) == false
                view.viewModel = viewModel
                expect(viewModel.getPreviewImageStarted) == true
            }
        }
    }

    private func createTableViewCell() -> ImageSearchTableViewCell {
        let bundle = NSBundle(forClass: ImageSearchTableViewCell.self)
        let storyboard = UIStoryboard(name: "Main", bundle: bundle)
        let tableViewController = storyboard
            .instantiateViewControllerWithIdentifier("ImageSearchTableViewController")
            as! ImageSearchTableViewController
        return tableViewController.tableView
            .dequeueReusableCellWithIdentifier("ImageSearchTableViewCell")
            as! ImageSearchTableViewCell
    }

このテストでは、`ImageSearchTableViewCell`の`viewModel`プロパティがセットされた時に`getPreviewImage`が呼ばれることを`ImageSearchTableViewCellModeling`のモックを使って確認しています。

`Command-U`を押してテストを実行しましょう。パスしました！これで、ネットワークから非同期で画像を読み込んで表示するテーブルビューを実装し終えました。実装だけでなくユニットテストもあるので、動くソフトウェアを自信を持って開発し続けることができますね！

## まとめ

今回のブログ記事では、画像を`UIImageView`に非同期でロードする機能をMVVMアーキテクチャで実装しました。ユニットテストを更新するとともに、Model、ViewModel、Viewの各プロトコルと実装クラスに新しいメソッドを追加する手順を学びました。実装の際、ViewModelに対するDependency Injectionもアップデートしました。抽象化されたイベントの送出とハンドリングのためにReactiveCocoaをModel、ViewModel、Viewのすべてにおいて用いました。

ブログ記事のシリーズを通して以下のことを学びました。

- Part 1: MVVMとReactiveCocoaのコンセプトと基本
- Part 2: MVVMフレームワークのターゲットで構成されるXcodeプロジェクトの設定と[Carthage](https://github.com/Carthage/Carthage)による外部フレームワークのインストール
- Part 3: ネットワークなどの外部システムからアプリを疎結合にするためにプロトコルを使ったModelの設計
- Part 4: ViewModelとViewの実装および`AppDelegate`からのDependency Injection
- Part 5: 新しい機能を追加するためのModel、ViewModel、Viewの修正とユニットテストの更新

これまで単に例題アプリを実装するだけでなく、MVVMの境界となるプロトコルのスタブやモックを用いたユニットテストも書いてきました[^1]。MVVMアーキテクチャの中でプロトコル、実装、そしてユニットテストを追加するサイクルを維持することにより、プロジェクトを進めていく際の自信を高めることができます。[ReactiveCocoa](https://github.com/Swinject/Swinject)の抽象化されたイベントと[Swinject](https://github.com/Swinject/Swinject)によるDependency InjectionでModel、View、ViewModelを疎結合にすることがそのサイクルの鍵となります[^訳注3]。

これで本シリーズは終了しますが、[GitHubのレポジトリにあるプロジェクト](https://github.com/Swinject/SwinjectMVVMExample)では、画像の詳細ビューの表示、テーブルを下までスクロールした時に追加で画像データを読み込む機能、エラーハンドリング、ローカライゼーションなど、より進んだ開発を行っています。興味があればチェックしてみてください。[GitHub上のSwinjectのプロジェクト](https://github.com/Swinject/Swinject)にスターを付けていただけると励みになります。

* * *

以上でシリーズの翻訳は終了です。

記事の中では、MVVMのすべてのインターフェイスをプロトコルできっちり分けていましたが、あまりやり過ぎると開発のスピードを落としてしまうので、プロジェクトのサイズ (開発者の人数やアプリのサイズ) に合わせて適宜調整するほうがいいかもしれません。特に、UIテストを行っているなら、ViewからViewModelはプロトコルを介さずに実体を参照するようにすれば十分だと思います。ReactiveCocoaの`MutableProperty`を`AnyProperty`プロパティでラップして、外部からは読み取り専用のプロパティを作る方法がありましたが、1人で開発するような小さなプロジェクトであればこれも少し煩雑かもしれません。

MVVMの他にも[VIPER](https://www.objc.io/issues/13-architecture/viper/)などのアーキテクチャがありますが、開発規模に応じたアーキテクチャの選択と実装が必要でしょう。

[^訳注1]: 訳注: ここでは簡便のため`NSObject`を継承するようにしていますが、Swiftだけで同等の事ができるようにするほうがいいかもしれないです。
[^訳注2]: 訳注: `UITableViewCell`のエクステンションはありませんが、UIKitの一部のエクステンションが[Rex](https://github.com/neilpa/Rex)で実装されています。
[^訳注3]: 訳注: 少し言い過ぎかもしれません。もともと英語の文章だったので。
[^1]: このブログ記事では、いつも機能の実装をしたあとでユニットテストを書いていましたが、実際にはテストを先に書くか機能の実装と一緒に書いていくほうが良いプラクティスです。