# How to Use NotificationCenter for Inter-Component Communication in Swift

> Master Swift NotificationCenter for seamless inter component communication. Broadcast events and observe notifications effectively while preventing memory leaks.

- Repository: [Palmier/palmier-pro](https://github.com/palmier-io/palmier-pro)
- Tags: how-to-guide
- Published: 2026-06-24

---

**Use `NotificationCenter` to broadcast events between decoupled components by posting named notifications in senders like `AnthropicClient` and observing them via either selector-based or block-based APIs in receivers like `AgentService` and `TimelineContainerView`, always cleaning up observers in `deinit` to prevent memory leaks.**

`NotificationCenter` provides the built-in publish/subscribe mechanism in Apple’s frameworks, allowing any part of an app to broadcast events without direct references to listeners. The **palmier-io/palmier-pro** repository demonstrates three production patterns for inter-component communication: selector-based observation for AppKit integration, block-based observation with tokens for Swift-native services, and custom notification posting for keychain updates.

## NotificationCenter Patterns in Palmier Pro

The Palmier Pro codebase implements distinct observation strategies depending on the component architecture. Understanding these patterns helps you choose the right approach for your own Swift projects.

### Selector-Based Observation for AppKit Integration

In [`Sources/PalmierPro/Timeline/TimelineContainerView.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Timeline/TimelineContainerView.swift) (lines 43-53), the codebase uses the classic selector-based API to observe scroll view changes. This pattern works best when coordinating with `NSObject` subclasses that require `@objc` methods.

```swift
scrollView.contentView.postsBoundsChangedNotifications = true
NotificationCenter.default.addObserver(
    context.coordinator,
    selector: #selector(Coordinator.scrollViewBoundsChanged),
    name: NSView.boundsDidChangeNotification,
    object: scrollView.contentView
)

```

The `Coordinator` class implements `scrollViewBoundsChanged` as an `@objc` method marked with `@MainActor` to ensure UI updates occur on the main thread. This approach requires manual cleanup in the `deinit` method:

```swift
deinit {
    NotificationCenter.default.removeObserver(self)
}

```

### Block-Based Observation with Tokens

For pure Swift components like `AgentService` in [`Sources/PalmierPro/Agent/AgentService.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Agent/AgentService.swift) (lines 13-21), the repository uses the modern block-based API that returns an opaque token. This pattern captures a closure and allows explicit queue specification.

```swift
apiKeyObserver = NotificationCenter.default.addObserver(
    forName: .anthropicAPIKeyChanged,
    object: nil,
    queue: .main
) { [weak self] _ in
    MainActor.assumeIsolated {
        self?.reloadAPIKey()
    }
}

```

The `apiKeyObserver` property stores the returned `NSObjectProtocol` token, enabling precise removal without deregistering all observers for that object. The `[weak self]` capture prevents retain cycles, while `MainActor.assumeIsolated` ensures thread safety when updating UI-dependent state.

### Posting Custom Notifications

When the Anthropic API key changes, [`Sources/PalmierPro/Agent/Clients/AnthropicClient.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Agent/Clients/AnthropicClient.swift) (lines 11-13, 27-28) broadcasts the event using a custom notification name defined in an extension.

```swift
static func save(_ key: String) {
    KeychainStore.save(key, account: account)
    NotificationCenter.default.post(name: .anthropicAPIKeyChanged, object: nil)
}

```

This notification carries no `userInfo` payload; observers simply reload the key from the keychain when received. The pattern decouples the keychain storage logic from the service layer that consumes the API key.

## Implementing Selector-Based Observation

Use the selector API when integrating with AppKit components or when the observer inherits from `NSObject` and requires Objective-C runtime compatibility.

1. Enable notification posting on the source object if required (e.g., `postsBoundsChangedNotifications = true`).
2. Call `addObserver(_:selector:name:object:)` with a reference to the observing object and a selector method.
3. Implement the selector method with the `@objc` attribute and `@MainActor` for UI updates.
4. Remove the observer in `deinit` using `removeObserver(_:)` with the same object reference.

The `TimelineContainerView` also observes `NSView.frameDidChangeNotification` to synchronize header positioning with scroll content, demonstrating how multiple notifications can target the same coordinator.

## Implementing Block-Based Observation

The token-based API preferred for Swift-only code provides type safety and closure-based syntax.

**Store the token** as an optional `NSObjectProtocol` property to enable later removal:

```swift
private var apiKeyObserver: NSObjectProtocol?

```

**Capture self weakly** in the closure to prevent the notification center from retaining the observer:

```swift
{ [weak self] notification in
    guard let self = self else { return }
    // Handle notification
}

```

**Specify the main queue** explicitly when observing UI-related events:

```swift
queue: .main

```

In `AgentService`, the observer reloads the API key whenever the keychain updates, ensuring the service always uses current credentials without direct coupling to the storage layer.

## Broadcasting Events with Custom Notifications

Define notification names in a centralized extension to avoid string literals:

```swift
extension Notification.Name {
    static let anthropicAPIKeyChanged = Notification.Name("anthropicAPIKeyChanged")
}

```

When posting from `AnthropicClient`, the `object` parameter typically references the sender (pass `self` for instance notifications or `nil` for global broadcasts). Include `userInfo` only when passing primitive data that cannot be retrieved from a shared source like the keychain.

## Critical Implementation Details

### Thread Safety and MainActor

UI-related notifications **must** be observed on the main thread. The Palmier Pro codebase handles this two ways:

- **Block-based**: Specify `queue: .main` in the `addObserver` call.
- **Selector-based**: Mark the selector method with `@MainActor` or wrap UI updates in `MainActor.assumeIsolated`.

Failing to dispatch UI updates to the main thread causes runtime crashes or layout glitches, particularly when observing scroll view notifications that fire during dragging operations.

### Memory Management and Observer Lifetime

**Selector-based observers** are retained by the notification center until explicitly removed. Always call `removeObserver(_:)` in `deinit` to prevent dangling pointers after the observing object deallocates.

**Token-based observers** require storing the returned `NSObjectProtocol` token. Remove specific observers using `removeObserver(_:)` with the token, or call `removeObserver(_:name:object:)` to deregister specific name/object combinations.

The `ColorPanelBridge` inner class in [`Sources/PalmierPro/Inspector/Components/ColorField.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Inspector/Components/ColorField.swift) demonstrates this by observing `NSColorPanel.colorDidChangeNotification` and cleaning up when the color field deinitializes, preventing the bridge from outliving its parent component.

## Complete Working Example

Below is a self-contained implementation following the Palmier Pro patterns:

```swift
extension Notification.Name {
    static let dataDidUpdate = Notification.Name("dataDidUpdate")
}

class DataStore {
    func update() {
        // Persistence logic...
        NotificationCenter.default.post(name: .dataDidUpdate, object: self)
    }
}

class DataViewController: NSViewController {
    private var updateObserver: NSObjectProtocol?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Block-based observation with weak self
        updateObserver = NotificationCenter.default.addObserver(
            forName: .dataDidUpdate,
            object: nil,
            queue: .main
        ) { [weak self] _ in
            self?.refreshUI()
        }
    }
    
    private func refreshUI() {
        // Update interface...
    }
    
    deinit {
        if let token = updateObserver {
            NotificationCenter.default.removeObserver(token)
        }
    }
}

```

## Summary

- **Define notification names** using `Notification.Name` extensions to ensure type safety and avoid string literals.
- **Choose selector-based observation** when working with `NSObject` subclasses and AppKit components like `TimelineContainerView`.
- **Choose block-based observation** for Swift-native services like `AgentService`, storing the token and capturing `self` weakly.
- **Always post on the main thread** or specify `queue: .main` when UI updates depend on the notification.
- **Remove observers in `deinit`** using the specific token or object reference to prevent memory leaks and dangling pointers.

## Frequently Asked Questions

### What is the difference between selector-based and block-based NotificationCenter observation?

Selector-based observation uses `addObserver(_:selector:name:object:)` and requires an Objective-C selector method, making it ideal for `NSObject` subclasses and AppKit integration as seen in [`TimelineContainerView.swift`](https://github.com/palmier-io/palmier-pro/blob/main/TimelineContainerView.swift). Block-based observation uses `addObserver(forName:object:queue:using:)` and returns a token object, providing Swift-friendly closure syntax and better control over the dispatch queue, which `AgentService` uses to handle API key changes.

### How do I prevent memory leaks when using NotificationCenter?

Always pair every `addObserver` call with a corresponding `removeObserver` call. For selector-based observers, call `removeObserver(_:)` with the observing object in `deinit`. For block-based observers, store the returned `NSObjectProtocol` token and pass it to `removeObserver(_:)` when the observing object deallocates. The Palmier Pro codebase uses `[weak self]` in block-based closures to prevent retain cycles.

### Can NotificationCenter be used with SwiftUI views?

Yes, but you should use the `onReceive` modifier or manage the observer lifecycle through a `ViewModel` or `Coordinator` object. The `TimelineContainerView` in Palmier Pro demonstrates how to bridge AppKit notifications into SwiftUI by using an `NSViewRepresentable` coordinator that handles the observer registration and cleanup separate from the view lifecycle.

### How do I pass data between components using NotificationCenter?

Include a `userInfo` dictionary in the `post(name:object:userInfo:)` call containing primitive values or Codable objects. However, the Palmier Pro codebase prefers posting notifications without payloads (as in [`AnthropicClient.swift`](https://github.com/palmier-io/palmier-pro/blob/main/AnthropicClient.swift)) and letting observers retrieve fresh data from the canonical source (like the keychain), ensuring consistency when multiple observers react to the same event.