iOS
Swift
Swinject
swift4

Swinjectにおけるインスタンスのスコープに注意

SwiftのDIコンテナライブラリであるSwinjectSwinjectStoryboardをプロジェクトで使用しているのですが、少しハマったことがあったので書きます。

以下に完全なコードを載せていますが、注目してほしいのはViewModelAssemblyのところです。
ViewModelAssembly内でAppViewModelのインスタンスを登録していて、それを同じViewModelAssembly内でMainViewModelSubViewModelにインジェクトしています。

一見正しそうに見えますが、実はMainViewModelSubViewModelには別々のAppViewModelインスタンスがインジェクトされてしまいます。

SwinjectStoryboard+Setup.swift
import Swinject
import SwinjectStoryboard

extension SwinjectStoryboard {
    class ApiAssembly: Assembly {
        func assemble(container: Container) {
            container.register(API.self) { _ in
                return API()
            }
        }
    }

    class ViewModelAssembly: Assembly {
        func assemble(container: Container) {
            container.register(AppViewModelProtocol.self) { _ in
                return AppViewModel()
            }

            container.register(MainViewModelProtocol.self) { r in
                let api = r.resolve(API.self)!
                let appViewModel = r.resolve(AppViewModelProtocol.self)!
                return MainViewModel(api: api, appViewModel: appViewModel)
            }

            container.register(SubViewModelProtocol.self) { r in
                let api = r.resolve(API.self)!
                let appViewModel = r.resolve(AppViewModelProtocol.self)!
                return SubViewModel(api: api, appViewModel: appViewModel)
            }
        }
    }

    class ViewControllerAssembly: Assembly {
        func assemble(container: Container) {
            container.storyboardInitCompleted(MainViewController.self) { r, c in
                c.vm = r.resolve(MainViewModelProtocol.self)!
            }
        }

        func assemble(container: Container) {
            container.storyboardInitCompleted(SubViewController.self) { r, c in
                c.vm = r.resolve(SubViewModelProtocol.self)!
            }
        }
    }

    @objc class func setup() {
        let assembler = Assembler(container: SwinjectStoryboard.defaultContainer)
        assembler.apply(assemblies: [
            ApiAssembly(),
            ViewModelAssembly(),
            ViewControllerAssembly()
        ])
    }
}

SwinjectではOpjectScopeという概念があり、デフォルトでは型が毎回resolveされるたびに、新しいインスタンスが作成されるようになっています。

インスタンスの作成は一度だけにして、二回目以降にresolveされる際は既存のインスタンスを返すようにするには、inObjectScope()メソッドを呼んであげる必要があります。

SwinjectStoryboard+Setup.swift
    class ViewModelAssembly: Assembly {
        func assemble(container: Container) {
            container.register(AppViewModelProtocol.self) { _ in
                return AppViewModel()
            }
            .inObjectScope(.container)  // スコープを指定する!!
        }
    }