Saturday, July 14, 2018

How do you make a method appear in the “overload set” only if another method can be called?

Leave a Comment

Consider an extension on NotificationCenter which does special things for notification objects that conform to certain protocols (yes these could be more common but it's an example to help demonstrate my real question).

extension NotificationCenter {    func addObserver<Note: BasicNotification>(using block: @escaping (Note) -> ())   -> NotificationToken {     let observer = addObserver(forName: Note.notificationName, object: nil,         queue: nil, using: { note in       block(Note(notification: note))     })     return NotificationToken(observer: observer, center: self)   }    func addObserver<Note: CustomNotification>(using block: @escaping (Note) -> ())   -> NotificationToken {     let observer = addObserver(forName: Note.notificationName, object: nil,         queue: nil, using: { note in       block(note.object as! Note)     })     return NotificationToken(observer: observer, center: self)   }  } 

Now, consider when we want this notification to only fire once, and then deregister...

extension NotificationCenter {    func observeOnce<Note: BasicNotification>(using block: @escaping (Note) -> ()) {     var token: NotificationToken!     token = addObserver(using: { (note: Note) in       block(note)       token.reset()     })   }    func observeOnce<Note: CustomNotification>(using block: @escaping (Note) -> ()) {     var token: NotificationToken!     token = addObserver(using: { (note: Note) in       block(note)       token.reset()     })   }  } 

This is the exact same code. What I really want is one observeOnce method - I don't want to have to write two of them.

If I don't use any conditional conformance...

  func observeOnce<Note>(using block: @escaping (Note) -> ()) {     var token: NotificationToken!     token = addObserver(using: { (note: Note) in       block(note)       token.reset()     })   } 

I get an error Cannot invoke 'addObserver' with an argument list of type '(using: (Note) -> ())' which makes perfect sense.

If I use a common base protocol (which both conform to)...

  func observeOnce<Note: SomeNotification>(using block: @escaping (Note) -> ()) {     var token: NotificationToken!     token = addObserver(using: { (note: Note) in       block(note)       token.reset()     })   } 

I get the exact same error, which does not quite make as much sense - I would expect something about the call being ambiguous rather than not existing at all.

Having seen the & operator mean conformance to multiple protocols, I did try using BasicNotification | CommonNotification in the extremely unlikely case where that may have some meaning... but of course it did not work.

I'v also tried a bunch of other alternatives, to no avail. What I am trying to do is have observeOnce be available to call if either of the others are available to call.

In C++, I would do something like this (didn't run it through a compiler - hopefully you get what I'm trying to do)...

template <typename T> auto observeOnce(std::function<void (T)> block) -> decltype(void(this->addObserver(std::move(block)))) {   // do my stuff here } 

The above code basically means that the function observeOnce only appears in the overload set if addObserver(std::move(block)) can be called.

So, what is the swift way of accomplishing the same thing?

1 Answers

Answers 1

One trick you could use is to reorganise your code. Instead of creating multiple (generic) addObserver methods inside the NotificationCenter, move them into your notification types (Basic & Custom Notification) and formalise them using a protocol. You can then extend this protocol with a single func to add the addOnce logic. When your Basic and Custom Notification implement this protocol, they automatically inherit this new addOnce functionality without the need for any duplicated code.

Refactoring scheme

Example

Here is an example on how to accomplish that idea:

First create a new protocol ObservableNotification that allows to add an observer block to a NotificationCenter.

protocol ObservableNotification {     static func addObserver(to center: NotificationCenter, using block: @escaping (Self)->Void) -> NotificationToken } 

Then, let your Notification protocols inherit from this ObservableNotification protocol

protocol NameableNotification {     static var notificationName: NSNotification.Name {get} }  protocol CustomNotification: NameableNotification, ObservableNotification {} protocol BasicNotification: NameableNotification, ObservableNotification {     init(notification: Notification) } 

And move your addObserver methods (from NotificationCenter) to the corresponding protocols as default implementations using a protocol extension:

extension BasicNotification {     static func addObserver(to center: NotificationCenter, using block: @escaping (Self)->Void) -> NotificationToken {         let observer = center.addObserver(forName: Self.notificationName, object: nil, queue: nil) { note in             block(Self(notification: note))         }         return NotificationToken(observer: observer, center: center)     } }  extension CustomNotification {     static func addObserver(to center: NotificationCenter, using block: @escaping (Self)->Void) -> NotificationToken {         let observer = center.addObserver(forName: Self.notificationName, object: nil, queue: nil) { note in             block(note.object as! Self)         }         return NotificationToken(observer: observer, center: center)     } } 

This way you can extend the ObservableNotification protocol with a default implementation for the observeOnce method and you will be able to call it on each type that conforms to ObservableNotification (CustomNotification and BasicNotification in your case). Like so:

extension ObservableNotification {     static func addOneTimeObserver(to center: NotificationCenter, using block: @escaping (Self)->Void) -> NotificationToken {         var token: NotificationToken!         token = addObserver(to: center) {             block($0)             token.reset()         }         return token     } } 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment