Reputation: 8165
Summary:
I'd like to create a Class<T>
which would have a corresponding ClassDelegate
protocol with func<T>
in it.
Goal:
To reuse a single object and behavior with multiple object classes. Receive a delegate callback with already a specialized class, without the need to cast the object to a specific class to work with it.
Sample Code:
A protocol with a generic method:
protocol GenericTableControllerDelegate: AnyObject {
func controller<T>(controller: GenericTableController<T>, didSelect value: T)
}
A generic base UITableViewController
subclass:
open class GenericTableController<DataType>: UITableViewController {
weak var delegate: GenericTableControllerDelegate?
var data = [DataType]()
open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = data[indexPath.row]
delegate?.controller(controller: self, didSelect: item)
}
}
A specialized version of the GenericTableController
:
final class SpecializedTableController: GenericTableController<NSObject> {}
Client of the SpecializedTableController
- achieves the result, but requires type-casting to be able to access the specialized data type:
final class ClientOfTableController: UIViewController, GenericTableControllerDelegate {
// Works OK
func controller<T>(controller: GenericTableController<T>, didSelect value: T) {
if let value = value as? NSObject {
// Requires unwrapping and casting
}
}
}
Client of the SpecializedTableController
, with "where" requirement - the only problem that it doesn't compile
final class AnotherClientOfTableController: UIViewController, GenericTableControllerDelegate {
// Works OK
func controller<T>(controller: GenericTableController<T>, didSelect value: T) where T: NSObject {
// `value` is String
}
}
Type 'AnotherClientOfTableController' does not conform to protocol 'GenericTableControllerDelegate' Do you want to add protocol stubs?
Is there a way to have a protocol with a generic method and being able to have a concrete (specialized) type in that method implementation?
Are there close alternatives achieving satisfying similar requirement (having a generic class but being able to handle a concrete type in the delegate callback)?
Upvotes: 5
Views: 1390
Reputation: 299345
Your mistake is in the protocol:
protocol GenericTableControllerDelegate: AnyObject {
func controller<T>(controller: GenericTableController<T>, didSelect value: T)
}
This says that in order to be a GTCD, a type must accept any type T
passed to this function. But you don't mean that. You meant this:
public protocol GenericTableControllerDelegate: AnyObject {
associatedtype DataType
func controller(controller: GenericTableController<DataType>, didSelect value: DataType)
}
And then you wanted the delegate's DataType to match the table view's DataType. And that gets us into the world of PATs (protocols with associated types), type erasers, and generalized existentials (which don't exist yet in Swift), and really it just gets to be a mess.
While this is a use case that generalized existentials are particularly well suited for (if they're ever added to Swift), in a lot of cases you probably don't want this anyway. The delegation pattern is an ObjC pattern developed before the addition of closures. It used to be very hard to pass functions around in ObjC, so even very simple callbacks were turned into delegates. In most cases, I think Richard Topchiy's approach is exactly right. Just pass a function.
But what if you really want to keep the delegate style? We can (almost) do that. The one glitch is that you can't have a property called delegate
. You can set it, but you can't fetch it.
open class GenericTableController<DataType>: UITableViewController
{
// This is the function to actually call
private var didSelect: ((DataType) -> Void)?
// We can set the delegate using any implementer of the protocol
// But it has to be called `controller.setDelegate(self)`.
public func setDelegate<Delegate: GenericTableControllerDelegate>(_ d: Delegate?)
where Delegate.DataType == DataType {
if let d = d {
didSelect = { [weak d, weak self] in
if let self = self { d?.controller(controller: self, didSelect: $0) }
}
} else {
didSelect = nil
}
}
var data = [DataType]()
// and here, just call our internal method
open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = data[indexPath.row]
didSelect?(item)
}
}
This is a useful technique to understand, but I probably wouldn't use it in most cases. There's definitely a headache as you add more methods, if those methods reference DataType. You require a lot of boilerplate. Note that there's a bit of messiness due to passing self
to the delegate method. That's something delegate methods need, but closures do not (you can always capture the controller in the closure if the closure needs it).
As you explore this kind of reusable code, I encourage you to think more about encapsulating strategies rather than about objects and delegate protocols. An example of encapsulating a strategy would be to have a SelectionHandler type that you hand to the controller:
struct SelectionHandler<Element> {
let didSelect: (Element) -> Void
}
With that, you can build simple strategies like "print it out:"
extension SelectionHandler {
static func printSelection() -> SelectionHandler {
return SelectionHandler { print($0) }
}
}
Or more interestingly, update a label:
static func update(label: UILabel) -> SelectionHandler {
return SelectionHandler { [weak label] in label?.text = "\($0)" }
}
So then you get code like:
controller.selectionHandler = .update(label: self.nameLabel)
Or, even more interestingly, you can build higher-order types:
static func combine(_ handlers: [SelectionHandler]) -> SelectionHandler {
return SelectionHandler {
for handler in handlers {
handler.didSelect($0)
}
}
}
static func trace(_ handler: SelectionHandler) -> SelectionHandler {
return .combine([.printSelection(), handler])
}
controller.selectionHandler = .trace(.update(label: self.nameLabel))
This approach composes much more powerfully than delegation, and starts to unlock the real advantages of Swift.
Upvotes: 5
Reputation: 8165
On of the possible ways to resolve this situation is to use callbacks instead of delegation. By passing not a closure but a instance method it looks almost identical to the delegation pattern:
open class GenericTableController2<DataType>: UITableViewController {
var onSelect: ((DataType) -> Void)?
var data = [DataType]()
open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = data[indexPath.row]
onSelect?(item)
}
}
final class CallbackExample: GenericTableController2<NSObject> {
}
final class CallBackClient: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let vc = CallbackExample()
vc.onSelect = handleSelection
}
func handleSelection(_ object: NSObject) {
}
}
As a benefit, the code is pretty straightforward and doesn't involve any advanced workarounds for Swift's type system which often has some issues when dealing with generics and protocols.
Upvotes: 1
Reputation: 16774
I don't think this is doable in a sense you want it. The closest would be combining with subclass. Consider the following:
protocol MagicProtocol {
func dooMagic<T>(_ trick: T)
}
class Magician<TrickType> {
private let listener: MagicProtocol
private let tricks: [TrickType]
init(listener: MagicProtocol, tricks: [TrickType]) { self.listener = listener; self.tricks = tricks }
func abracadabra() { listener.dooMagic(tricks.randomElement()) }
}
class Audience<DataType>: MagicProtocol {
var magician: Magician<DataType>?
init() {
magician?.abracadabra()
}
func doExplicitMagic(_ trick: DataType) {
}
func dooMagic<T>(_ trick: T) {
doExplicitMagic(trick as! DataType)
}
}
Now I can create a subclass and restrict it to some type:
class IntegerAudience: Audience<Int> {
override func doExplicitMagic(_ trick: Int) {
print("This works")
}
}
The problem is that there is no correlation between the 2 generics. So at some point a cast must be done. Here we do it in protocol method:
doExplicitMagic(trick as! DataType)
it seems like this is pretty safe and it could never crash but if you look a bit closer we could do this:
func makeThingsGoWrong() {
let myAudience = IntegerAudience()
let evilMagician = Magician(listener: myAudience, tricks: ["Time to burn"])
evilMagician.abracadabra() // This should crash the app
}
Here myAudience
corresponds to protocol MagicProtocol
which may not be restricted to generic. But myAudience
is restricted to Int
. Nothing is stopping the compiler but if it did, what would the error be?
Anyway, it works as long as you use it correctly. If you don't then it will crash. You could do an optional unwrap but I am not sure it is appropriate.
Upvotes: 1