如何在故事板管理的 UIViewControllers 中依赖注入?

How to dependency inject in storyboard managed UIViewControllers?

大家好我正在尝试测试我项目的 ViewController 之一。这个 class 依赖于另一个助手 class ,如下所示:

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

这些助手 classes 在 UIViewController 生命周期中使用,如 viewDidLoad 或 viewWillAppear。在我的测试中,我使用 UIStoryboard class 实例化 ViewController class,如下所示:

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 class。在正常情况下,我会通过将这些助手作为参数传递给 class 构造函数来使用常规依赖注入。在那种情况下,我不能那样做,因为我没有管理 View Controller 生命周期。所以我一实例化它,助手就已经被使用了。

我的问题是:“有没有办法为我们无法控制其生命周期的视图控制器进行依赖注入,或者至少有解决方法?

谢谢。

编辑:所以调用 viewDidLoad 是因为我在 didSet 覆盖方法中使用了 IBOutlets,而不是因为调用了 instantiateViewController。所以我可以在正确实例化视图控制器后移走该代码并进行注入。

您需要使用 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
        }
    }
}

故事板中的视图控制器始终使用 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) 确保它不能被另一个 class 修改,并且隐式展开 (!) 意味着你如果未设置,将会崩溃。

在每个 UIView/UIViewController 中使用 configure() 方法 - 一旦您习惯了这种模式,它就会成为第二天性。

你可以这样包装 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
    }
}

故事板设置:

代码 运行 结果打印:

injected
viewDidLoad()

如您所见,注入发生在 viewDidLoad() 之前

iOS13、Xcode11解

您可以利用 instantiateViewController(identifier:creator:) 来实现您想要实现的目标。只需在 init? 初始化程序中命名所有依赖项,然后使用 instantiateViewController 方法初始化您的 ViewController。这样做的好处是您可以将变量保密:

class MyViewController: UIViewController {
    private let networkManager: MyNetworkManager

    // Name your dependencies here
    init?(coder: NSCoder, networkManager: MyNetworkManager) {
        self.networkManager = networkManager
        super.init(coder: coder)
    }

    required init?(coder: NSCoder) {
        fatalError("NetworkManager is missing")
    }
    
    /// Returns an instance of the MyViewController with dependency injection
    static func createFromStoryBoardWith(
        networkManager: MyNetworkManager)
        -> MyViewController
    {
        let storyboard = UIStoryboard(name: "MyViewControllerStoryboard", bundle: nil)
        let viewController = storyboard.instantiateViewController(identifier: "MyViewControllerStoryboardID", creator: { coder in
            MyViewController(coder: coder, networkManager: networkManager) // The magic happens here
        })
        return viewController
    }
}

其中 NetworkManager 是一个简单的结构:

struct MyNetworkManager {
    var id: String
}

您可以像这样使用它:

let networkManager = MyNetworkManager(id: "ABCDE")
let myViewController = MyViewController.createFromStoryBoardWith(networkManager: networkManager)