LoginSignup
42
37

More than 5 years have passed since last update.

Dependency Injection in MVVM Architecture with ReactiveCocoa Part 4: ViewとViewModelの実装

Last updated at Posted at 2016-01-11

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

Dependency Injection in MVVM Architecture with ReactiveCocoa Part 4: Implementing the View and ViewModel


前回のブログ記事では、例題アプリのモデル部分を設計して実装しました。今回の記事では、ViewとViewModelのパートへと移ります。最初に、ViewとViewModelの空の実装をプロジェクトに追加し、アプリを実行できるようにします。その後、ユニットテストとともに実際の実装を追加します。その開発の中で、ReactiveCocoaのオブザーバブルなプロパティであるAnyProperty型とMutableProperty型の使い方を学べます。

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

ViewとViewModelの設計の概要

ViewとViewModelを疎結合にするため、そのインターフェイスを下の図のようにプロトコルで定義します。ImageSearchTableViewModelingImageSearchTableViewCellModelingはプロトコルで、ImageSearchTableViewModelImageSearchTableViewCellModelはそれらのプロトコルに適合した実装です。ImageSearchTableViewModelはイベントにラップされた画像のエンティティをModel層から受け取り、さらにImageSearchTableViewControllerへとイベントで伝えるためにそのエンティティをImageSearchTableViewCellModelに変換します。

View and View Model Design

ViewとViewModelの空の実装

最初に、SwinjectによるDependency Injectionを利用したViewとViewModelの空の実装を追加します。Agileプラクティスで言われるように、まず中身はないが動くソフトウェアを作り、ひとつずつ機能を追加していきます。

空のViewを追加

始めに、不要なファイルと設定を削除します。SwinjectMVVMExampleグループのViewController.swiftMain.storyboardを削除してください。SwinjectMVVMExampleグループのInfo.plistを開き、Main storyboard file base nameを削除してください。(生のキー/値を表示していればUIMainStoryboardFileと表示されているかもしれません。)

次に、ExampleViewグループに以下の内容のImageSearchTableViewController.swiftを追加してください。その際、ExampleViewターゲットにファイルを追加することに注意してください。そのためには、ExampleViewグループを右クリックしてNew File...を選択し、iOS > Source > Swift Fileと順に選択し、ファイル名を入力する画面でExampleViewターゲットにチェックを入れておきます。

ImageSearchTableViewController.swift

import UIKit

public final class ImageSearchTableViewController: UITableViewController {
}

同様に、ExampleViewグループに以下の内容のImageSearchTableViewCell.swiftを追加してください。

ImageSearchTableViewCell.swift

import UIKit

internal final class ImageSearchTableViewCell: UITableViewCell {
}

ここで、ImageSearchTableViewControllerpublicなのに対し、ImageSearchTableViewCellinternalであることに注意してください。そのセルにアクセスするのはテーブルビューコントローラのみで、ExampleViewフレームワークを使用する側には見せる必要がないためです。

ExampleViewグループにMain.storyboardという名前で新しいストーリーボードを追加してください。そのストーリーボードを開き、Object Libraryからナビゲーションコントローラを追加します。そのナビゲーションコントローラを選択し、Attribute Inspectorの"Is Initial View Controller"にチェックを入れます。

ナビゲーションコントローラのルートビューコントローラとなっているテーブルビューコントローラを選択し、カスタムクラスとストーリーボードIDをImageSearchTableViewControllerにします。そのテーブルビューコントローラのプロトタイプセルを選択し、カスタムクラスとセル識別子をImageSearchTableViewCellにします。テーブルビューコントローラのナビゲーションアイテムを選択し、そのタイトルを"Pixabay Images"にします。

SwinjectMVVMExampleEmptyViewProject

SwinjectMVVMExampleEmptyViewStoryboard

SwinjectMVVMExampleグループのAppDelegate.swiftを修正し、手動でストーリーボードからイニシャルビューコントローラをインスタンス化するようにします。後でDependency Injectionを追加するため、UIStoryboardの代わりにSwinjectStoryboardを使用します。SwinjectStoryboardのインスタンス化はイニシャライザではなくcreate関数で行います1SwinjectStoryboardに与えるバンドルはメインバンドルではなく、NSBundle.init(forClass:)で取得したExampleViewターゲットのバンドルにします。

AppDelegate.swift

import UIKit
import Swinject
import ExampleView

@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 bundle = NSBundle(forClass: ImageSearchTableViewController.self)
        let storyboard = SwinjectStoryboard.create(name: "Main", bundle: bundle)
        window.rootViewController = storyboard.instantiateInitialViewController()

        return true
    }

    // 以下略...
}

これでアプリを実行する準備ができました。Command-Rを押して実行してみましょう。下の画像のような空のテーブルビューが表示されると思います。

SwinjectMVVMExampleEmptyViewScreenshot.png

空のViewModelを追加

ExampleViewModelグループに以下の内容のImageSearchTableViewModeling.swiftImageSearchTableViewModel.swiftを追加してください。ファイルを保存するときにExampleViewModelターゲットに追加するよう注意してください。ImageSearchTableViewModelが依存しているImageSearchingはイニシャライザを通して注入します。これはイニシャライザインジェクションパターンと呼ばれるものです。

ImageSearchTableViewModeling.swift

public protocol ImageSearchTableViewModeling {
}

ImageSearchTableViewModel.swift

import ExampleModel

public final class ImageSearchTableViewModel: ImageSearchTableViewModeling {
    private let imageSearch: ImageSearching

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

ExampleViewModelグループに以下の内容のImageSearchTableViewCellModeling.swiftImageSearchTableViewCellModel.swiftを追加してください。

ImageSearchTableViewCellModeling.swift

public protocol ImageSearchTableViewCellModeling {
}

ImageSearchTableViewCellModel.swift

public final class ImageSearchTableViewCellModel: ImageSearchTableViewCellModeling {
}

ImageSearchTableViewControllerImageSearchTableViewCellが持つViewModelへの依存性をプロパティインジェクションパターンの形で定義します。

ImageSearchTableViewController.swift

import UIKit
import ExampleViewModel

public final class ImageSearchTableViewController: UITableViewController {
    public var viewModel: ImageSearchTableViewModeling?
}

ImageSearchTableViewCell.swift

import UIKit
import ExampleViewModel

internal final class ImageSearchTableViewCell: UITableViewCell {
    internal var viewModel: ImageSearchTableViewCellModeling?
}

Dependency Injectionを適用

AppDelegateでDependency Injectionを行うため、依存関係を登録するcontainerプロパティを以下のように追加します。ここでは、Containerのコンビニエンスイニシャライザを使用し、クロージャの中でcontainerインスタンスの設定をしています。containerプロパティはSwinjectStoryboardをインスタンス化するときに渡します。

AppDelegate.swift

import UIKit
import Swinject
import ExampleModel
import ExampleViewModel
import ExampleView

@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)!)
        }

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

    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 bundle = NSBundle(forClass: ImageSearchTableViewController.self)
        let storyboard = SwinjectStoryboard.create(
            name: "Main",
            bundle: bundle,
            container: container)
        window.rootViewController = storyboard.instantiateInitialViewController()

        return true
    }
    // 以下略...
}

必要な型がすべてcontainerに登録してあることを確認するため、ユニットテストを追加しましょう。テストを追加する前に、SwinjectMVVMExampleTestsグループのSwinjectMVVMExampleTests.swiftは不要なので削除しておきます。その後、同グループに以下の内容でAppDelegateSpec.swiftを追加してください。そのユニットテストでは、登録してある型をcontainerから取得できることを.notTo(beNil())を使って確認しています。

AppDelegateSpec.swift

import Quick
import Nimble
import Swinject
import ExampleModel
import ExampleViewModel
import ExampleView
@testable import SwinjectMVVMExample

class AppDelegateSpec: QuickSpec {
    override func spec() {
        var container: Container!
        beforeEach {
            container = AppDelegate().container
        }

        describe("Container") {
            it("resolves every service type.") {
                // Models
                expect(container.resolve(Networking.self)).notTo(beNil())
                expect(container.resolve(ImageSearching.self)).notTo(beNil())

                // ViewModels
                expect(container.resolve(ImageSearchTableViewModeling.self))
                    .notTo(beNil())
            }
            it("injects view models to views.") {
                let bundle = NSBundle(forClass: ImageSearchTableViewController.self)
                let storyboard = SwinjectStoryboard.create(
                    name: "Main",
                    bundle: bundle,
                    container: container)
                let imageSearchTableViewController = storyboard
                    .instantiateViewControllerWithIdentifier("ImageSearchTableViewController")
                    as! ImageSearchTableViewController

                expect(imageSearchTableViewController.viewModel).notTo(beNil())
            }
        }
    }
}

Command-Uを入力してユニットテストを実行してみましょう。パスしましたね。依存性が注入された状態で、ViewとViewModelの空実装ができました。

ViewとViewModelの実際の実装

このセクションでは、テーブルビューに画像のメタデータ (タグやピクセルサイズ) を表示するための実際の実装をViewとViewModelに追加していきます。テーブルビューセルにUIImageViewを追加しますが、このブログ記事ではメタデータを表示するためのラベルだけ実装することとします。イメージビューは次のブログ記事で使用します。

ViewModelの実装

最初に、テーブルのデータソースを実装しましょう。MVVMアーキテクチャでは、データソースはViewModelで実装します。cellModelsプロパティとstartSearchメソッドをImageSearchTableViewModelingプロトコルに追加してください。

ImageSearchTableViewModeling.swift

import ReactiveCocoa

public protocol ImageSearchTableViewModeling {
    var cellModels: AnyProperty<[ImageSearchTableViewCellModeling]> { get }
    func startSearch()
}

cellModelsプロパティはAnyPropertyとして定義し、オブザーブできるようにします。AnyProperty型が持つSignalProducer型のproducerプロパティにオブザーバを追加することができます。

ImageSearchTableViewModelを修正し、プロトコルに追加したプロパティとメソッドを実装しましょう。

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

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

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

cellModelsプロパティはMutableProperty_cellModelsプロパティをラップしています。これは、MutableProperty型の_cellModelsプロパティが外部から変更されることを防ぐためであり、AnyPropertyでラップすることにより読み取り専用のオブザーバブルなプロパティを実現できます。

startSearchメソッドにより、imageSearch.searchImagesで返されるSignalProducerを開始します。nextイベントの副作用の中で、imageSearchが返すresponseからマッピングしたImageSearchTableViewCellModelingの配列を_cellModelsの値にセットします。その副作用は.observeOn(UIScheduler())によりメインスレッドで実行されていることに注意してください。ViewModelからViewへのイベントがメインスレッドで実行されることをViewModelで保証すべきだからです。

ReactiveCocoaにはメインスレッドで実行するためのスケジューラが2種類あります。ひとつはQueueScheduler.mainQueueSchedulerで、メインスレッドでイベントが実行されるよう常に非同期なスケジューリングが行われます。もうひとつは今回使用したUISchedulerで、すでにメインスレッドの中にいればそのまま同期で実行し、そうでなければ非同期でメインスレッドにスケジューリングします。

次に、idpageImageSizeTexttagTextプロパティをImageSearchTableViewCellModelingプロトコルに追加してください。idはデバッグ用に追加します。

ImageSearchTableViewCellModeling.swift

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

さらに、ImageSearchTableViewCellModelを修正して上記プロトコルを実装し、プロパティがinitの中で初期化されるようにします (initImageSearchTableViewModelが呼びます)。ImageSearchTableViewCellModelが実装するのはビューのロジックで、ModelのデータがどのようにViewに表示されるか定義します。

ImageSearchTableViewCellModel.swift

import ExampleModel

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

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

ViewModelの実装のユニットテスト

ViewModelで実装したことを確認するためのユニットテストを追加しましょう。最初に、ExampleViewModelTests.swiftが不要なのでExampleViewModelTestsグループから削除しましょう。次に、そのグループに以下の内容のDummyResponse.swiftImageSearchTableViewModelSpec.swiftを追加してください。

DummyResponse.swift

@testable import ExampleModel
@testable import ExampleViewModel

let dummyResponse: ResponseEntity = {
    let image0 = ImageEntity(
        id: 10000,
        pageURL: "https://somewhere.com/page0/",
        pageImageWidth: 1000,
        pageImageHeight: 2000,
        previewURL: "https://somewhere.com/preview0.jpg",
        previewWidth: 250,
        previewHeight: 500,
        imageURL: "https://somewhere.com/image0.jpg",
        imageWidth: 100,
        imageHeight: 200,
        viewCount: 99,
        downloadCount: 98,
        likeCount: 97,
        tags: ["a", "b"],
        username: "User0")
    let image1 = ImageEntity(
        id: 10001,
        pageURL: "https://somewhere.com/page1/",
        pageImageWidth: 1500,
        pageImageHeight: 3000,
        previewURL: "https://somewhere.com/preview1.jpg",
        previewWidth: 350,
        previewHeight: 700,
        imageURL: "https://somewhere.com/image1.jpg",
        imageWidth: 150,
        imageHeight: 300,
        viewCount: 123456789,
        downloadCount: 12345678,
        likeCount: 1234567,
        tags: ["x", "y"],
        username: "User1")
    return ResponseEntity(totalCount: 123, images: [image0, image1])
}()

ImageSearchTableViewModelSpec.swift

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

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())
        }
    }

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

        it("eventually sets cellModels property after the search.") {
            var cellModels: [ImageSearchTableViewCellModeling]? = nil
            viewModel.cellModels.producer
                .on(next: { cellModels = $0 })
                .start()
            viewModel.startSearch()

            expect(cellModels).toEventuallyNot(beNil())
            expect(cellModels?.count).toEventually(equal(2))
            expect(cellModels?[0].id).toEventually(equal(10000))
            expect(cellModels?[1].id).toEventually(equal(10001))
        }
        it("sets cellModels property on the main thread.") {
            var onMainThread = false
            viewModel.cellModels.producer
                .on(next: { _ in onMainThread = NSThread.isMainThread() })
                .start()
            viewModel.startSearch()

            expect(onMainThread).toEventually(beTrue())
        }
    }
}

ImageSearchTableViewModelSpecで最初に定義しているスタブは、dummyResponseをイベントとして送ります。1つ目のテストでは、dummyResponseから変換された配列がcellModelsプロパティに非同期にセットされることを確認しています。2つ目のテストでは、イベントがメインスレッドで送られてくることを確認しています。

次に、ExampleViewModelTestsグループにImageSearchTableViewCellSpec.swiftを追加してください。ここでは単純に、値がidプロパティにセットされること、また変換された値がpageImageSizeTextプロパティとtagTextプロパティにセットされることを確認しています。

ImageSearchTableViewCellModelSpec.swift

import Quick
import Nimble
@testable import ExampleModel
@testable import ExampleViewModel

class ImageSearchTableViewCellModelSpec: QuickSpec {
    override func spec() {
        it("sets id.") {
            let viewModel = ImageSearchTableViewCellModel(image: dummyResponse.images[0])

            expect(viewModel.id).toEventually(equal(10000))
        }
        it("formats tag and page image size texts.") {
            let viewModel = ImageSearchTableViewCellModel(image: dummyResponse.images[0])

            expect(viewModel.pageImageSizeText).toEventually(equal("1000 x 2000"))
            expect(viewModel.tagText).toEventually(equal("a, b"))
        }
    }
}

Command-Uを入力してユニットテストを実行してください。パスしましたね。それでは次のセクションに移りましょう。

Viewの実装

このセクションではViewを実装していきます。Main.storyboardを開き、プロトタイプセルにUIImageViewを1つ、UILabelを2つ追加し、好きなようにレイアウトしてください。もしセルの高さを変更したければ、(UITableViewControllerではなく) UITableViewrowHeightプロパティをストーリーボードのSize Inspectorで変更してください。

SwinjectMVVMExampleStoryboardCellLayout

それから、ImageSearchTableViewCellにアウトレットを追加し、ストーリーボードの各アイテムと接続してください。

ImageSearchTableViewCell.swift

import UIKit
import ExampleViewModel

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

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

インスタンスがセットされたタイミングでラベルのテキストを更新するため、viewModelプロパティにdidSetオブザーバを定義していることに注意してください。画像については、次のブログ記事でイメージビューにセットする方法を説明します。

ImageSearchTableViewControllerに以下のように実装を追加します。

ImageSearchTableViewController.swift

import UIKit
import ExampleViewModel

public final class ImageSearchTableViewController: UITableViewController {
    private var autoSearchStarted = false

    public var viewModel: ImageSearchTableViewModeling? {
        didSet {
            if let viewModel = viewModel {
                viewModel.cellModels.producer
                    .on(next: { _ in self.tableView.reloadData() })
                    .start()
            }
        }
    }

    public override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)

        if !autoSearchStarted {
            autoSearchStarted = true
            viewModel?.startSearch()
        }
    }
}

// MARK: UITableViewDataSource
extension ImageSearchTableViewController {
    public override func tableView(
        tableView: UITableView,
        numberOfRowsInSection section: Int) -> Int
    {
        if let viewModel = viewModel {
            return viewModel.cellModels.value.count
        }
        return 0
    }

    public override func tableView(
        tableView: UITableView,
        cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
    {
        let cell = tableView.dequeueReusableCellWithIdentifier(
            "ImageSearchTableViewCell",
            forIndexPath: indexPath) as! ImageSearchTableViewCell
        if let viewModel = viewModel {
            cell.viewModel = viewModel.cellModels.value[indexPath.row]
        }
        else {
            cell.viewModel = nil
        }

        return cell
    }
}

最初に、ここでもdidSetviewModelプロパティに定義してあり、viewModel.cellModelsプロパティにオブザーバを追加する処理をしています。そのオブザーバにより、viewModel.cellModelsが更新された時にテーブルビューが再読み込みされるようにしてあります。

次に、viewWillAppearをオーバーライドし、ビューが初めて表示された時だけstartSearchを実行するようにしています。viewWillAppearはメインスレッドでのみ呼ばれるので、ロックなしでautoSearchStartedフラグを使用しています。

最後に、単にViewModelを使用するようにUITableViewDataSourceプロトコルを実装しています。

アプリを実行する準備ができました。Command-Rを入力し、以下の画像のようにアプリが表示されるか確認しましょう。イメージビューはまだ実装していませんが、ラベルは画像のメタデータについて表示しています。

SwinjectMVVMExampleFilledLabelsScreenshot

Unit Tests for View Implementation

ViewModelのstartSearchがビューの表示時に1回だけ呼ばれることを確認するため、ExampleViewTestsグループに以下のユニットテストを追加しましょう。モックを使用して何回メソッドが呼ばれたかカウントしています。

ImageSearchTableViewControllerSpec.swift

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

class ImageSearchTableViewControllerSpec: QuickSpec {
    // MARK: Mock
    class MockViewModel: ImageSearchTableViewModeling {
        let cellModels = AnyProperty(
            MutableProperty<[ImageSearchTableViewCellModeling]>([]))
        var startSearchCallCount = 0

        func startSearch() {
            startSearchCallCount++
        }
    }

    // MARK: Spec
    override func spec() {
        it("starts searching images when the view is about to appear at the first time.") {
            let viewModel = MockViewModel()
            let storyboard = UIStoryboard(
                name: "Main",
                bundle: NSBundle(forClass: ImageSearchTableViewController.self))
            let viewController = storyboard.instantiateViewControllerWithIdentifier(
                "ImageSearchTableViewController")
                as! ImageSearchTableViewController
            viewController.viewModel = viewModel

            expect(viewModel.startSearchCallCount) == 0
            viewController.viewWillAppear(true)
            expect(viewModel.startSearchCallCount) == 1
            viewController.viewWillAppear(true)
            expect(viewModel.startSearchCallCount) == 1
        }
    }
}

Command-Uを押してテストを実行しましょう。パスしましたね!

まとめ

例題アプリのViewとViewModel部分を実装しました。最初に空の実装のViewとViewModelをプロジェクトに追加し、Agileプラクティスが言うように動くソフトウェアを作るところから始めました。その後、実際の実装を追加しました。Model、View、ViewModelの依存性をアプリケーションから注入する方法について見ました。containerプロパティをAppDelegateに追加することにより、Dependency Injectionに漏れなどがないかテストすることができました。ReactiveCocoaがサポートするプロパティの型について学びました。MutablePropertyは値が変更可能でオブザーバブルなプロパティでした。AnyPropertyMutablePropertyの読み取り専用のビューとして使いました。次回のブログ記事では、非同期で画像をロードする機能を実装します。

もし質問、提案、問題などがあれば気軽にコメントをどうぞ。


  1. UIStoryboardが普通の指定イニシャライザを持たず、子クラスでイニシャライザをオーバーライドできないため、SwinjectStoryboardのインスタンス化は少し癖があります。この問題のワークアラウンドとして、イニシャライザでなくcreate関数を使ってSwinjectStoryboardのインスタンスを生成します。 

42
37
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
37