惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

www.infosecurity-magazine.com
www.infosecurity-magazine.com
Vercel News
Vercel News
G
Google Developers Blog
MyScale Blog
MyScale Blog
The Register - Security
The Register - Security
I
InfoQ
Blog — PlanetScale
Blog — PlanetScale
D
DataBreaches.Net
Microsoft Security Blog
Microsoft Security Blog
V
Visual Studio Blog
V2EX - 技术
V2EX - 技术
F
Fortinet All Blogs
博客园_首页
S
Secure Thoughts
GbyAI
GbyAI
S
Security Affairs
N
News | PayPal Newsroom
Forbes - Security
Forbes - Security
Recent Announcements
Recent Announcements
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Security Archives - TechRepublic
Security Archives - TechRepublic
宝玉的分享
宝玉的分享
Hugging Face - Blog
Hugging Face - Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
H
Heimdal Security Blog
A
About on SuperTechFans
P
Proofpoint News Feed
H
Help Net Security
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Y
Y Combinator Blog
L
LINUX DO - 最新话题
Apple Machine Learning Research
Apple Machine Learning Research
L
LangChain Blog
博客园 - 叶小钗
A
Arctic Wolf
Cisco Talos Blog
Cisco Talos Blog
T
The Exploit Database - CXSecurity.com
人人都是产品经理
人人都是产品经理
T
Threat Research - Cisco Blogs
N
News and Events Feed by Topic
Security Latest
Security Latest
The Hacker News
The Hacker News
T
Tor Project blog
O
OpenAI News
博客园 - 三生石上(FineUI控件)
PCI Perspectives
PCI Perspectives
量子位
大猫的无限游戏
大猫的无限游戏
Stack Overflow Blog
Stack Overflow Blog

Inside Nutrient

A guide to the invisible work behind documents Introducing Nutrient Documents for Salesforce: Native document generation and signing Document AI vs. traditional OCR: Choosing between OCR, AI, and hybrid pipelines PDF SDK compliance and security evaluation checklist for enterprise teams (2026) Invariant Corp replaces paper processes with Nutrient Workflow and scales without limits What is process mapping? A complete guide Nutrient vs. Conga Composer for Salesforce document generation (2026) Document routing: How to automate document distribution The CTO’s AI playbook: Why accountability architecture beats orchestration Compliance workflow automation: Why built-in compliance is table stakes Workflow diagrams: Examples, symbols, and how to build one that actually runs Digital forms: Replace paper forms with automated workflows Approval workflow software: How to automate approvals Why document-centric automation is different The CEO’s AI playbook: Why decision architecture beats model selection Nutrient SDK product updates for Q1 2026 PDF redaction verification: How to prove sensitive data is permanently removed What is a VPAT? The complete guide to accessibility conformance reports What is PDF/UA? The accessible PDF standard explained Salesforce eSignatures: Generate, sign, and track documents in one flow Online document viewer: Options, tradeoffs, and how to embed one Document viewer for web apps: React, Vue, Angular (2026) Best document viewers in 2026: A buyer’s guide How to edit a PDF in Python: Add text, images, and annotations Nutrient advances Workflow platform with agentic AI for enterprise-grade speed and consistency in document-heavy operations How to create a Salesforce quote template from opportunity data The business case for accessibility: Five ways it drives enterprise value Python PDF library comparison (2026): 7 libraries for developers Why your AI agent hallucinates PDF table data PDF.js limitations: When to upgrade to a commercial PDF SDK How Subject scaled 5× with Nutrient’s PDF SDK without rebuilding its document layer I replaced our sales training with an AI coach that runs in Slack — here’s what broke Redirecting to: https://securitybuzz.com/cybersecurity-news/why-enterprise-permissions-are-ais-most-dangerous-inheritance/ Nutrient .NET SDK vs. iText Core: Complete comparison for .NET developers DocuVieware: Support’s most frequently asked setup questions Introducing Nutrient Workflow How to convert PDF to Word in C# (.NET) When email and spreadsheets stop working: Work order approval workflows for field teams on the move Compliance with confidence: Why document-centric automation is the foundation of your mission Nutrient expands AI Assistant, automating multistep document workflows inside any application What is document generation? A developer’s guide to PDF generation Document Converter data flow and how real-time watermarks skip the queue PDF/UA compliance guide: Requirements, standards, and best practices Computers still can’t understand you How Athena Intelligence built AI agents for regulated enterprises with Nutrient’s document infrastructure How to convert HTML to PDF (2026): 4 methods from browser print to SDK How to build a document extraction pipeline with Nutrient Vision API OCR vs. intelligent document processing: Choosing the right document extraction engine Beyond OCR: How document intelligence eliminates manual processing in regulated industries Nutrient vs. IronPDF: Complete comparison for .NET developers Nutrient vs. Aspose.PDF: Complete comparison for .NET developers Redirecting to: https://fortune.com/2026/02/19/openclaw-who-is-peter-steinberger-openai-sam-altman-anthropic-moltbook/ Lufthansa Systems uses Nutrient to deliver reliable, scalable PDF rendering for pilots worldwide Nutrient vs. Syncfusion: Complete comparison for .NET developers React’s useTransition: The hook you’re probably using wrong First City Monument Bank streamlines banking processes with Nutrient Workflow Redirecting to: https://www.sdcexec.com/warehousing/automation/article/22957364/nutrient-workflow-automation-the-missing-link-in-supply-chain-efficiency The complete guide to digital signatures: PAdES, CAdES, and XAdES explained Nutrient Python SDK: Production-grade document processing for Python Introducing agentic document editing for web applications with AI Assistant Nutrient vs. QuestPDF: Complete comparison for .NET developers How we fixed the GdPicture license expiration (and what to do if you’re affected) Red team security testing with agentic AI The future of healthcare document automation Best healthcare workflow software compared Nutrient SDK product updates for Q4 2025 How Harvey scaled legal document workflows 50 percent MoM without rebuilding infrastructure HIPAA-compliant document management in hospitals How we optimized rendering performance while handling thousands of annotations in React — Part 2 Automated PII removal with Nutrient API Redirecting to: https://www.devopsdigest.com/2026-low-code-no-code-predictions Redirecting to: https://www.kmworld.com/Articles/Editorial/ViewPoints/Leaders-predict-AI-to-continue-permeating-all-aspects-of-KM-in-2026-172594.aspx What are deep agents and how do they solve complex problems? Whipping up document magic: Your easy-bake recipe for Vue and Nutrient Web SDK 🧁 What I’ve learned about product iteration planning while building SDKs Passwordless document signing: Three-layer security guide New zip folder functionality streamlines file management in Document Automation Server The keyboard shortcuts playbook: Taking control of keyboard events in Nutrient Web SDK From experienced engineer to AI beginner: My unexpected journey AI-assisted manual testing: Handling Safari’s PDF rendering and UI quirks How to keep a 20-year-old SDK up to date How we optimized rendering performance while handling thousands of annotations in React — Part 1 Nutrient announces new executive hires to accelerate next phase of growth High performance UI using web workers Automate document conversion at scale with Python and Nutrient DCS From curiosity to PLG (and AI): My journey to understanding product-led growth Prost to progress: One year as Nutrient Pigeon usage at Nutrient: Bridging native SDKs to Flutter Modernizing CI build servers: How to migrate from Chef to Ansible Unix man pages: AI-friendly documentation since 1971 Consistent hashing for even load distribution Best AI redaction APIs: Complete comparison guide for 2025 Why AI document redaction matters for modern security From coding to coordinating: How AI transformed my workflow What is intelligent document processing (IDP)? A complete guide Enterprise PDF SDKs: Best PSPDFKit (now Nutrient) alternatives Nutrient SDK product updates for Q3 2025 GdPicture support best practices Redacting sensitive data with Nutrient AI redaction API How AI is transforming the customer experience at Nutrient: From instant answers to intelligent support
A deep dive into notifications and messages on iOS 26
Douglas Hill · 2025-08-20 · via Inside Nutrient

NotificationCenter(opens in a new tab) enables loose coupling across codebases using Apple’s Foundation framework, such as iOS apps. One part of a project can post a notification when an event occurs, and any other part can react by listening for/observing that notification.

As proposed in SF-0011 Concurrency-Safe Notifications(opens in a new tab), iOS 26 introduces new message APIs as an alternative to NotificationCenter. These APIs improve both type safety and concurrency safety, allowing the Swift compiler to better verify code correctness.

This article reviews the range of NotificationCenter APIs, discussing their pros and cons. It then compares them with the new message APIs and concludes with guidance on bridging between messages and the older notification APIs, highlighting key nuances of the new system.

Notification APIs

There’s one way to post notifications, and there are several ways to observe them. These APIs are all based around Notification(opens in a new tab).

Notification is the Swift struct overlay on the Objective-C NSNotification(opens in a new tab) class. Since it’s fairly uncommon to copy or mutate notifications, this difference isn’t practically important.

A notification consists of three components:

  • The name is a String that identifies the type of event that occurred.
  • The object can be any type. It’s typically the object that can be thought of as sending the notifications, but this isn’t enforced.
  • The ‘user info’ dictionary can contain objects of any type that might be useful to know about the event that occurred.

In typical use of Notification, the name implies the object will be of a certain type, and the user info dictionary will contain objects of certain types under certain keys. The compiler can’t verify this, so as a codebase grows in complexity, documentation and tests are your best bet to ensure all notifications are posted and received with consistent type expectations.

For completeness, the API for posting notifications(opens in a new tab) looks like this:

NotificationCenter.default.post(name: .XYZTrainDidArrive, object: expressTrain, userInfo: [trainDelayInMinsUserInfoKey: 13])

If you like, you can explicitly create a Notification with the same values, but I can’t think of many practical situations where this would be beneficial:

NotificationCenter.default.post(Notification(name: .XYZTrainDidArrive, object: expressTrain, userInfo: [trainDelayInMinsUserInfoKey: 13]))

The code above adds a prefix to the name to avoid potential conflicts, since it’s declared in an extension of Notification.Name, a namespace we don’t control. The same applies to the underlying string value. While omitting the prefix is usually safe in app code, it’s important when developing a framework like ours.

Selector-based observation API

The oldest approach(opens in a new tab) relies on the Objective-C runtime and separates observation setup from handling:

@objc func handleArrivingTrain(_ notification: Notification) {

// Handle the notification.

}

Start the observation:

NotificationCenter.default.addObserver(self, selector: #selector(handleArrivingTrain), name: .XYZTrainDidArrive, object: expressTrain)

Stop the observation like this, for example, after a view disappears:

NotificationCenter.default.removeObserver(self, name: .XYZTrainDidArrive, object: expressTrain)

A theoretical downside of this API is that if multiple places in the project set up distinct observations with the same observer, name, and object, then that single call to removeObserver would stop all these observations, with no way to make the removal more specific. I’ve never seen this be problematic in practice.

If you can’t stop the observation from an appropriate place or (more likely) forget to do this, nothing bad will typically happen, because if the observing object is destroyed, the notification centre detects this and stops posting to that observer. This works by the notification centre keeping a weak reference to the observer, so there’s no opportunity to introduce retain cycles with this API.

The use of the Objective-C runtime means the Swift compiler can’t check this for concurrency safety, so therefore won’t produce any concurrency warnings or errors. This is likely to be fine most of the time, but not always.

While this API is the oldest, I think it’s still a very practical option, especially with UI code that all runs on the main thread.

Closure-based observation API

The closure-based API(opens in a new tab) keeps setup and handling together, which may improve code readability:

let trainObservationToken = NotificationCenter.default.addObserver(forName: .XYZTrainDidArrive, object: expressTrain, queue: nil) { [weak self] notification in

// Handle the notification.

}

Since this API is imported from Objective-C, the return value can implicitly be ignored. However, behind the scenes, NotificationCenter strongly retains the returned token until removeObserver is called.

This means that if you don’t stop the observation, you’ll have a small memory leak of the observation token and an ‘execution leak’, in that the closure will keep running whenever this notification is posted for the entire remaining lifetime of the process. If the closure captures additional objects, you may have a larger memory leak. Observations are stopped like this:

NotificationCenter.default.removeObserver(trainObservationToken)

To reiterate: To use this API properly, you need to both store the token (probably in a property) and call removeObserver later on.

This API doesn’t work well with strict concurrency either. Even if you explicitly specify the main queue, the compiler sees the closure as running synchronously and without actor isolation. Therefore, it’s hard to get code using this to compile when using Swift 6 language mode. You can use assumeIsolated, but then you’ll have trouble if you need the notification because it’s not sendable, because it could contain anything. For notifications related to UI, nothing is ever leaving the main actor, but the compiler doesn’t know this.

The closure-based API is the notification API I see used incorrectly most often. I recommend avoiding it.

Combine observation API

You can observe notifications using Combine(opens in a new tab). Here’s the most basic situation:

self.trainObservation = NotificationCenter.default.publisher(for: .XYZTrainDidArrive, object: localTrain).sink { [weak self] notification in

// Handle the notification.

}

And then:

self.trainObservation.cancel()

This is basically a better version of the closure-based API:

  • There are slightly fewer characters in the most basic case.
  • You’ll see a warning if you don’t use the return value.
  • This won’t do anything if you don’t store the return value, since the observations will stop as soon as it’s created. This is good, because you’ll likely notice the mistake as soon as you run your code.
  • The observation will automatically stop if you store it in a property and the owning object is destroyed, because the observation will be destroyed too.
  • There’s a lot of flexibility with Combine — for example, you can conveniently filter or map the stream of notifications.

This approach doesn’t produce any strict concurrency warnings or errors, seemingly because the closure passed to sink isn’t marked as @Sendable. My understanding is that this closure may in fact be sent across actor boundaries, so this approach is similar to the selector-based API with regards to strict concurrency: no warnings and no guarantees about safety, which is probably fine in a lot of cases.

Async sequence API

The newest notification observation API(opens in a new tab) leverages AsyncSequence(opens in a new tab). You can set up the observation like this:

let trainObservationTask = Task {

for await notification in NotificationCenter.default.notifications(named: .XYZTrainDidArrive, object: localTrain) {

// Handle the notification.

}

// Execution won’t reach here until the task is cancelled.

}

This has similar behaviour to the closure-based API: You can implicitly ignore the created task, and unless it’s cancelled, the observation will continue (in other words, the code will await the end of the sequence) for the entire remaining lifetime of the process. The observation can be stopped by cancelling the Task or a parent Task:

trainObservationTask.cancel()

The documentation says that iterating over the Notifications will result in a warning about crossing an actor boundary, and therefore we should use map to extract only Sendable data from each notification and then iterate over that. However, in my test project, I didn’t see this warning in Swift 5 or 6 language modes.

We haven’t found a situation where this API seems like the best choice in our codebase, but it’s an interesting option. This API doesn’t fit well for notifications that should be posted and observed on the main thread, but it may be a good option if you’re heavily using concurrency.

Message APIs

iOS 26 adds new APIs for both posting and observing messages. There are two new types for different concurrency scenarios: NotificationCenter.MainActorMessage(opens in a new tab) and NotificationCenter.AsyncMessage(opens in a new tab). We’ll just look at MainActorMessage in this article.

These types are both protocols, moving away from the Notification struct entirely. I guess Apple chose to not reuse the term notification to make the distinction clearer. In a couple of years, perhaps new iOS developers will be puzzled as to why it’s called NotificationCenter rather than MessageCenter. Or, maybe they won’t, because their AI agents will handle details like this.

The simplest definition of a message doesn’t interoperate with Notification at all:

struct TrainDidArriveMessage: NotificationCenter.MainActorMessage {

typealias Subject = Train

}

Our type in this simple example has no properties. This is the equivalent of a Notification without anything in userInfo. We’ll add back the equivalent of trainDelayInMinsUserInfoKey shortly.

The equivalent of the object (the sender) with Notification is now called the subject, which is a Train in this example. We can post a message like this:

NotificationCenter.default.post(TrainDidArriveMessage(), subject: expressTrain)

And we can observe the message:

self.trainObservationToken = NotificationCenter.default.addObserver(of: expressTrain, for: TrainDidArriveMessage.self) { [weak self] message in

// Handle the message.

}

This new API works excellently with Swift strict concurrency. Since we used MainActorMessage in this example, our closure that handles the message is isolated to the main actor.

We also have compile-time guarantees about message structure. If the message has some properties, there’s of course no need to force unwrap or force cast when reading these (like there would be with userInfo). What a luxury!

Similar to the Combine API, the observation will be stopped if the observation token isn’t kept alive, which encourages correct use to avoid leaking the observation. We can stop observing earlier like this:

NotificationCenter.default.removeObserver(self.trainObservationToken)

This looks the same as the older notification API to stop observing, but the type of the parameter is NotificationCenter.ObservationToken(opens in a new tab) instead of Any.

API design considerations

It’s nice to have message types nested under their subject type. For example, instead of using a module-level TrainDidArriveMessage, we could do this:

extension Train {

struct DidArriveMessage: NotificationCenter.MainActorMessage {

typealias Subject = Train

}

}

Apple includes the word Message at the end of its message types — for example, UIScene.WillConnectMessage. I did the same in my first pass adding messages in Nutrient iOS SDK, but then (before seeing Apple’s API) thought the usage would be nicer and the meaning still clear without this word, so we’d just have Train.DidArrive as the type of the message. (Our framework is about document viewing and editing rather than trains, but I’ll keep my example consistent.) I’d love to know Apple’s reasoning behind its naming decision.

For frameworks, it’s a common situation that the framework defines a message for public observation that should only be posted internally by the framework. Correct API usage can be enforced by not making any public initialisers of the message type. Interestingly, Apple has made its initialisers public so, for example, we could create and post our own UIScene.WillConnectMessage, although I can’t see why this would be useful. Again, I’d really like to know the reasoning behind this, as I’m sure adding public APIs is not something done lightly at Apple’s scale.

Message identifiers

We have the option of using NotificationCenter.MessageIdentifier(opens in a new tab) to make setting up each observation slightly more streamlined as the cost of more boilerplate for each message type. The identifier is set up with this generic boilerplate mess that you should probably copy and paste rather than learn or understand:

extension NotificationCenter.MessageIdentifier where Self == NotificationCenter.BaseMessageIdentifier<Train.DidArriveMessage> {

static var didArrive: Self { .init() }

}

This turns Train.DidArriveMessage.self into just .didArrive when setting up an observation, which is nice, but not a huge reduction:

let observationToken = NotificationCenter.default.addObserver(of: expressTrain, for: .didArrive) { [weak self] message in

// Handle the message.

}

The identifier can simply be .didArrive without the risk of conflict identifiers, because this extension is only valid where the subject type is Train to match the first parameter. For example, this won’t conflict if you have a Bus that also has a message with identifier .didArrive. At least with Xcode 26 beta 4, this type constraint isn’t recognised by auto-completion, which will suggest all possible messages from Apple’s frameworks as alternatives to .didArrive, even if the resulting code won’t compile.

The overhead of defining identifiers might not be worthwhile for notifications used in a single module, but it make more sense for frameworks — especially frameworks with public APIs like ours, where messages might be observed in many apps.

Bridging between messages and notifications

So that we don’t have to update all code at once, the messages API can interoperate in either direction with existing Notification code. This means a message can be posted and observed as a notification, and vice versa. This is especially useful for frameworks like ours, since our customers might be faster or slower than us at adopting the newer APIs.

Messages without properties

If the message has no properties (corresponding to no userInfo), we can bridge to and from Notification by specifying the Subject type and implementing name and makeMessage:

extension Train {

struct DidArriveMessage: NotificationCenter.MainActorMessage {

typealias Subject = Train

public static var name: Notification.Name {

.XYZTrainDidArrive

}

public static func makeMessage(_ notification: Notification) -> Self? {

.init()

}

}

}

The default implementation of makeNotification is sufficient in this simple case.

You should always implement name and makeMessage together. If you only implement name, then this error will be logged when attempting to post a notification and observe it as a message with a matching name:

Unable to deliver Notification to Message observer because TrainDidArriveMessage.makeMessage() returned nil. If this is unexpected, check or provide an implementation of makeMessage() which returns a non-nil value for this notification’s payload.

The process won‘t crash; the message simply won’t be received. This sort of near-silent failure won’t be fun to diagnose in production, so don’t do that to yourself.

Messages with properties

If our messages/notifications include properties/userInfo such as information about how much the arriving train is delayed, they’d look like this:

let trainDelayInMinsUserInfoKey = "XYZDelayInMins"

extension Train {

struct DidArriveMessage: NotificationCenter.MainActorMessage {

let delayInMins: Int

typealias Subject = Train

public static var name: Notification.Name {

.XYZTrainDidArrive

}

public static func makeMessage(_ notification: Notification) -> Self? {

.init(delayInMins: notification.userInfo![trainDelayInMinsUserInfoKey] as! Int)

}

public static func makeNotification(_ message: Self, object: Train?) -> Notification {

Notification(name: name, object: object, userInfo: [trainDelayInMinsUserInfoKey: message.delayInMins])

}

}

}

We’ve added makeNotification, since the default implementation is no longer sufficient.

Of course, bridging from an API without compiler checks to one with compiler checks, you’ll end up with some unwraps and casts. These go in makeMessage. Since my goal is to write correct code in the long term rather than code that doesn’t crash, I think force unwrapping and casting is the fastest way to validate that the notification code that the compiler can’t check is in fact correct. If you return nil in makeMessage, then this programmer error will show only as a logged runtime warning.

The subject is unavailable when observing

With the notification observation API, from what I’ve seen, it’s fairly common to use nil as the object to observe notifications from any object, and then filter in a way that’s more complex than just object equality.

For example, suppose you wanted to handle all trains arriving from a certain operator. With the notification API, you could set up your observation omitting the object (equivalent to passing nil) and then do your custom filtering:

let trainObservation = NotificationCenter.default.publisher(for: .XYZTrainDidArrive)

.filter { notification in

(notification.object as! Train?)?.operator == someTrainOperator

}

.sink { [weak self] notification in

// Handle the notification for trains from a specific operator.

}

However, the subject isn’t passed into our closure. Here, we use Train.self to indicate we want to receive messages about any train arriving:

self.trainObservationToken = NotificationCenter.default.addObserver(of: Train.self, for: .didArrive) { [weak self] message in

// No access to the subject (the specific train) here.

}

This issue is discussed at the bottom of the SF-0011 proposal(opens in a new tab) stating the reason for not including this was:

the addObserver() closure would always have to specify a subject parameter even for messages without subject instances.

The reasoning seems to be that because this parameter would only sometimes be useful, it’s better to not provide it at all. I don’t understand this conclusion.

To work around this limitation, we could make the subject part of the message:

extension Train {

struct DidArriveMessage: NotificationCenter.MainActorMessage {

typealias Subject = Train

let train: Train

}

}

But then a message could be posted with an inconsistent subject like this:

NotificationCenter.default.post(TrainDidArriveMessage(train: expressTrain), subject: localTrain)

This is a limitation of the current API, so I’m not sure what best practice will evolve here.

Conclusion

Aside from the subject being unavailable when observing, this messages API seems very nicely done. Compiler checks for type- and concurrency-safety are a huge improvement, so this is the clear API to use once you can require iOS 26 as your minimum version.

In our frameworks, we intend to provide equivalent APIs for both messages and notifications so our customers can choose whichever works best for them. While interoperability results in a lot of boring bridging code, this means our customers can start using messages without us holding them back. Once we require iOS 26 in a couple of years, we’ll surely be using messages internally — while our customers can choose to stick with notifications if they prefer.

Additional reading