What is wrong with my template/generic Swift initializer/constructor? - ios

I created a SlidingNavigationController where I wanted to have an initializer that takes three parameters. All three parameters should be UIViewControllers but they need to confirm to my SlidingIconProtocol. So I wrote code like this (simplified version):
struct SlidingItem {
var bigIconView: UIView
var smallIconView: UIView
}
protocol SlidingIconProtocol {
var slidingItem: SlidingItem { get set }
}
class SlidingNavigationController: UIViewController {
init<T:UIViewController where T:SlidingIconProtocol>(centralVC: T, leftVC: T, rightVC: T) {
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class CentralVC: UIViewController, SlidingIconProtocol {
var slidingItem = SlidingItem(bigIconView: UIView(), smallIconView: UIView())
}
class LeftVC: UIViewController, SlidingIconProtocol {
var slidingItem = SlidingItem(bigIconView: UIView(), smallIconView: UIView())
}
class RightVC: UIViewController, SlidingIconProtocol {
var slidingItem = SlidingItem(bigIconView: UIView(), smallIconView: UIView())
}
let myVC = SlidingNavigationController(centralVC: CentralVC(), leftVC: LeftVC(), rightVC: RightVC())
The problem is that Swift fails to compile on the last line of code with: "Cannot invoke initializer for type 'SlidingNavigationController' with an argument list of type '(centralVC: CentralVC, leftVC: LeftVC, rightVC: RightVC)'"
Not sure why this does not work, since even Swift/Xcode completion is giving me option to use this initializer. And all passed parameter confirm to SlidingIconProtocol.
Does anyone know what is wrong with the code and what is the right way in Swift to achieve the same (is it possible at all) ?

You can't use template like that way. In your code:
init<T:UIViewController where T:SlidingIconProtocol>(centralVC: T, leftVC: T, rightVC: T)
{
super.init(nibName: nil, bundle: nil)
}
T represents a class that is a subclass of UIViewController and implements SlidingIconProtocol. So when you call:
let myVC = SlidingNavigationController(centralVC: CentralVC(), leftVC: LeftVC(), rightVC: RightVC())
The T is assumed as CentralVC (first parameter), and the init method will be represented as:
init< CentralVC:UIViewController where CentralVC:SlidingIconProtocol>(centralVC: CentralVC, leftVC: CentralVC, rightVC: CentralVC)
{
super.init(nibName: nil, bundle: nil)
}
But you are passing different class object as the second and third parameter. And it will throw error. In your class the following code is valid:
let myVC = SlidingNavigationController(centralVC: CentralVC(), leftVC: CentralVC(), rightVC: CentralVC())
Because all the passed arguments are object of same class (CentralVC). So fixing the issue, you need to implement the init method in the following way:
init<T1:UIViewController, T2:UIViewController, T3:UIViewController where T1:SlidingIconProtocol, T2:SlidingIconProtocol, T3:SlidingIconProtocol>(centralVC: T1, leftVC: T2, rightVC: T3)
{
super.init(nibName: nil, bundle: nil)
}

Related

Cast type not work in generic function scope. Why?

I did a class Router (subclass of UINavigationController) in my project for centralize and minimize the code used to instantiate and navigate between views. But when i try minimize a particular function (buildView), the type casting not work. But this works fine out of scope of function buildView, same whithout the as! operator in function goHome.
enum Routes {
case home
case account
var file: String {
switch self {
case .home:
return "HomeView"
case .account:
return "AccountView"
}
}
protocol HomeInterface: class {
func goTo(view: Routes)
func showModal(view: Routes, caller: UIViewController)
}
class HomePresenter: NSObject, HomeInterface {
init(view: HomeViewInterface) {
self.view = view
}
internal func goTo(view: Routes) { /* Implementation */ }
internal func showModal(view: Routes, caller: UIViewController) {/* Implementation */ }
}
protocol HomeViewInterface: class {
/* Implementation */
}
class HomeViewController: UIViewController, HomeViewInterface {
var presenter: HomeInterface?
override func viewDidLoad() {
super.viewDidLoad()
}
/* Implementation */
}
Working Code
func goHome() {
let viewInstance = buildView(view.file, HomeViewController.identifier, HomeViewController.self)
viewInstance.presenter = HomePresenter(view: viewInstance)
self.view?.pushViewController(viewInstance, animated: true)
}
private func buildView<T>(_ nameFile: String, _ identifier: String, _ viewClass: T.Type) -> T {
return UIStoryboard(name: nameFile, bundle: nil).instantiateViewController(withIdentifier: identifier) as! T
}
Desired final code, but does not work:
func goHome() {
buildViewFinal(view.file, HomeViewController.identifier, HomeViewController.self)
}
func buildViewFinal<T, P>(_ nameFile: String, _ identifier: String, viewClass: T, presenter: P) {
let viewInstance = UIStoryboard(name: nameFile, bundle: nil).instantiateViewController(withIdentifier: identifier) as? T
viewInstance.presenter = P(view: viewInstance)
self.view?.pushViewController(viewInstance, animated: true)
}
When i try minimize the code only to buildViewFinalfunction, the property presenter of viewInstance is not recognize, showing a compile error
Value of type 'T?' has no member 'presenter'
, and in pushViewControllershow error:
Cannot convert value of type 'T?' to expected element type
'UIViewController'
The main goal is turn all code to create and navigate useful and simple.
So, how this works fine in first code, but fails in recognize type inside buildViewFinal scope?
In your first piece of code, you are passing HomeViewController.self as viewClass and so it knows that buildViewFinal is going to return an instance of HomeViewController and that HomeViewController has a presenter property.
In the second code snippet, the compiler doesn't know anything about T, so it can't assume that it will have a presenter property.
You could use type constraint to enforce that T is a HomeViewController or whatever class defines the presenter property:
func buildViewFinal<T: HomeInterface, P>(_ nameFile: String, _ identifier: String, viewClass: T, presenter: P) {
let viewInstance = UIStoryboard(name: nameFile, bundle: nil).instantiateViewController(withIdentifier: identifier) as? T
viewInstance.presenter = P(view: viewInstance)
self.view?.pushViewController(viewInstance, animated: true)
}
but then you will have a problem that viewInstance can't be pushed because the compiler doesn't know that it is an instance of a UIViewController subclass.
Really generics and protocols are just complicating things here.
You are dealing with UIKit which is class oriented, so you might as well just use good old inheritance

How to subclass a UIViewController and add properties in swift?

I want to make a new kind of view controller in Swift that has an additional property that must be explicitly initialized. It doesn't make any sense for this property to be nil or have a default value and the controller will only be initialized programmatically. I tried defining it like this:
class MyController : UIViewController {
var prop: Int
init(prop: Int) {
self.prop = prop
super.init()
}
required init(coder aDecoder: NSCoder?) {
fatalError("don't serialize this")
}
}
I tried running this but it crashed because super.init() tries to run the nib constructor which isn't defined, so I tried adding that:
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
But now the compiler complains that prop isn't being initialized. And this is my question: how can I initialize prop correctly here? I don't want a default value, and anything I set will override the correct value that I set in the other initializer.
I kinda hacked around it by setting some default value in the nib init, but then having my first init do this
self.prop = prop
super.init()
self.prop = prop
But other than being really weird and ugly, that makes me worried that now it is possible to initialize my view controller from a nib and end up with the default value, which would be bad.
What is the correct and idiomatic way to do this in Swift?
At some point the view controller must be initialized by calling init(nibName:bundle:) or init(coder:)
Try this:
class MyViewController: UIViewController {
var prop: Int
init(prop: Int, nibName nibNameOrNil: String? = nil, bundle nibBundleOrNil: Bundle? = nil) {
self.prop = prop
super.init(nibName:nibNameOrNil, bundle: nibBundleOrNil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Try the following
class MyController: UIViewController {
var prop: Int
required init?(coder aDecoder: NSCoder) {
fatalError()
}
init(prop: Int) {
self.prop = prop
super.init(nibName: nil, bundle: nil)
}
}

Convenience init for UIViewController not appearing when added via extension

I'm wanting to add a convenience initilizer to UIViewController via an extension because I want all UIViewControllers/UIViewController subclasses to have access to it. But when I add it, it doesn't appear in the drop down list of available initilizers and if I try to use it I get an error saying Missing argument label 'coder:' in call.
extension UIViewController {
convenience init(test: String) {
self.init(nibName: nil, bundle: nil)
print(test)
}
let testController = TestController(test: "Hello World!") // Missing argument label 'coder:' in call
Is there some kind of trick to get this to work?
I am able to add convenience initilizer's to other UIKit classes and have them appear as available inits.
Its working fine as you can check and match your code, maybe you need to delete derived data:
extension UIViewController {
convenience init(test: String) {
self.init(nibName: nil, bundle: nil)
print(test)
}
}
class SecondViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var textf: UITextField!
var doubleValue: Double?
override func viewDidLoad() {
super.viewDidLoad()
let testController = SecondViewController(test: "ffwfew")
print(testController)
}
}

How do I pass arguments to the resolve method when using Swinject?

I have a test project that I'm trying to pass an argument to the resolve method in a Swinject project.
Here is an example of what my Swinject storyboard extetion file has in it.
import Swinject
extension SwinjectStoryboard {
class func setup() {
let mainDm = MainDM()
defaultContainer.register(MainDM.self) { _ in
mainDm
}
defaultContainer.registerForStoryboard(ViewController.self) { r, c in
c.dm = r.resolve(MainDM.self)
c.container = defaultContainer
}
defaultContainer.register(GetMessageAction.self) { _, delegate in
GetMessageAction(dm:mainDm, delegate: delegate)
}
}
}
in my ViewController I'm trying to do the following to resolve the GetMessageAction
#IBOutlet weak var myText: UILabel!
var dm:MainDM!
var container:Container!
override func viewDidLoad() {
super.viewDidLoad()
NSTimer.scheduledTimerWithTimeInterval(NSTimeInterval(3), target: self, selector: #selector(ViewController.getMessage), userInfo: nil, repeats: false)
}
func getMessage() {
let action:GetMessageAction? = container.resolve(GetMessageAction.self, argument: self)!
action?.execute()
}
I get the following message when my getMessage function runs
fatal error: unexpectedly found nil while unwrapping an Optional value
As resolving with arguments is dependent on exactly matching types of arguments, you need to downcast passed object:
container.resolve(GetMessageAction.self, argument: self as GetMessageActionDelegate)!
Assuming that GetMessageActionDelegate is the type of delegate passed in constructor GetMessageAction(dm:delegate:).
The swift file of the ViewController you have created in your Storyboard must declare init(NSCoder), it is actually not mentioned in the README.md, I'm thinking about opening an issue regarding this...
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
You can take a look at my open source project using exactly this technique, I am setting up the dependencies using the extension of SwinjectStoryboard here for example the LoadingDataVC.
extension SwinjectStoryboard {
class func setup() {
defaultContainer.register(HTTPClientProtocol.self) { _ in
HTTPClient()
}.inObjectScope(.Container)
defaultContainer.register(APIClientProtocol.self) { r in
APIClient(
httpClient: r.resolve(HTTPClientProtocol.self)!
)
}.inObjectScope(.Container)
defaultContainer.register(ImagePrefetcherProtocol.self) { _ in
ImagePrefetcher()
}.inObjectScope(.Container)
defaultContainer.registerForStoryboard(GameVC.self) { r, c in
c.imagePrefetcher = r.resolve(ImagePrefetcherProtocol.self)
}
defaultContainer.registerForStoryboard(LoadingDataVC.self) { r, c in
c.apiClient = r.resolve(APIClientProtocol.self)
c.imagePrefetcher = r.resolve(ImagePrefetcherProtocol.self)
}
}
}
Once you have the required init it should work! :)
Use either of the following methods of a storyboard to get a view controller registered by registerForStoryboard.
instantiateViewControllerWithIdentifier
instantiateInitialViewController
https://github.com/Swinject/Swinject/blob/v1/Documentation/Storyboard.md
https://github.com/Swinject/SwinjectStoryboard/issues/5

UIViewController extension to instantiate from storyboard

I'm trying to write a little extension in Swift to handle instantiation of a UIViewController from a storyboard.
My idea is the following: Since UIStoryboard's method instantiateViewControllerWithIdentifier needs an identifier to instantiate a given storyboard's view controller, why don't assign every view controller in my storyboard an identifier equal to its exact class name (i.e a UserDetailViewController would have an identifier of "UserDetailViewController"), and, create a class method on UIViewController that would:
accept a UIStoryboard instance as a unique parameter
get the current class name as a string
call instantiateViewControllerWithIdentifier on the storyboard instance with the class name as a parameter
get the newly created UIViewController instance, and return it
So, instead of (which repeats the class name as a string, not very nice)
let vc = self.storyboard?.instantiateViewControllerWithIdentifier("UserDetailViewController") as UserDetailViewController
it would be:
let vc = UserDetailViewController.instantiateFromStoryboard(self.storyboard!)
I used to do it in Objective-C with the following category:
+ (instancetype)instantiateFromStoryboard:(UIStoryboard *)storyboard
{
return [storyboard instantiateViewControllerWithIdentifier:NSStringFromClass([self class])];
}
But I'm completely stuck with the Swift version. I hope is that there is some kind of way to do it.
I tried the following:
extension UIViewController {
class func instantiateFromStoryboard(storyboard: UIStoryboard) -> Self {
return storyboard.instantiateViewControllerWithIdentifier(NSStringFromClass(Self))
}
}
Returning Self instead of AnyObject allows the type inference to work. Otherwise, I would have to cast every single return of this method, which is annoying, but maybe you have a better solution?
This gives me the error: Use of unresolved identifier 'Self'
The NSStringFromClass part seems to be the problem.
What do you think?
Is there any way to return Self from class functions?
How would you get this working without the need to cast the return value every time? (i.e keeping -> Self as return value)
How about writing an extension to UIStoryboard instead of UIViewController?
extension UIStoryboard {
func instantiateVC<T: UIViewController>() -> T? {
// get a class name and demangle for classes in Swift
if let name = NSStringFromClass(T.self)?.componentsSeparatedByString(".").last {
return instantiateViewControllerWithIdentifier(name) as? T
}
return nil
}
}
Even adopting this approach, cost of an use side is low as well.
let vc: UserDetailViewController? = aStoryboard.instantiateVC()
Thanks to MartinR and his answer, I know the answer:
UPDATE: rewritten with a protocol.
Instantiable
protocol StringConvertible {
var rawValue: String {get}
}
protocol Instantiable: class {
static var storyboardName: StringConvertible {get}
}
extension Instantiable {
static func instantiateFromStoryboard() -> Self {
return instantiateFromStoryboardHelper()
}
private static func instantiateFromStoryboardHelper<T>() -> T {
let identifier = String(describing: self)
let storyboard = UIStoryboard(name: storyboardName.rawValue, bundle: nil)
return storyboard.instantiateViewController(withIdentifier: identifier) as! T
}
}
//MARK: -
extension String: StringConvertible { // allow string as storyboard name
var rawValue: String {
return self
}
}
StoryboardName
enum StoryboardName: String, StringConvertible {
case main = "Main"
//...
}
Usage:
class MyViewController: UIViewController, Instantiable {
static var storyboardName: StringConvertible {
return StoryboardName.main //Or you can use string value "Main"
}
}
let viewController = MyController.instantiateFromStoryboard()
you can create UIViewController Instance like this:
Create enum with all your storyboard name.
enum AppStoryboard: String {
case main = "Main"
case profile = "Profile"
}
Then, here is the extension for instantiate UIViewController
extension UIViewController {
class func instantiate<T: UIViewController>(appStoryboard: AppStoryboard) -> T {
let storyboard = UIStoryboard(name: appStoryboard.rawValue, bundle: nil)
let identifier = String(describing: self)
return storyboard.instantiateViewController(withIdentifier: identifier) as! T
}
}
Usage:
let profileVC: ProfileVC = ProfileVC.instantiate(appStoryboard: .profile)
self.navigationController?.pushViewController(profileVC,animated:true)
We are porting our objective c project to swift. We have split the project into modules. Modules have their own storyboards. We have extended your(even our's as well) problem's solution to one more level by avoiding explicit storyboard names.
// Add you modules here. Make sure rawValues refer to a stroyboard file name.
enum StoryModule : String {
case SomeModule
case AnotherModule = "AnotherModulesStoryBoardName"
// and so on...
}
extension UIStoryboard {
class func instantiateController<T>(forModule module : StoryModule) -> T {
let storyboard = UIStoryboard.init(name: module.rawValue, bundle: nil);
let name = String(T).componentsSeparatedByString(".").last
return storyboard.instantiateViewControllerWithIdentifier(name!) as! T
}
}
// Some controller whose UI is in a stroyboard named "SomeModule.storyboard",
// and whose storyboardID is the class name itself, ie "MyViewController"
class MyViewController : UIViewController {
// Controller Code
}
// Usage
class AClass
{
// Here we must alwasy provide explicit type
let viewController : MyViewController = UIStoryboard.instantiateController(forModule: StoryModule.SomeModule)
}
Two things:
Class constructors in Objective-C are convenience initializers in Swift. Use convenience init rather than class func.
NSStringFromClass(Self) with NSStringFromClass(self.type).
Here is a modern Swift example, based on #findall's solution:
extension UIStoryboard {
func instantiate<T>() -> T {
return instantiateViewController(withIdentifier: String(describing: T.self)) as! T
}
static let main = UIStoryboard(name: "Main", bundle: nil)
}
Usage:
let userDetailViewController = UIStoryboard.main.instantiate() as UserDetailViewController
I think it is ok to fail when trying to instantiate a view controller from a storyboard as this kind of problem should be detected soon.
Or, you can do so
func instantiateViewControllerWithIdentifier<T>(_ identifier: T.Type) -> T {
let identifier = String(describing: identifier)
return instantiateViewController(withIdentifier: identifier) as! T
}
Use protocol in UIViewController to reach your thoughts
let vc = YourViewController.instantiate(from: .StoryboardName)
You can see the use of my link :D
https://github.com/JavanC/StoryboardDesignable
You can add this extension :-
extension UIStoryboard{
func instantiateViewController<T:UIViewController>(type: T.Type) -> T? {
var fullName: String = NSStringFromClass(T.self)
if let range = fullName.range(of:".", options:.backwards, range:nil, locale: nil){
fullName = fullName.substring(from: range.upperBound)
}
return self.instantiateViewController(withIdentifier:fullName) as? T
}
}
And can instantiate view controller like this :-
self.storyboard?.instantiateViewController(type: VC.self)!
In complement for the version of #ChikabuZ, here mine that takes into account which bundle the storyboard is in (for example, if your storyboads are in another bundle than your app). I added also a small func if you want to use xib instead of storyboad.
extension UIViewController {
static func instantiate<TController: UIViewController>(_ storyboardName: String) -> TController {
return instantiateFromStoryboardHelper(storyboardName)
}
static func instantiate<TController: UIViewController>(_ storyboardName: String, identifier: String) -> TController {
return instantiateFromStoryboardHelper(storyboardName, identifier: identifier)
}
fileprivate static func instantiateFromStoryboardHelper<T: UIViewController>(_ name: String, identifier: String? = nil) -> T {
let storyboard = UIStoryboard(name: name, bundle: Bundle(for: self))
return storyboard.instantiateViewController(withIdentifier: identifier ?? String(describing: self)) as! T
}
static func instantiate<TController: UIViewController>(xibName: String? = nil) -> TController {
return TController(nibName: xibName ?? String(describing: self), bundle: Bundle(for: self))
}
}
I had a similar thought and settled on using the extension below. It still uses the normal instantiation process, but removes reliance on stringly typed Storyboard and View Controller names:
let myVC = UIStoryboard(.main).instantiate(MyViewController.self)
The return type above is pre-cast to MyViewController, not the standard UIViewController.
extension UIStoryboard {
enum Name: String {
case main = "Main"
case launch = "LaunchScreen"
case other = "Other"
}
convenience init(_ name: Name, bundle: Bundle? = nil) {
self.init(name: name.rawValue, bundle: bundle)
}
func instantiate<T: UIViewController>(_ type: T.Type) -> T {
instantiateViewController(withIdentifier: String(describing: type)) as! T
}
}
Note that you must ensure that each VC's Storyboard Identifier exactly matches its class name! Failure to do so will result in the exception:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Storyboard (<UIStoryboard: 0x6000035c04e0>) doesn't contain a view controller with identifier 'MyViewController''

Resources