14

I'd like to create a reusable view controller UsersViewControllerBase.

UsersViewControllerBase extends UIViewController, and implements two delegates (UITableViewDelegate, UITableViewDataSource), and has two views (UITableView, UISegmentedControl)

The goal is to inherit the implementation of the UsersViewControllerBase and customise the segmented items of segmented control in UsersViewController class.

class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource{
  @IBOutlet weak var segmentedControl: UISegmentedControl!
  @IBOutlet weak var tableView: UITableView!
  //implementation of delegates
}

class UsersViewController: UsersViewControllerBase {
}

The UsersViewControllerBase is present in the storyboard and all outlets are connected, the identifier is specified.

The question is how can I init the UsersViewController to inherit all the views and functionality of UsersViewControllerBase

When I create the instance of UsersViewControllerBase everything works

let usersViewControllerBase = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewControllerBase") as? UsersViewControllerBase

But when I create the instance of UsersViewController I get nil outlets (I created a simple UIViewController and assigned the UsersViewController class to it in the storyboard )

let usersViewController = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewController") as? UsersViewController

It looks like views are not inherited.

I would expect init method in UsersViewControllerBase that gets controller with views and outlets from storyboard:

  class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource{
      @IBOutlet weak var segmentedControl: UISegmentedControl!
      @IBOutlet weak var tableView: UITableView!
      init(){
        let usersViewControllerBase = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewControllerBase") as? UsersViewControllerBase
        self = usersViewControllerBase //but that doesn't compile
      }
    }

And I would init UsersViewController:

let usersViewController = UsersViewController()

But unfortunately that doesn't work

Alexey
  • 7,127
  • 9
  • 57
  • 94
  • Well, they _are_ inherited, so something else is going wrong (like maybe the setup of your storyboard scene). Did you remember to _connect_ those outlets? – matt Nov 19 '15 at 16:25
  • no I didn't connect outlets in the UsersViewController because I thought they are already connected (so probably inherited) in the UsersViewControllerBase. And the idea is not to recreate the same view in the UsersViewController but inherit it from UsersViewControllerBase – Alexey Nov 19 '15 at 16:32

3 Answers3

7

When you instantiate a view controller via instantiateViewControllerWithIdentifier, the process is essentially as follows:

  • it finds a scene with that identifier;
  • it determines the base class for that scene; and
  • it returns an instance of that class.

And then, when you first access the view, it will:

  • create the view hierarchy as outlined in that storyboard scene; and
  • hook up the outlets.

(The process is actually more complicated than that, but I'm trying to reduce it to the key elements in this workflow.)

The implication of this workflow is that the outlets and the base class are determined by the unique storyboard identifier you pass to instantiateViewControllerWithIdentifier. So for every subclass of your base class, you need a separate storyboard scene and have hooked up the outlets to that particular subclass.

There is an approach that will accomplish what you've requested, though. Rather than using storyboard scene for the view controller, you can instead have the view controller implement loadView (not to be confused with viewDidLoad) and have it programmatically create the view hierarchy needed by the view controller class. Apple used to have a nice introduction to this process in their View Controller Programming Guide for iOS, but have since retired that discussion, but it can still be found in their legacy documentation.

Having said that, I personally would not be compelled to go back to the old world of programmatically created views unless there was a very compelling case for that. I might be more inclined to abandon the view controller subclass approach, and adopt something like a single class (which means I'm back in the world of storyboards) and then pass it some identifier that dictates the behavior I want from that particular instance of that scene. If you want to keep some OO elegance about this, you might instantiate custom classes for the data source and delegate based upon some property that you set in this view controller class.

I'd be more inclined to go down this road if you needed truly dynamic view controller behavior, rather than programmatically created view hierarchies. Or, even simpler, go ahead and adopt your original view controller subclassing approach and just accept that you'll need separate scenes in the storyboard for each subclass.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Very good explanation, thanks a lot. I prefer to use "subclassing approach" and separate scenes for every subclass. At least I avoid code duplication and copy-pasting equal scenes in storyboard is not a big deal. – Alexey Nov 19 '15 at 18:32
  • In case you still want to have the ability of subclassing VC with outlets, you can take the .xib approach. Create a .xib with all the views and outlets, and create a class for it. Now, `UserViewControllerBase` will add this .xib to it's `self.view` inside `viewDidLoad()` and have the custom view as a member. then, `UsersViewController` will inherit the base class and will have the custom view from its superclass, and it will have access to all the outlets through the member custom view. Personally, I don't love this solution, but it better than duplicating a storyboard VC – gutte Jun 14 '17 at 14:33
7

So, you have your base class:

class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource {
    @IBOutlet weak var segmentedControl: UISegmentedControl!
    @IBOutlet weak var tableView: UITableView!
    //implementation of delegates
}

[A] And your subclass: class UsersViewController: UsersViewControllerBase { var text = "Hello!" }

[B] A protocol that your subclass will be extending:

protocol SomeProtocol {
    var text: String? { get set }
}

[C] And some class to handle your data. For example, a singleton class:

class MyDataManager {

    static let shared = MyDataManager()
    var text: String?

    private init() {}

    func cleanup() {
        text = nil
    }
}

[D] And your subclass:

class UsersViewController: UsersViewControllerBase {

    deinit {
        // Revert
        object_setClass(self, UsersViewControllerBase.self)
        MyDataManager.shared.cleanup()
    }
}

extension UsersViewController: SomeProtocol {
    var text: String? {
        get {
            return MyDataManager.shared.text
        }
        set {
            MyDataManager.shared.text = newValue
        }
    }
}

To properly use the subclass, you need to do (something like) this:

class TestViewController: UIViewController {
    ...
    func doSomething() {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        //Instantiate as base
        let usersViewController = storyboard.instantiateViewControllerWithIdentifier("UsersViewControllerBase") as! UsersViewControllerBase
        //Replace the class with the desired subclass
        object_setClass(usersViewController, UsersViewController.self)
        //But you also need to access the property 'text', so:
        let subclassObject = usersViewController as! UsersViewController
        subclassObject.text = "Hello! World."
        //Use UsersViewController object as desired. For example:
        navigationController?.pushViewController(subclassObject, animated: true)
    }
}

EDIT:

As pointed out by @VyachaslavGerchicov, the original answer doesn't work all the time so the section marked as [A] was crossed out. As explained by an answer here:

object_setClass in Swift

... setClass cannot add instance variables to an object that has already been created.

[B], [C], and [D] were added as a work around. Another option to [C] is to make it a private inner class of UsersViewController so that only it has access to that singleton.

yoninja
  • 1,952
  • 2
  • 31
  • 39
  • Did you try it by yourself? It works so so - it seems it allows you to override the existing methods but you are not allowed to add variables like `[CustomObject]` - it fails with EXC_BAD_ACCESS. – Vyachaslav Gerchicov Feb 05 '21 at 09:29
  • @VyachaslavGerchicov, Hi! I used this approach before and it worked. I'm not sure why it doesn't work for you. Can you create a new question regarding your issue so we can address it accordingly? Maybe you can link to this question. – yoninja Feb 08 '21 at 09:01
  • just declare `var items: [YourCustomObject]!` in `UsersViewController` and assign it in your `doSomething()`. It fails on assigning. Tried the same with `String` and other system classes - it works 50/50 - sometimes it fails unpredictably – Vyachaslav Gerchicov Feb 08 '21 at 10:31
0

The problem is that you created a scene in the storyboard, but you didn't give the view controller's view any subviews or connect any outlets, so the interface is blank.

If your goal is to reuse a collection of views and subviews in connection with instances of several different view controller classes, the simplest way, if you don't want to create them in code, is to put them in a .xib file and load it in code after the view controller's own view-loading process (e.g. in viewDidLoad).

But if the goal is merely to "customise the segmented items of segmented control" in different instances of this view controller, the simplest approach is to have one view controller class and one corresponding interface design, and perform the customization in code. However, you could load just that segmented control from its own .xib in each case, if it's important to you design it visually.

matt
  • 515,959
  • 87
  • 875
  • 1,141