Create an array of objects that implements a speci

2019-06-25 04:31发布

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.

5条回答
虎瘦雄心在
2楼-- · 2019-06-25 05:04

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.

查看更多
聊天终结者
3楼-- · 2019-06-25 05:12

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.
查看更多
We Are One
4楼-- · 2019-06-25 05:18

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)
}
查看更多
Bombasti
5楼-- · 2019-06-25 05:21

Why not simply create :

Why not creating :

class ObservingViewController : UIViewController, MyProtocol {


}

var viewControllers : [ObservingViewController] = []
查看更多
爱情/是我丢掉的垃圾
6楼-- · 2019-06-25 05:22

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)
}
查看更多
登录 后发表回答