You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

218 lines
10 KiB

//
// SwinjectStoryboard.swift
// Swinject
//
// Created by Yoichi Tagaya on 7/31/15.
// Copyright © 2015 Swinject Contributors. All rights reserved.
//
import Swinject
#if canImport(UIKit)
import UIKit
#elseif canImport(Cocoa)
import Cocoa
#endif
#if os(iOS) || os(tvOS) || os(OSX)
/// The `SwinjectStoryboard` provides the features to inject dependencies of view/window controllers in a storyboard.
///
/// To specify a registration name of a view/window controller registered to the `Container` as a service type,
/// add a user defined runtime attribute with the following settings:
///
/// - Key Path: `swinjectRegistrationName`
/// - Type: String
/// - Value: Registration name to the `Container`
///
/// in User Defined Runtime Attributes section on Indentity Inspector pane.
/// If no name is supplied to the registration, no runtime attribute should be specified.
@objcMembers
@objc(SwinjectStoryboard)
public class SwinjectStoryboard: _SwinjectStoryboardBase, SwinjectStoryboardProtocol {
/// A shared container used by SwinjectStoryboard instances that are instantiated without specific containers.
///
/// Typical usecases of this property are:
/// - Implicit instantiation of UIWindow and its root view controller from "Main" storyboard.
/// - Storyboard references to transit from a storyboard to another.
public static var defaultContainer = Container()
// Boxing to workaround a runtime error [Xcode 7.1.1 and Xcode 7.2 beta 4]
// If container property is Resolver type and a Resolver instance is assigned to the property,
// the program crashes by EXC_BAD_ACCESS, which looks a bug of Swift.
internal var container: Box<Resolver>!
private override init() {
super.init()
}
#if os(iOS) || os(tvOS)
/// Creates the new instance of `SwinjectStoryboard`. This method is used instead of an initializer.
///
/// - Parameters:
/// - name: The name of the storyboard resource file without the filename extension.
/// - storyboardBundleOrNil: The bundle containing the storyboard file and its resources. Specify nil to use the main bundle.
///
/// - Note:
/// The shared singleton container `SwinjectStoryboard.defaultContainer` is used as the container.
///
/// - Returns: The new instance of `SwinjectStoryboard`.
public class func create(
name: String,
bundle storyboardBundleOrNil: Bundle?) -> SwinjectStoryboard {
return SwinjectStoryboard.create(name: name, bundle: storyboardBundleOrNil,
container: SwinjectStoryboard.defaultContainer)
}
/// Creates the new instance of `SwinjectStoryboard`. This method is used instead of an initializer.
///
/// - Parameters:
/// - name: The name of the storyboard resource file without the filename extension.
/// - storyboardBundleOrNil: The bundle containing the storyboard file and its resources. Specify nil to use the main bundle.
/// - container: The container with registrations of the view/window controllers in the storyboard and their dependencies.
///
/// - Returns: The new instance of `SwinjectStoryboard`.
public class func create(
name: String,
bundle storyboardBundleOrNil: Bundle?,
container: Resolver) -> SwinjectStoryboard
{
// Use this factory method to create an instance because the initializer of UI/NSStoryboard is "not inherited".
let storyboard = SwinjectStoryboard._create(name, bundle: storyboardBundleOrNil)
storyboard.container = Box(container)
return storyboard
}
/// Instantiates the view controller with the specified identifier.
/// The view controller and its child controllers have their dependencies injected
/// as specified in the `Container` passed to the initializer of the `SwinjectStoryboard`.
///
/// - Parameter identifier: The identifier set in the storyboard file.
///
/// - Returns: The instantiated view controller with its dependencies injected.
public override func instantiateViewController(withIdentifier identifier: String) -> UIViewController {
SwinjectStoryboard.pushInstantiatingStoryboard(self)
let viewController = super.instantiateViewController(withIdentifier: identifier)
SwinjectStoryboard.popInstantiatingStoryboard()
injectDependency(to: viewController)
return viewController
}
private func injectDependency(to viewController: UIViewController) {
guard !viewController.wasInjected else { return }
defer { viewController.wasInjected = true }
let registrationName = viewController.swinjectRegistrationName
// Xcode 7.1 workaround for Issue #10. This workaround is not necessary with Xcode 7.
// If a future update of Xcode fixes the problem, replace the resolution with the following code and fix storyboardInitCompleted too.
// https://github.com/Swinject/Swinject/issues/10
if let container = container.value as? _Resolver {
let option = SwinjectStoryboardOption(controllerType: type(of: viewController))
typealias FactoryType = ((Resolver, Container.Controller)) -> Any
let _ = container._resolve(name: registrationName, option: option) { (factory: FactoryType) in factory((self.container.value, viewController)) as Any } as Container.Controller?
} else {
fatalError("A type conforming Resolver protocol must conform _Resolver protocol too.")
}
#if swift(>=4.2)
for child in viewController.children {
injectDependency(to: child)
}
#else
for child in viewController.childViewControllers {
injectDependency(to: child)
}
#endif
}
#elseif os(OSX)
/// Creates the new instance of `SwinjectStoryboard`. This method is used instead of an initializer.
///
/// - Parameters:
/// - name: The name of the storyboard resource file without the filename extension.
/// - storyboardBundleOrNil: The bundle containing the storyboard file and its resources. Specify nil to use the main bundle.
///
/// - Note:
/// The shared singleton container `SwinjectStoryboard.defaultContainer` is used as the container.
///
/// - Returns: The new instance of `SwinjectStoryboard`.
public class func create(
name: NSStoryboard.Name,
bundle storyboardBundleOrNil: Bundle?) -> SwinjectStoryboard {
return SwinjectStoryboard.create(name: name, bundle: storyboardBundleOrNil,
container: SwinjectStoryboard.defaultContainer)
}
/// Creates the new instance of `SwinjectStoryboard`. This method is used instead of an initializer.
///
/// - Parameters:
/// - name: The name of the storyboard resource file without the filename extension.
/// - storyboardBundleOrNil: The bundle containing the storyboard file and its resources. Specify nil to use the main bundle.
/// - container: The container with registrations of the view/window controllers in the storyboard and their dependencies.
///
/// - Returns: The new instance of `SwinjectStoryboard`.
public class func create(
name: NSStoryboard.Name,
bundle storyboardBundleOrNil: Bundle?,
container: Resolver) -> SwinjectStoryboard
{
// Use this factory method to create an instance because the initializer of UI/NSStoryboard is "not inherited".
let storyboard = SwinjectStoryboard._create(name, bundle: storyboardBundleOrNil)
storyboard.container = Box(container)
return storyboard
}
/// Instantiates the view/Window controller with the specified identifier.
/// The view/window controller and its child controllers have their dependencies injected
/// as specified in the `Container` passed to the initializer of the `SwinjectStoryboard`.
///
/// - Parameter identifier: The identifier set in the storyboard file.
///
/// - Returns: The instantiated view/window controller with its dependencies injected.
public override func instantiateController(withIdentifier identifier: NSStoryboard.SceneIdentifier) -> Any {
SwinjectStoryboard.pushInstantiatingStoryboard(self)
let controller = super.instantiateController(withIdentifier: identifier)
SwinjectStoryboard.popInstantiatingStoryboard()
injectDependency(to: controller)
return controller
}
private func injectDependency(to controller: Container.Controller) {
guard let controller = controller as? InjectionVerifiable, !controller.wasInjected else { return }
defer { controller.wasInjected = true }
let registrationName = (controller as? RegistrationNameAssociatable)?.swinjectRegistrationName
// Xcode 7.1 workaround for Issue #10. This workaround is not necessary with Xcode 7.
// If a future update of Xcode fixes the problem, replace the resolution with the following code and fix storyboardInitCompleted too:
// https://github.com/Swinject/Swinject/issues/10
if let container = container.value as? _Resolver {
let option = SwinjectStoryboardOption(controllerType: type(of: controller))
typealias FactoryType = ((Resolver, Container.Controller)) -> Any
let _ = container._resolve(name: registrationName, option: option) { (factory: FactoryType) -> Any in factory((self.container.value, controller)) } as Container.Controller?
} else {
fatalError("A type conforming Resolver protocol must conform _Resolver protocol too.")
}
if let windowController = controller as? NSWindowController, let viewController = windowController.contentViewController {
injectDependency(to: viewController)
}
if let viewController = controller as? NSViewController {
#if swift(>=4.2)
for child in viewController.children {
injectDependency(to: child)
}
#else
for child in viewController.childViewControllers {
injectDependency(to: child)
}
#endif
}
}
#endif
}
#endif