Yesterday, was hanging out in #swift-lang, and Josh A asked how to create a generic type where 1. the same type was used for multiple members, and 2. that type was limited to UIView subclasses.
Answer:
struct MyStruct<T: UIView> { var view: T var handler: (T) -> Void }
(I should point out that this was Josh’s architecture before I suggested he just capture the view into a handler closure. But I’m going to keep using this example because it’s a good demonstration of constraining types.)
More after the fold…
Adding the type constraint to the generic argument token enables you to restrict the kind of subtypes that can be used to construct your custom type. In this example, you can build instances where the view type and the type of the first argument to the handler match:
let aSwitch = UISwitch() let aScrollView = UIScrollView() let switchExample = MyStruct(view: aSwitch, handler: { (theView: UISwitch) in print("Switch Example") }) let scrollExample = MyStruct(view: aScrollView, handler: { (theView: UIScrollView) in print("Scroll Example") }) // Run the examples switchExample.handler(switchExample.view) scrollExample.handler(scrollExample.view)
What you can’t do is mix and match the types. You’ll encounter a compiler error. Even though both the view and the handler argument use UIView subtypes in the following screenshot, they don’t use the same UIView subtype.
Which brings me to Josh’s next question, which was “How do I make an array of these examples and run them?”
You can’t just do a basic assignment:
let structs = [switchExample, scrollExample] // error // error: type of expression is ambiguous without more context
You can’t construct a heterogeneous collection of MyStruct
s:
let structs: [MyStruct] = [switchExample, scrollExample] // error // error: cannot convert value of type 'MyStruct<UISwitch>' to expected element type 'MyStruct'
or
let structs: [MyStruct<UIView>] = [switchExample, scrollExample] // error // error: cannot convert value of type 'MyStruct<UISwitch>' to expected element type 'MyStruct<UIView>'
You can construct an [Any]
array but then you can’t execute the handlers because of the typing issues. I’ll spare you all the ways you can fail at trying to get an array of Any
to try to cast to something that will run.
Update: I’ve re-written the following because I got it wrong the first time round.
To skip to the punchline, just create a function that allows you to apply the handler to the view and include it as part of a protocol:
protocol MyStructable { func perform() } extension MyStruct: MyStructable { func perform() { self.handler(self.view) } } var structs: [MyStructable] = [scrollExample, switchExample] for s in structs { s.perform() }
Establishing a protocol lets you create a heterogenous array typed with the protocol. You can then run the common function to get the results you need. Thanks to mbuchetics for pointing out my mistake!
Got a better solution (other than, of course, redesigning the entire exercise just to use a simple closure that captures the view)? Anything I missed or got wrong? Let me know.
6 Comments
This practice is actually quite very useful. I am able to constrain reusable cell to UITableViewCell subclass, and it works the same with UITableViewHeaderFooterView like this:
One interesting implementation that I also tested was:
Unfortunately, the compiler will yell at you for being ambiguous in the latter case because they are both AnyClass types.
The example doesn’t work because the cast to MyStruct will always fail (Could not cast value of type ‘MyStruct’ to ‘MyStruct’). At least with Xcode 7.2.1.
Dagnabit. Just tested: Works fine 7.2.1 in playground, and not on the iPhone simulator. Trying to fix now.
I tried it in a playground as well but it wasn’t working for me. There is no actual error, the conditional cast just never succeeds.
Updating the post to note this was just my error.
I think using a protocol method in this example is better than casting anyway.