Message.Subject
has no conformance requirements in its protocol, but addObserver()
and post()
both refine Message.Subject
to either conform to AnyObject
or confirm to Identifiable
where Identifiable.ID == ObjectIdentifier
.
Is this identifier at any point converted to the object reference? ObjectIdentifier
does not guarantee that it is an identifier of an object that is still alive. The following produces a valid ObjectIdentifier
, but it cannot be converted back to the object:
func getID() -> ObjectIdentifier {
return ObjectIdentifier(MyClass())
}
The messages()
sequence uses a reasonably-sized buffer to reduce the likelihood of dropped messages caused by the interaction of synchronous and asynchronous code.
That does not sound reliable. Dropping message can lead to logical bugs. When prioritising between correctness and performance, I strongly prefer correctness first. I would prefer to follow the precedent of the AsyncStream.Continuation.BufferingPolicy
. I.e. default is .unbounded
, when enabling bounded buffer it is possible to choose which messages are kept and which are discarded.
struct EventDidOccur: NotificationCenter.Message {
var foo: Foo
...
static func makeMessage(_ notification: Notification) -> Self? {
guard let foo = notification.userInfo["foo"] as? Foo else { return nil }
return Self(foo: foo)
}
static func makeNotification(_ message: Self) -> Notification {
return Notification(name: Self.name, object: object, userInfo: ["foo": self.foo])
}
}
Where did the object
come from? Is it part of the EventDidOccur
? If so, then why is it not parsed in the makeMessage()
?
public func post<Message: AsyncMessage>(_ message: Message, subject: Message.Subject)
where Message.Subject: AnyObject
public func post<Message: AsyncMessage>(_ message: Message, subject: Message.Subject)
where Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier
public func post<Message: AsyncMessage>(_ message: Message, subject: Message.Subject.Type = Message.Subject.self)
Would be nice to be able to express in the Message
declaration if it is posted with an instance or a type as a subject and check that post()
is used correctly in the compile time.
One possible way to encode this in the type system would be something like this:
public protocol MessageSubject {}
public struct InstanceSubject<T: AnyObject> : MessageSubject {
public init(_ instance: T)
}
public struct TypeSubject<T> : MessageSubject {
public init()
}
public struct IdentifiableSubject<T: Identifiable> : MessageSubject where T.ID == ObjectIdentifier {
public init(_ value: T)
}
public protocol Message {
associatedtype Subject: MessageSubject
...
}
extension NotificationCenter {
public func post<Message: AsyncMessage>(_ message: Message, subject: Message.Subject)
public func post<Message: AsyncMessage, T>(_ message: Message, subject: T) where Message.Subject == InstanceSubject<T>
public func post<Message: AsyncMessage, T>(_ message: Message, subject: T) where Message.Subject == CustomSubject<T>
public func post<Message: AsyncMessage, T>(_ message: Message) where Message.Subject == ClassSubject<T>
}
These methods do not need to be implemented if all posters and observers are using NotificationCenter.Message
.
IMO, this is fragile. This requires looking into implementation of the specific message to understand if it supports interoperability or not. And the fact that interoperability is optional was a surprise for me. Without reading the proposal text, just based on the API itself, I would not have guessed that this is something that I need to check.
I would prefer to either:
-
Make interoperability required and provide something like EmptyMessage
protocol providing default implementations for trivial cases only:
protocol EmptyMessage: Message {
init()
}
extension EmptyMessage {
static func makeMessage(_ notification: Notification) -> Self? {
return .some(.init())
}
static func makeNotification(_ message: Self) -> Notification {
return Notification(name: Self.name)
}
}
-
Make interoperability optional, but compile-time checked:
protocol Message {
associatedtype Subject
}
protocol NotificationConvertible: Message {
static var name: Notification.Name { get }
static func makeMessage(_ notification: Notification) -> Self?
static func makeNotification(_ message: Self) -> Notification
}
Why are observers of the MainActorMessage
synchronous, but observers of the AsyncMessage
are asynchronous? If there are multiple asynchronous observers, are they executed sequentially or concurrently?
If a Message
author needs the subject
to be delivered to the observer closure, they can do so by making it a property on their Message
type.
Or simply capturing the subject in the observer closure.
When an ObservationToken
goes out of scope, the corresponding observer will be removed from its center automatically if it is still registered. This behavior helps prevent memory leaks from tokens which are accidentally dropped by the user.
Would be nice to unify this with Combine.Cancellable
- it has exactly the same usage pattern, the same operations available (if you consider center.removeObserver(token)
to be equivalent to token.cancel()
) and would be nice to use familiar store(in:)
extension methods. But I do understand that those are two different frameworks, with different ownership models and evolution processes, so this is unlikely to happen.
Also alternative to consider - see enum Iteration
in swift-evolution/proposals/0475-observed.md at main · swiftlang/swift-evolution · GitHub.