Skip to content

Conversation

@erichoracek
Copy link

@erichoracek erichoracek commented Jul 17, 2025

When we have a @Publishable on a @MainActor type (e.g. an @Observable view model), we conform to a special MainActorPublishable which fixes build errors that occur otherwise

Summary by CodeRabbit

  • New Features

    • Introduced support for main actor isolation in property publishers and observation registrars, ensuring thread-safe updates on the main thread.
    • Added new protocols and types to enable main actor–confined property observation and publishing.
    • Enhanced macro expansions to automatically apply main actor isolation when detected on annotated classes.
  • Tests

    • Added a test verifying macro expansion for main actor–annotated observable classes.

@coderabbitai
Copy link

coderabbitai bot commented Jul 17, 2025

Walkthrough

The changes introduce support for main actor isolation in the Publishable macro system. New protocols and registrar types are added to enable property publishers and observation registrars that are isolated to the main actor. Macro expansion logic and code generation builders are updated to detect and propagate the @MainActor attribute, and new tests verify this functionality.

Changes

File(s) Change Summary
Sources/Publishable/PropertyPublisher/Publishable.swift Added MainActorPublishable protocol and updated macro extension to conform to it when needed.
Sources/Publishable/Registrars/PublishableObservationRegistrar.swift Added MainActorPublishableObservationRegistrar protocol and related extension; added UncheckedSendable type.
Sources/PublishableMacros/Builders/ObservationRegistrarDeclBuilder.swift Added mainActor property; code generation now conditionally adds @MainActor and protocol conformance.
Sources/PublishableMacros/Builders/PropertyPublisherDeclBuilder.swift Added mainActor property; code generation now conditionally adds @MainActor to generated class.
Sources/PublishableMacros/Main/PublishableMacro.swift Macro logic updated to detect @MainActor and propagate it to generated code and protocol conformance.
Tests/PublishableMacrosTests/PublishableMacroTests.swift Added testMainActorExpansion to verify macro expansion for @MainActor-annotated classes.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Macro
    participant Builders
    participant GeneratedType

    User->>Macro: Annotates class with @MainActor and @Publishable
    Macro->>Builders: Detects @MainActor, sets mainActor = true
    Builders->>GeneratedType: Generate PropertyPublisher & Registrar with @MainActor
    GeneratedType-->>User: Provides main-actor-isolated publisher and registrar
Loading

Poem

In a patch of code where actors dwell,
MainActor’s magic now casts its spell.
Publishers publish, registrars observe,
All on the main thread, with type-safe verve!
Macro bunnies hop with glee,
For main-actor safety, as swift as can be!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
Sources/PublishableMacros/Builders/PropertyPublisherDeclBuilder.swift (1)

21-21: Remove unnecessary SwiftLint disable command.

The type_contents_order disable command is superfluous as indicated by the static analysis warning.

-    func build() -> [DeclSyntax] { // swiftlint:disable:this type_contents_order
+    func build() -> [DeclSyntax] {
Sources/Publishable/Registrars/PublishableObservationRegistrar.swift (2)

77-123: Solid implementation with minor formatting improvements needed.

The extension correctly handles MainActor isolation requirements. The withMutation implementation properly uses MainActor.assumeIsolated with a clear explanatory comment about the isolation context.

Consider breaking up the long lines for better readability:

-            try MainActor.assumeIsolated { [unchecked = UncheckedSendable(wrappedValue: (self, object, keyPath, mutation))] in
-                unchecked.wrappedValue.1.publisher.beginModifications()
-                let result = try unchecked.wrappedValue.0.underlying.withMutation(of: unchecked.wrappedValue.1, keyPath: unchecked.wrappedValue.2, unchecked.wrappedValue.3)
-                unchecked.wrappedValue.0.publish(unchecked.wrappedValue.1, keyPath: unchecked.wrappedValue.2)
-                unchecked.wrappedValue.1.publisher.endModifications()
+            try MainActor.assumeIsolated { [
+                unchecked = UncheckedSendable(
+                    wrappedValue: (self, object, keyPath, mutation)
+                )
+            ] in
+                let (registrar, obj, kp, mut) = unchecked.wrappedValue
+                obj.publisher.beginModifications()
+                let result = try registrar.underlying.withMutation(
+                    of: obj,
+                    keyPath: kp,
+                    mut
+                )
+                registrar.publish(obj, keyPath: kp)
+                obj.publisher.endModifications()

125-131: Clean up the UncheckedSendable implementation.

The struct serves its purpose well, but can be simplified and made more explicit.

Apply these improvements:

-struct UncheckedSendable<Value>: @unchecked Sendable {
+internal struct UncheckedSendable<Value>: @unchecked Sendable {
     var wrappedValue: Value
-
-    init(wrappedValue: Value) {
-        self.wrappedValue = wrappedValue
-    }
 }

The memberwise initializer is automatically synthesized by Swift, and adding explicit access control improves clarity.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d76ced2 and 76bfca7.

📒 Files selected for processing (6)
  • Sources/Publishable/PropertyPublisher/Publishable.swift (2 hunks)
  • Sources/Publishable/Registrars/PublishableObservationRegistrar.swift (1 hunks)
  • Sources/PublishableMacros/Builders/ObservationRegistrarDeclBuilder.swift (2 hunks)
  • Sources/PublishableMacros/Builders/PropertyPublisherDeclBuilder.swift (1 hunks)
  • Sources/PublishableMacros/Main/PublishableMacro.swift (2 hunks)
  • Tests/PublishableMacrosTests/PublishableMacroTests.swift (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
Sources/PublishableMacros/Builders/PropertyPublisherDeclBuilder.swift (2)
Sources/PublishableMacros/Builders/ObservationRegistrarDeclBuilder.swift (1)
  • build (25-60)
Sources/PublishableMacros/Builders/PublisherDeclBuilder.swift (1)
  • build (20-26)
Sources/Publishable/Registrars/PublishableObservationRegistrar.swift (1)
Sources/Publishable/PropertyPublisher/AnyPropertyPublisher.swift (2)
  • beginModifications (49-54)
  • endModifications (56-61)
🪛 SwiftLint (0.57.0)
Sources/PublishableMacros/Builders/PropertyPublisherDeclBuilder.swift

[Warning] 21-21: SwiftLint rule 'type_contents_order' did not trigger a violation in the disabled region; remove the disable command

(superfluous_disable_command)

Tests/PublishableMacrosTests/PublishableMacroTests.swift

[Warning] 161-161: Function body should span 50 lines or less excluding comments and whitespace: currently spans 71 lines

(function_body_length)

Sources/Publishable/Registrars/PublishableObservationRegistrar.swift

[Warning] 125-125: Type declaration should start with an empty line.

(empty_line_after_type_declaration)


[Warning] 125-125: Top-level declarations should specify Access Control Level keywords explicitly

(explicit_top_level_acl)


[Warning] 77-77: An 'extension' should not be placed amongst the file type(s) 'supporting_type'

(file_types_order)


[Warning] 114-114: Line should be 120 characters or less; currently it has 126 characters

(line_length)


[Warning] 116-116: Line should be 120 characters or less; currently it has 172 characters

(line_length)


[Warning] 128-128: This memberwise initializer would be synthesized automatically - you do not need to define it

(unneeded_synthesized_initializer)

🔇 Additional comments (12)
Sources/PublishableMacros/Builders/PropertyPublisherDeclBuilder.swift (2)

15-15: LGTM! Property addition supports MainActor isolation.

The mainActor property correctly enables conditional generation of @MainActor-annotated PropertyPublisher classes.


22-49: LGTM! Conditional MainActor generation is correctly implemented.

The conditional logic properly generates @MainActor-annotated PropertyPublisher classes when needed while preserving all existing functionality for non-MainActor types.

Sources/PublishableMacros/Builders/ObservationRegistrarDeclBuilder.swift (2)

15-15: LGTM! Property addition supports MainActor isolation.

The mainActor property correctly enables conditional generation of @MainActor-annotated ObservationRegistrar structs.


26-59: LGTM! Conditional MainActor generation and protocol conformance are correctly implemented.

The conditional logic properly:

  • Generates @MainActor-annotated ObservationRegistrar structs when needed
  • Conforms to MainActorPublishableObservationRegistrar for MainActor types
  • Conforms to PublishableObservationRegistrar for non-MainActor types
  • Preserves all existing functionality

The protocol conformance selection ensures proper actor isolation alignment.

Sources/PublishableMacros/Main/PublishableMacro.swift (4)

48-49: LGTM! MainActor detection is correctly implemented.

The attribute detection logic properly identifies @MainActor annotations on type declarations for propagation to code generation.


52-53: LGTM! Builder initialization correctly propagates MainActor flag.

The mainActor parameter is properly passed to both PropertyPublisherDeclBuilder and ObservationRegistrarDeclBuilder, ensuring consistent MainActor annotation generation.


75-75: LGTM! Consistent MainActor detection in ExtensionMacro.

The same MainActor detection logic is correctly applied in the extension macro for consistency.


82-82: LGTM! Conditional protocol conformance is correctly implemented.

The extension properly conforms to MainActorPublishable for MainActor types and Publishable for non-MainActor types, ensuring proper actor isolation alignment.

Tests/PublishableMacrosTests/PublishableMacroTests.swift (1)

161-233: LGTM! Comprehensive test coverage for MainActor expansion.

The test thoroughly verifies:

  • @MainActor attribute preservation on the original class
  • @MainActor annotation on generated PropertyPublisher class
  • @MainActor annotation on generated ObservationRegistrar struct
  • Conformance to MainActorPublishableObservationRegistrar
  • Extension conformance to MainActorPublishable

The test provides excellent coverage for the MainActor support functionality.

Note: The function body length warning (71 lines) is acceptable for a comprehensive macro expansion test that needs to verify detailed generated code.

Sources/Publishable/PropertyPublisher/Publishable.swift (2)

34-34: LGTM! Macro conformances correctly include MainActorPublishable.

The conformances list properly includes both Publishable and MainActorPublishable protocols to support both actor-isolated and non-actor-isolated types.


61-80: LGTM! MainActorPublishable protocol is correctly designed.

The protocol properly:

  • Mirrors the structure of Publishable for consistency
  • Adds @MainActor annotation to the publisher property for actor isolation
  • Provides appropriate documentation matching the original protocol
  • Maintains the same associated type and inheritance constraints

This design ensures proper actor isolation while maintaining API consistency.

Sources/Publishable/Registrars/PublishableObservationRegistrar.swift (1)

63-75: Well-designed protocol addition for MainActor support!

The new MainActorPublishableObservationRegistrar protocol maintains architectural consistency with the existing PublishableObservationRegistrar while properly adding @MainActor isolation to the publish method.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
Sources/Publishable/Registrars/PublishableObservationRegistrar.swift (1)

77-77: Consider SwiftLint warning about file organization.

SwiftLint suggests grouping all types together before extensions. However, the current organization (protocol followed immediately by its extension) enhances readability and maintainability. Consider if this trade-off is acceptable or if reorganizing would improve the codebase consistency.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 76bfca7 and 77aceb3.

📒 Files selected for processing (2)
  • Sources/Publishable/Registrars/PublishableObservationRegistrar.swift (1 hunks)
  • Sources/PublishableMacros/Builders/PropertyPublisherDeclBuilder.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/PublishableMacros/Builders/PropertyPublisherDeclBuilder.swift
🧰 Additional context used
🧬 Code Graph Analysis (1)
Sources/Publishable/Registrars/PublishableObservationRegistrar.swift (1)
Sources/Publishable/PropertyPublisher/AnyPropertyPublisher.swift (2)
  • beginModifications (49-54)
  • endModifications (56-61)
🪛 SwiftLint (0.57.0)
Sources/Publishable/Registrars/PublishableObservationRegistrar.swift

[Warning] 77-77: An 'extension' should not be placed amongst the file type(s) 'supporting_type'

(file_types_order)

🔇 Additional comments (5)
Sources/Publishable/Registrars/PublishableObservationRegistrar.swift (5)

63-75: LGTM! Well-designed protocol for MainActor isolation.

The protocol correctly mirrors PublishableObservationRegistrar while adding appropriate MainActor constraints. The @MainActor annotation on the publish method ensures thread safety when interacting with MainActor-isolated publishers.


79-96: LGTM! Correct MainActor isolation for mutation lifecycle methods.

The willSet and didSet methods are appropriately marked @MainActor and follow the same pattern as the original protocol, ensuring thread safety when managing publisher modifications.


98-103: LGTM! Correct isolation for read-only access.

The access method correctly omits @MainActor annotation since it's a read-only operation that can safely delegate to the underlying registrar without actor isolation.


105-130: LGTM! Sophisticated concurrency handling for MainActor isolation.

The withMutation implementation correctly handles the complexity of bridging between @Observable's nonisolated mutation methods and MainActor-isolated publishers. The use of MainActor.assumeIsolated with the explanatory comment clearly documents the reasoning, and the UncheckedSendable wrapper appropriately facilitates cross-actor data transfer.


133-136: LGTM! Standard concurrency helper pattern.

The UncheckedSendable wrapper is a well-established pattern for safely transferring non-Sendable values across actor boundaries when the developer can guarantee thread safety. The private visibility and minimal implementation are appropriate.

Copy link
Owner

@NSFatalError NSFatalError left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for contributing @erichoracek! My sincere apologies for taking so long to take a proper look at your PR.

I've extended the applicability of your changes in a separate PR to achieve two things:

  • Support any global actor, not just the MainActor
  • Not having to deal with specialized MainActorPublishable protocol - global actor isolated conformances are coming in Swift 6.2, so this one would become redundant quickly

One downside of my approach is that I've removed the publisher property from the Publishable protocol. Then again, as global actor isolated conformances are going to be released shortly with Xcode 26, this is a stopgap solution for Swift 6.1 specifically. I intend to reexpose the publisher again when the new release lands (besides, using Publishable as an existential type is of limited value anyway).

I'll appreciate if you'd find a moment to verify that my approach works for your use cases too. Thanks again for your work!

@erichoracek
Copy link
Author

Thanks for working on a more general solution for this—it seems like a good solve given the constraints and agree it is more elegant and flexible than the separate MainActorPublished protocol—doesn't seem like a big deal to remove the property from the protocol temporarily.

In terms of whether it will work for our use case, it should work well, however we have additional changes we've made on top of this MainActor change to make publishing opt in via a new @ObservationPublished macro (see this branch for how it works), as we weren't comfortable with the keypath casting and publishing overhead for non-observed properties in the current approach wherein all observable properties are published by default. If you merge that separate PR, I could rebase our additional changes on top of it and submit it for your consideration, although it does change the API and behavior of the library quite a bit. Let me know if you think it's worth opening a PR for that!

@NSFatalError
Copy link
Owner

@erichoracek I finally found some time to work on this - sorry for keeping you waiting so long. I ended up using isolated conformances to preserve the existing behavior, and those changes have now been merged.

Regarding your approach with @ObservationPublished, it’s definitely something I’ll consider going forward. However, I personally wouldn’t want it to replace the existing @Publishable behavior, but rather supplement it as an alternative approach. In my use cases, I value the ability to observe computed properties (which works nicely with the new @Memoized macro I’ve just added), as well as the ability to support the @Model macro without requiring any specific adaptations for SwiftData.

I’ll close this PR since it has become redundant after my changes, but feel free to suggest other improvements or contribute further. Thanks again for your work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants