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.

Hamish
  • 78,605
  • 19
  • 187
  • 280
Tobonaut
  • 2,245
  • 2
  • 26
  • 39
  • 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). – Hamish Feb 07 '17 at 12:55
  • Yep, they have to be both. Maybe my idea of the implementation is wrong. – Tobonaut Feb 07 '17 at 12:59

5 Answers5

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)
}
Hamish
  • 78,605
  • 19
  • 187
  • 280
  • 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 Feb 07 '17 at 15:09
  • @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. – Hamish Feb 07 '17 at 16:00
  • 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 Feb 07 '17 at 19:08
  • 2
    @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. – Hamish Feb 07 '17 at 21:39
  • That sounds like a practical idea. I'll try it later. Thanks buddy. – Tobonaut Feb 08 '17 at 07:27
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.

Alain T.
  • 40,517
  • 4
  • 31
  • 51
0

Why not simply create :

Why not creating :

class ObservingViewController : UIViewController, MyProtocol {


}

var viewControllers : [ObservingViewController] = []
CZ54
  • 5,488
  • 1
  • 24
  • 39
  • 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. – dfrib Feb 07 '17 at 12:48
  • Yep, @dfri. That's my problem. – Tobonaut Feb 07 '17 at 12:58
  • One of OP's classes inherits from `UITableViewController`, so this won't work. – Hamish Feb 07 '17 at 13:46
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)
}
Yannick
  • 3,210
  • 1
  • 21
  • 30
  • [OP says](http://stackoverflow.com/questions/42090028/create-an-array-of-objects-that-implements-a-specific-protocol#comment71351002_42090028) they need to be able to use the array's elements as `UIViewController` typed instances – I had the same thought too :) – Hamish Feb 07 '17 at 16:15
  • I missed that part. Thanks :) Maybe this helps someone else with a similar problem – Yannick Feb 07 '17 at 17:09
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.
Raimund Wege
  • 441
  • 4
  • 19