0

I've come to Swift from Objective-C and there's a lot of things that Objective-C can do but swift is much more complicated. Such as OOP dynamic initializer.

E.g. I've got this code working in Objective-C:

@interface CommonVC: UIViewController
+ (instancetype)showFrom:(UIViewController *)vc;
@end

@implementation CommonVC

+ (instancetype)showFrom:(UIViewController *)vc {
    CommonVC *instance = [self instantiateFrom:vc.nibBundle];
    [vc presentViewController:instance animated:YES completion:nil];
    return instance;
}

// this is like convenience initializer.
+ (instancetype)instantiateFrom:(NSBundle *)aBundle {
    return [self.alloc initWithNibName:NSStringFromClass(self.class) bundle:aBundle];
}

@end

@interface SubClassVC: CommonVC
@end

And then use the subclass or superclass like this:

SubClassVC *subVC = [SubClassVC showFrom:self];
// or in swift:
SubClassVC.show(from: self)

However, in swift, it seems impossible to implement something like that. I've tried a few, but always got compile error. Here's one:

class CommonVC: UIViewController {

    class func show(from sender: UIViewController) -> Self {
        let vc = self(sender: sender) // Compiler error: Constructing an object of class type 'Self' with a metatype value must use a 'required' initializer
        sender.present(vc, animated: true, completion: nil)
        return unsafeDowncast(vc, to: self)
    }

    convenience init(sender: UIViewController) {
        self.init(nibName: type(of: self).className, bundle: sender.nibBundle)
        loadView()
    }
}

So how do I write generic convenience initializer of a viewController from the super class and then call that with the subclass?

Of course, my convenience init has lots of stuff that I just cut down to this simple code, also the function show(from:) has a different presentation instead of this simple present(_:animated:completion:).

Even if I make a function to do the setup after initialize, it still wouldn't work

class CommonVC: UIViewController {

    class func show(from sender: UIViewController) -> Self {
        let vc = self.init(nibName: type(of: self).className, bundle: sender.nibBundle) // Compiler error: Constructing an object of class type 'Self' with a metatype value must use a 'required' initializer
        vc.setupAfterInitialize()
        sender.present(vc, animated: true, completion: nil)
        return unsafeDowncast(vc, to: self)
    }

    convenience init(sender: UIViewController) {
        self.init(nibName: type(of: self).className, bundle: sender.nibBundle)
        setupAfterInitialize()
    }

    internal func setupAfterInitialize() {
        // do stuff
        loadView()
    }
}

And the code looks stupid, doesn't make convenience init convenience.

For now, I can't use the class function show(from:) but have move the presentation outside and make things like:

CommonVC.show(from: self)
SubClassVC(sender: self).present()

// instead of this simple presentation:
// SubClassVC.show(from: self)

I've even tried this but still not working:

class func show<T: CommonVC>(from sender: UIViewController) -> Self {
    T.init(nibName: type(of: self).className, bundle: sender.nibBundle)
    ....
Eddie
  • 1,878
  • 2
  • 20
  • 41
  • 1
    Possibly helpful: https://stackoverflow.com/questions/33200035/return-instancetype-in-swift. – Martin R Sep 27 '18 at 08:06
  • yes, yours is where I get the `-> Self` and `unsafedowncast(vc, to: self)`. I'll try derping around more.... – Eddie Sep 27 '18 at 08:20

1 Answers1

1

When you switch from Objective-C to Swift, it's tempting to simply translate your Objective-C style into Swift code. But Swift is fundamentally different in some ways.

It may be possible to implement a generic class which all your controllers are subclass of, but we tend to try and avoid inheritance in Swift where possible (in favour of protocols and extensions).

A good Swift rule of thumb, from Apple, is: "always start with a protocol"...

It's actually very easy to implement what you want using a protocol, and extension:

protocol Showable {
    init(className: String, bundle: Bundle?)
    static func show(from: UIViewController) -> Self
}

extension Showable where Self: UIViewController {

    init(className: String, bundle: Bundle?) {
        self.init(nibName: className, bundle: bundle)
    }

    static func show(from: UIViewController) -> Self {
        let nibName = String(describing: self)
        let instance = self.init(className: nibName, bundle: from.nibBundle)
        from.present(instance, animated: true, completion: nil)
        return instance
    }

}

In the above code, I've declared a Showable protocol and an extension that provides a default implementation that applies where the adopting class is an instance of UIViewController.

Finally, to provide this functionality to every single view controller in your project, simply declare an empty extension:

extension UIViewController: Showable { }

With these two short snippets of code added you can now do what you describe in your question (as long as an appropriately named nib exists for your view controller instance):

let secondController = SecondViewController.show(from: self)
ThirdController.show(from: secondController)

And that's the beauty of Swift. All your UIViewController subclasses now get this functionality for free; no inheritance required.

Pete Morris
  • 1,282
  • 4
  • 12
  • Thanks for the answer, I'll try your solution and mark it accepted. +1 for `A good Swift rule of thumb, from Apple, is: "always start with a protocol"`. – Eddie Sep 28 '18 at 06:58
  • 1
    No problem. By the way, if you're transitioning from many years working in Objective-C to Swift then I'd highly recommend the WWDC talk "Protocol-oriented programming": https://www.youtube.com/watch?v=g2LwFZatfTI – Pete Morris Sep 28 '18 at 07:07