Add `intermediate` keyword to subclass definition to mark that the actual direct superclass is unknown when the subclass is defined

Hello all.

This is my first post on Swift Forums.
I don't know if a proposal that addresses the similar problem already exists or not, so please forgive me if that's the case.

Introduction

Add intermediate keyword to subclass definition to mark that the actual direct superclass is unknown when the subclass is defined.

Motivation

It's not uncommon when developing an iOS app to define base view controller subclasses that you want all the view controllers in the project to inherit from.

class BaseViewController: UIViewController {
    var myInt: Int
    var myString: String

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do extra work that is common for view controllers in this app
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // Do extra work that is common for view controllers in this app
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // Do extra work that is common for view controllers in this app
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // Do extra work that is common for view controllers in this app
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        // Do extra work that is common for view controllers in this app
    }
}

Now, soon you'll find that you also need to write a subclass of UITableViewController too.
So the naive approach would be to copy the code above and change the class name and the class it inherits from.

class BaseTableViewController: UITableViewController {
    var myInt: Int
    var myString: String

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do extra work that is common for view controllers in this app
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // Do extra work that is common for view controllers in this app
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // Do extra work that is common for view controllers in this app
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // Do extra work that is common for view controllers in this app
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        // Do extra work that is common for view controllers in this app
    }
}

But what about UICollectionViewController, UINavigationController, UITabBarController, UIPageViewController or even view controllers that don't yet exist.
The code above will have to be repeated again and again.
You can already see how painful this could become.
And when we change one part of the code, we have to make sure all of them are properly synchronized.

Of course, we can use protocol extension to ease this situation, but we still have to override methods to forward the work to the extension.
And if we add a new overriden method, says viewWillLayoutSubviews(), we'll have to update all the subclasses to forward the work too.
Also, since we cannot define instant variables in the extension, it can become quite tedious when we want to have private instant variables to keep track of the internal state.

Proposed solution

We add a new intermediate keyword to the subclass definition.

intermediate class IntermediateBaseViewController: UIViewController {
    var myInt: Int
    var myString: String

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do extra work that is common for view controllers in this app
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // Do extra work that is common for view controllers in this app
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // Do extra work that is common for view controllers in this app
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // Do extra work that is common for view controllers in this app
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        // Do extra work that is common for view controllers in this app
    }
}

This will mark that this subclass's actual direct superclass is unknown at the time it's defined.
We can write the code for this subclass as if it's just a normal direct subclass of UIViewController.
But like protocol, we cannot instantiate an object from this subclass.

Now we add the following code to define all the subclasses that we want to inherit from different type of view controllers.
We add the actual subclass of UIViewController that we want IntermediateBaseViewController to actually inherits from in the parentheses.

class BaseViewController: IntermediateBaseViewController(UIViewController) { }
class BaseTableViewController: IntermediateBaseViewController(UITableViewController) { }
class BaseCollectionViewController: IntermediateBaseViewController(UICollectionViewController) { }

The result would be the same as if the code below is generated by the compiler:

class IntermediateBaseViewController_UIViewController: UIViewController {
    // copied all the code from the IntermediateBaseViewController's declaration
}

class BaseViewController: IntermediateBaseViewController_UIViewController { }

class IntermediateBaseViewController_UITableViewController: UITableViewController {
    // copied all the code from the IntermediateBaseViewController's declaration
}

class BaseTableViewController: IntermediateBaseViewController_UITableViewController { }

class IntermediateBaseViewController_UICollectionViewController: UICollectionViewController {
    // copied all the code from the IntermediateBaseViewController's declaration
}

class BaseCollectionViewController: IntermediateBaseViewController_UICollectionViewController { }

And of course, each of the subclasses are free to add additional methods or instant variables.
They can also, call methods and use instant variables defined in IntermediateBaseViewController.
They can even override the methods that're defined in IntermediateBaseViewController since it's not different from overriding any methods from superclass.

class BaseViewController: IntermediateBaseViewController(UIViewController) {
    var myDouble: Double

    func myMethod() {
        // ..
    }

    func override viewDidLoad() {
        super.viewDidLoad()
        // Do additional work
    }
}

Additional consideration

We can make intermediate subclass able to conform to protocols, and all the subclasses would conform to those protocols as well.

class RootClass {
    // ...
}

class SubclassOne: RootClass {
    // ...
}

protocol MyProtocol {
    // ...
}

intermediate class IntermediateBaseClass: RootClass, MyProtocol {
    // Add method to conform to MyProtocol
}

class SubclassTwo: IntermediateBaseClass(RootClass) { }

class SubclassThree: IntermediateBaseClass(SubclassOne) { }

In this case SubclassTwo and SubclassThree will conform to MyProtocol without additional work because its superclass has already adopted that protocol.

Source compatibility

This is a additive proposal with no source breaking changes.

Effect on ABI stability

My knowledge on Swift compiler is limited, but I think if the compiler can generate the code explained above, there shouldn't be any problems with ABI stability.

1 Like

I guess barely anyone reads all posts, and many messages are simply ignored - so I doubt anyone will be angry at you, even if the exact same issue just has been discussed (which afaik isn't the case ;-)

I just had the situation you described... but only with two classes, and one or two methods.
It was also about UIViewController, so this might be a Cocoa-specific thing that isn't that relevant for other uses of Swift.
It's also a feature that I would delay until there is a roadmap for metaprogramming facilities, instead of trying to add completely new syntax now.

But seeing your example, I wonder if this could be implemented by lifting a limitation of generics:

intermediate class IntermediateBaseViewController: UIViewController
class BaseTableViewController: IntermediateBaseViewController(UITableViewController) { }

could be expressed as

class IntermediateBaseViewController<Parent: UIViewController>: Parent
class BaseTableViewController: IntermediateBaseViewController<UITableViewController> {}

It might be more complicated because of final classes, but afaics, that is a general issue.

4 Likes

Oh, how much I feel. I remember defining all those base classes as well. Analytics, convenience methods, etc, there is a lot to put in them.

I've stopped doing this because I felt more pain than gain eventually. I guess I write more boilerplate now, but I also enjoy that view controllers are more isolated.

Yet I'll be very interested in reading the various answers and experiences from other members: you have put the finger on a real iOS app design challenge.

This somehow reminds me of Kotlin's delegation by pattern. https://kotlinlang.org/docs/reference/delegation.html

My personal take on the topic: I think this is more a question about application design than a language issue.

It's been a long time this subject hasn't been a real itch in my developer life, but would it become one again, I would explore view controller containment as a general solution:

Instead of showing/presenting/pushing a FooViewController subclass of MyBaseViewController, I'd show/present/push a MyContainerController that wraps a FooViewController, plain subclass of UIViewController. The same technique applies to UITableViewControler subclasses, etc: the container is intended to fully replace the intermediate subclasses you mention.

MyContainerController would perform its custom logic in viewDidLoad, viewWillAppear, etc, and forward those methods appropriately to its child view controller. MyContainerController would store as many properties it needs in order to perform its tasks.

When customized cooperation is needed between both classes, I'd use a good old protocol for the child view controller, and a general UIViewController.myContainerController property, similar to UIViewController.navigationController (returns an optional, looks up the view controller hierarchy until it finds one).

Edit: view controller containment is introduced in chapter "Implementing a Container View Controller" at https://developer.apple.com/documentation/uikit/uiviewcontroller. Let's not lie: it's as wide and non trivial as it is powerful.

4 Likes

On a side note, I'd like to recommend this sensible and inspiring series by @davedelong: https://davedelong.com/blog/2017/11/06/a-better-mvc-part-1-the-problems/ - it helps taking a step back when we contemplate UIViewController design issues (we all face them).

1 Like

How close does this get you to a solution?

// When you want to use this, declare your class as e.g.
// class MyViewController: UITableViewController, IntermediateBaseViewController
protocol IntermediateBaseViewController: class {
    var myInt: Int { get set }
    var myString: String { get set }
}
extension IntermediateBaseViewController where Self: UIViewController {
    @objc override func viewDidLoad() {
        super.viewDidLoad()
        // Do extra work that is common for view controllers in this app
    }

    @objc override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // Do extra work that is common for view controllers in this app
    }

    @objc override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // Do extra work that is common for view controllers in this app
    }

    @objc override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // Do extra work that is common for view controllers in this app
    }

    @objc override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        // Do extra work that is common for view controllers in this app
    }
}

I'm not sure this will work, but it might, or it might get close to working. (For example, it might work if you delete the override keywords, or add @objc to the protocol, etc. Or you might have to put the overrides into conforming types manually, but you can make them call extension methods like doViewWillAppear(_:) which contain all the logic.)

Hi, thank you for replying.

Actually this is what I went with and it's gotten me a long way. In my case it looks more like this:

@objc private protocol _ViewControllerImplementation {
    var _myInt: Int { get set }
    var _myDouble: Double { get set }
    var _myBool: Bool { get set }
    var _myString: String { get set }
}

private extension _ViewControllerImplementation where Self: UIViewController {
    func _viewDidLoad() {
        // Implementation
    }

    func _viewDidAppear(_ animated: Bool) {
        // Implementation
    }

    func _viewDidDisappear(_ animated: Bool) {
        // Implementation
    }

    func _shouldPerformSegue(withIdentifier identifier: String, sender: Any?) {
        // Implementation
    }
}

And then repeat the code below for every subclasses.

class MyViewController: UIViewController, _ViewControllerImplementation {
    fileprivate var _myInt: Int = 0
    fileprivate var _myDouble: Double = 0
    fileprivate var _myBool: Bool = false
    fileprivate var _myString: String = ""

    override func viewDidLoad() {
        super.viewDidLoad()
        _viewDidLoad()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        _viewDidAppear(animated)
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        _viewDidDisappear(animated)
    }

    override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
        let shouldPerformSegue = super.shouldPerformSegue(withIdentifier: identifier, sender: sender)
        _shouldPerformSegue(withIdentifier: identifier, sender: sender)
        return shouldPerformSegue
    }
}

There're still some drawbacks, but it's a lot improvement over copy-and-pasted approach.

  • As you mentioned, override doesn't work in this case (or it could but I don't know how).
  • super cannot be called from protocol extension. (again I might be wrong but I couldn't get it to work)
  • When I need a private instant variable to keep track of state, I have to define it in the protocol declaration and then repeat it in all subclasses.
  • For every new overridden methods, I have to add the interception point in all subclasses to forward the call to the implementation.

I have at times wanted to make something like this, but the compiler doesn't let you use a generic type parameter as a superclass:

class MyWrapper<T> : T where T: UIViewController {
  // overrides...
}
extension MyWrapper where T: UITableViewController {
  // UITVC overrides..
}

let wrappedNav: MyWrapper<UINavigationController> // **is** a UINavigationController
1 Like

I think this is brilliant! And it would fit Swift more properly than my proposed solution.

1 Like

I'm probably not understanding this proposal.

It looks like module inclusion, like Ruby.
I could create a module with methods in it, and then just include it to certain types to make them have these methods.