如何在故事板托管的UIViewControllers中进行依赖注入?

时间:2018-10-19 08:49:48

标签: ios swift testing dependency-injection storyboard

大家好,我正在尝试测试项目的ViewController之一。此类依赖于另一个这样的帮助程序类:

private let dispatcher: Dispatcher = Dispatcher.sharedInstance
private var loginSync = LoginSync.sharedInstance
private var metadataSync = MetadataSync.sharedInstance

这些辅助类在UIViewController生命周期中使用,例如viewDidLoad或viewWillAppear。在我的测试中,我使用UIStoryboard类实例化了ViewController类,

func testSearchBarAddedIntoNavigationViewForiOS11OrMore() {
    // Given a YourFlow ViewController embedded in a navigation controller
    let mockLoginSync = MockLoginSync()
    let storyboard = UIStoryboard(name: "Main", bundle: nil)

    // Here is too early and view controller is not instantiated yet and I can't assign the mock.
    let vc = storyboard.instantiateViewController(withIdentifier: "YourFlow")
    // Here is too late and viewDidLoad has already been called so assigning the mock at this point is pointless.
    let navigationController = UINavigationController(rootViewController: vc)

    // Assertion code
}

所以我的问题是我需要能够模拟LoginSync类。在正常情况下,我会通过将那些辅助函数作为类构造函数中的参数传递来使用常规依赖项注入。在那种情况下,我无法做到这一点,因为我没有管理View Controller生命周期。因此,一旦我实例化它,辅助程序就已经被使用。

我的问题是:“是否有一种方法可以为我们无法控制其生命周期的View控制器进行依赖注入,或者至少无法解决该问题的解决方法?

谢谢。

编辑:之所以调用viewDidLoad是因为我在didSet重写方法中使用了IBOutlets,而不是因为调用了InstantiateViewController。因此,我可以将代码移开并正确实例化视图控制器后进行注入。

3 个答案:

答案 0 :(得分:5)

您可以像这样包装UIVIewControllerCreation

class func createWith(injection: YourInjection) -> YourViewController {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let vc = storyboard.instantiateViewController(withIdentifier: "YourVCId") as? YourViewController
    vc.injected = injection
    return vc
}

并像这样使用它:

let vc = YourViewController.createWith(<your injection>)

这里是一个例子:

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        let vc = RedViewController.createWith(injection: "some")
        navigationController?.pushViewController(vc, animated: true)
    }
}


class RedViewController: UIViewController {
    var injected: String = "" {
        didSet {
            print(#function)
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .red
        print(#function)
    }

    class func createWith(injection: String) -> RedViewController {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboard.instantiateViewController(withIdentifier: "Red") as! RedViewController
        vc.injected = injection
        return vc
    }
}

故事板设置:

enter image description here

代码运行结果打印:

injected
viewDidLoad()

您可以看到,注入发生在viewDidLoad()之前

答案 1 :(得分:1)

情节提要中的视图控制器始终使用init?(coder aDecoder: NSCoder)进行初始化,因此无法在初始化时设置任何属性。

我发现以下是一种不错的解决方法……

而不是使用

let loginSync: LoginSync

声明为

private (set) var loginSync: LoginSync!

声明

func configure(loginSync: LoginSync) {
    self.loginSync = loginSync
}

然后

let vc = storyboard.instantiateViewController(withIdentifier: "YourFlow")
vc.configure(loginSync: MockLoginSync())

您也可以在segues中使用它...

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    switch segue.destination) {
    case let vc as MyViewController:
        vc.configure(loginSync: MockLoginSync())
    default:
        break
    }
}

这并不完美,但是设置属性private (set)可以确保不能从另一个类中对其进行修改,并且隐式展开(!)意味着如果未设置该属性,则会崩溃。

在每个configure() / UIView中使用UIViewController方法-一旦习惯了这种模式,它便成为第二自然。

答案 2 :(得分:0)

您需要使用segues导航到视图控制器,以便您可以在prepareForSegue期间注入依赖项,如下所示:

override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
    if (segue.identifier == "SomeSegueToYourFlow") {
        if let yourFlowVC = segue.destination as? YourFlowController {
            let mockLoginSync = MockLoginSync()
            yourFlowVC.loginSync = mockLoginSync
        }
    }
}