4

TL;DR

I'm looking for an array type (var array = [TheTypeImLookingFor]()) like 'all objects that subclasses UIViewController and implements the protocol MyProtocol.

Explanation

I'm building a kind of wizard view with a container view and embedded child views (controller). No problem, this will work as long, as I have only one base type of child view controllers.

Due to the content of screens, I have now a bunch of view controllers of type MyTableViewController which is a subclass of UITableViewController and other view controllers that have regular UIViewControllers as base.

All of the view controllers have one thing in common. A default data property myData: MyObject.

I created a protocol MyProtocol that contains this property.

Now, I have to combine all this view controllers into one array to use it as wizard steps. As long as I only have to access the view controller methods (array items are type of UIViewController) I'm able to use var viewControllers = [UIViewController]() or if I wanna only access the myData property, I change the array item type to MyObject.

But the problem is, I have to access the methods from the UIViewController and from the protocol.

That's why I'm looking for an array type like 'all objects that subclasses UIViewController and implements the protocol MyProtocol.

I tried:

  • var viewControllers = [UIViewController: MyProtocol]() // is a dict
  • `var viewControllers = UIViewController where MyProtocol
  • `var viewControllers = UIViewController.conforms(to: MyProtocol)
  • ...

But nothing works as expected.

2
  • Do the elements of the array have to be typed as both UIViewController and MyProtocol? Otherwise you could just stick the methods from UIViewController that you require in MyProtocol (or create a separate protocol for them and use protocol composition). Commented Feb 7, 2017 at 12:55
  • Yep, they have to be both. Maybe my idea of the implementation is wrong. Commented Feb 7, 2017 at 12:59

5 Answers 5

2

As far as I know, there's currently no way to type something so that it describes anything which inherits from a given class and conforms to a given protocol.

One possible hacky workaround would be to just create a wrapper type in order to perform typecasting for you in the case that you need to treat the instance as a MyProtocol.

struct MyProtocolViewController {

    let base: UIViewController

    init<T : UIViewController>(_ base: T) where T : MyProtocol {
        self.base = base
    }

    func asMyProtocol() -> MyProtocol {
        return base as! MyProtocol
    }
}

Now you can create a [MyProtocolViewController], and can either treat an element as a UIViewController, or a MyProtocol.

// given that ViewController and AnotherViewController conform to MyProtocol.
let viewControllers = [MyProtocolViewController(ViewController()),
                       MyProtocolViewController(AnotherViewController())]

for viewController in viewControllers {
    print(viewController.asMyProtocol().myData)
    print(viewController.base.prefersStatusBarHidden)
}
Sign up to request clarification or add additional context in comments.

5 Comments

Thanks a lot guys! You helped me with your ideas a lot. At the moment I work with 2 arrays one for the uncasted UIViewController objects and one for the casted (to the protocol) objects. This is ugly and stupid but it works at the moment.
@Tobonaut I wouldn't recommend using parallel arrays for this, although I do admit that there doesn't seem to be any 'perfect' solution to it.
hm, ok. Do you think I have the wrong "thoughts" about this? I'm a former Java EE developer and maybe I use the wrong pattern.
@Tobonaut Parallel arrays are generally considered to be an anti-pattern due to the fact that it makes it difficult to keep them in sync, and it separates data which should be together (e.g your controller typed as a UIViewController and a MyProtocol). It's nearly always preferable to contain these in a suitable data structure, such as a struct.
That sounds like a practical idea. I'll try it later. Thanks buddy.
1

You could use protocol composition with a placeholder protocol for the class:

protocol UIViewControllerClass {}
extension UIViewController: UIViewControllerClass {}

protocol MyProtocol:class {}

class MySpecialVC:UIViewController,MyProtocol {}    

var viewControllers = [UIViewControllerClass & MyProtocol]()

viewControllers.append( MySpecialVC() )   

This covers the type safety part but doesn't let you access UIViewController methods without type casting. You can reduce the type casting ugliness by adding a typed property to your protocol (when it applies to the base class)

extension MyProtocol where Self: UIViewControllerClass
{
   var vc:UIViewController { return self as! UIViewController }
}

// accessing the view controller's methods would then only require insertion of a property name.
viewControllers.first!.vc.view

Alternatively, you could define the UIViewController methods you need to call in the placeholder protocol but that could quickly become tiresome and redundant if you're going to use many of them.

Comments

0

Why not simply create :

Why not creating :

class ObservingViewController : UIViewController, MyProtocol {


}

var viewControllers : [ObservingViewController] = []

3 Comments

I believe the OP wants the elements of this array to know that they are also subclass objects of UIViewController, which will not be the case if only working with typed protocol objects.
Yep, @dfri. That's my problem.
One of OP's classes inherits from UITableViewController, so this won't work.
0

You can also create a protocol that defines all the UIViewController functions that you need. Make sure that you copy the method signature, otherwise you will have to implement the functions again.

protocol UIViewControllerInteractions {
    //copy the signature from the methods you want to interact with here, e.g.
    var title: String? { get set }
}

Then, you can extend your existing protocol.

protocol MyProtocol: UIViewControllerInteractions { }

Or create a new protocol that extends UIViewControllerInteractions and MyProtocol.

protocol MyProtocolViewController: UIViewControllerInteractions, MyProtocol { }

Now, when you extend your SubclassUIViewController, you still only have to add your myData because the methods in the UIViewControllerInteractions are already implemented by UIViewController (that's why we copied the method signature)

class SubclassUIViewController: MyProtocol {
    var myData ...
}

You can now have an array of MyProtocol or MyProtocolViewController and also call the methods defined in UIViewControllerInteractions which will call the UIViewController methods.

var viewController: [MyProtocol] = [...]
viewController.forEach { (vc) in
    print(vc.myData)
    print(vc.title)
}

2 Comments

OP says they need to be able to use the array's elements as UIViewController typed instances – I had the same thought too :)
I missed that part. Thanks :) Maybe this helps someone else with a similar problem
0

I had a similar issue and solved it with a custom base class. Imagine an array like:

var viewControllers: [MapViewController]

which all should extend from UIViewController and implement the following protocol:

protocol MapViewControllerDelegate {
    func zoomToUser()
}

Then I've declared a base class like:

class MapViewController: UIViewController {
    var delegate: MapViewControllerDelegate?
}

Caution: this class doesn't implement the above protocol but holds a property which provides the desired functionality. The next step is to define one of the UIViewController that will be added to the array:

class GoogleMapsViewController: MapViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
    }
}

extension GoogleMapsViewController: MapViewControllerDelegate {
    func zoomToUser() {
        // Place custom google maps code here
    }
}

The important part is located in the viewDidLoad method. The view controller assigns itself as the delegate.

Usage:

let googleMapsViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "GoogleMapsViewController") as! GoogleMapsViewController
let mapboxMapsViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "MapboxMapsViewController") as! MapboxMapsViewController
let mapViewControllers: [MapViewController] = [googleMapsViewController, mapboxViewController]
for mapVC in mapViewControllers {
    mapVC.delegate?.zoomToUser()
}

The benefits:

  • The MapViewController is like an abstract class and If I change the MapViewControllerDelegate the compiler forces me to implement the changes in the GoogleMapsViewController and in the MapboxMapsViewController.
  • If I need a second protocol I could just implement a second delegate property.
  • No type casting needed like in the other answers. Each UIViewController is still a UIViewController and provides all its methods.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.