如何在故事板管理的 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)
大家好我正在尝试测试我项目的 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)