[Pitch] Make NSNotification names a protocol like ErrorProtocol


(Kenny Leung) #1

Hi All.

While converting to Swift3, my biggest hurdle was figuring out what to do with a custom NSNotification. In Objective-C, it’s a plain NSString, and the same in Swift2.

I assumed in Swift3 that it worked like ErrorProtocol and errors as enums, but as it turns out, you need to create a custom instance of Notification.Name(“MyNotificationName”). This does not seem very Swifty, and I think it would work better if there was a NotificationNameProtocol and you could create enums with String raw values.

Magically, a string seems to compile in one area of the API, but not another:

// This compiles with either a string or instance of Notification.Name for name:
        NotificationCenter.default().addObserver(self, selector: #selector(Self._contextDidSave(notification:)), name: "blah", object: managedObjectContext)

// This will only compile with an instance of Notification.Name:
                NotificationCenter.default().post(name: SomeNotificationObject, object: self)

-Kenny


(Brent Royal-Gordon) #2

While converting to Swift3, my biggest hurdle was figuring out what to do with a custom NSNotification. In Objective-C, it’s a plain NSString, and the same in Swift2.

I assumed in Swift3 that it worked like ErrorProtocol and errors as enums, but as it turns out, you need to create a custom instance of Notification.Name(“MyNotificationName”). This does not seem very Swifty, and I think it would work better if there was a NotificationNameProtocol and you could create enums with String raw values.

The migrator constructs the Notification.Name instances at the call site, but I believe the *actual* intent is that you should assign them to a constant—either one in your own type:

  // Old
  let MyClassWillFooNotificationName = "MyClassWillFoo"
  // New:
  class MyClass: … {
    static let WillFoo = Notification.Name("MyClass.WillFoo")
  }

Or in an extension to `Notification.Name` itself.

···

--
Brent Royal-Gordon
Architechies


(Kenny Leung) #3

Yes, this is the solution that I’m using, but:

1. I think following the pattern of ErrorProtocol would be better

2. It doesn’t explain why I can use String in one instance and not the other when they are both typed the same:

// Can use String or Notification.Name
    @nonobjc final public func addObserver(_ observer: AnyObject, selector aSelector: Selector, name aName: Name, object anObject: AnyObject?)
// Can only use Notification.Name
    public func post(name aName: NSNotification.Name, object anObject: AnyObject?)

-Kenny

···

On Jun 29, 2016, at 5:11 PM, Brent Royal-Gordon <brent@architechies.com> wrote:

While converting to Swift3, my biggest hurdle was figuring out what to do with a custom NSNotification. In Objective-C, it’s a plain NSString, and the same in Swift2.

I assumed in Swift3 that it worked like ErrorProtocol and errors as enums, but as it turns out, you need to create a custom instance of Notification.Name(“MyNotificationName”). This does not seem very Swifty, and I think it would work better if there was a NotificationNameProtocol and you could create enums with String raw values.

The migrator constructs the Notification.Name instances at the call site, but I believe the *actual* intent is that you should assign them to a constant—either one in your own type:

  // Old
  let MyClassWillFooNotificationName = "MyClassWillFoo"
  // New:
  class MyClass: … {
    static let WillFoo = Notification.Name("MyClass.WillFoo")
  }

Or in an extension to `Notification.Name` itself.

--
Brent Royal-Gordon
Architechies


(Brent Royal-Gordon) #4

1. I think following the pattern of ErrorProtocol would be better

I don't think so. ErrorProtocol bridging uses the `domain` to figure out which concrete ErrorProtocol type to use, but NSNotification names are just plain strings with no internal structure, so it wouldn't be possible to bridge them back in the same way. Nor do you typically receive a bunch of unrelated notifications in one place and then have to sort them out into broad buckets in the same way—usually NotificationCenter handles all the testing for you. The resemblance between error enums and notification name constants is superficial.

2. It doesn’t explain why I can use String in one instance and not the other when they are both typed the same:

// Can use String or Notification.Name
   @nonobjc final public func addObserver(_ observer: AnyObject, selector aSelector: Selector, name aName: Name, object anObject: AnyObject?)
// Can only use Notification.Name
   public func post(name aName: NSNotification.Name, object anObject: AnyObject?)

The generated headers show two overloads:

    public func addObserver(_ observer: AnyObject, selector aSelector: Selector, name aName: String?, object anObject: AnyObject?)
    @nonobjc final
    public func addObserver(_ observer: AnyObject, selector aSelector: Selector, name aName: Name, object anObject: AnyObject?)

This looks like a bug to me—there ought to be one method which takes an optional Name, with the String variant hidden using the NS_REFINED_FOR_SWIFT macro. I'd file a radar if I were you.

···

On Jun 29, 2016, at 6:07 PM, Kenny Leung via swift-evolution <swift-evolution@swift.org> wrote:

--
Brent Royal-Gordon
Architechies


(Douglas Gregor) #5

1. I think following the pattern of ErrorProtocol would be better

I don't think so. ErrorProtocol bridging uses the `domain` to figure out which concrete ErrorProtocol type to use, but NSNotification names are just plain strings with no internal structure, so it wouldn't be possible to bridge them back in the same way. Nor do you typically receive a bunch of unrelated notifications in one place and then have to sort them out into broad buckets in the same way—usually NotificationCenter handles all the testing for you. The resemblance between error enums and notification name constants is superficial.

Right. ErrorProtocol is deeply ties with the Swift error-handling model and its interoperability with Objective-C.

2. It doesn’t explain why I can use String in one instance and not the other when they are both typed the same:

// Can use String or Notification.Name
  @nonobjc final public func addObserver(_ observer: AnyObject, selector aSelector: Selector, name aName: Name, object anObject: AnyObject?)
// Can only use Notification.Name
  public func post(name aName: NSNotification.Name, object anObject: AnyObject?)

The generated headers show two overloads:

   public func addObserver(_ observer: AnyObject, selector aSelector: Selector, name aName: String?, object anObject: AnyObject?)
   @nonobjc final
   public func addObserver(_ observer: AnyObject, selector aSelector: Selector, name aName: Name, object anObject: AnyObject?)

This looks like a bug to me—there ought to be one method which takes an optional Name, with the String variant hidden using the NS_REFINED_FOR_SWIFT macro. I'd file a radar if I were you.

This was an artifact of of an elaborate Foundation/Swift dance that didn't reach its conclusion for the first preview. It'll be fixed (with the string version going away and the other's name becoming optional).

  - Doug

···

Sent from my iPhone
On Jun 29, 2016, at 6:51 PM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

On Jun 29, 2016, at 6:07 PM, Kenny Leung via swift-evolution <swift-evolution@swift.org> wrote:

--
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution