Skip to content

Fix Swift 6 actor isolation errors in RowndPlugin.swift#72

Open
leonelgalan wants to merge 1 commit into
rownd:mainfrom
leonelgalan:fix/swift6-mainactor-state-subscription
Open

Fix Swift 6 actor isolation errors in RowndPlugin.swift#72
leonelgalan wants to merge 1 commit into
rownd:mainfrom
leonelgalan:fix/swift6-mainactor-state-subscription

Conversation

@leonelgalan
Copy link
Copy Markdown
Contributor

@leonelgalan leonelgalan commented Feb 26, 2026

Xcode 26 (Swift 6.2) enforces @mainactor isolation even in Swift 5 language mode. The configure method accesses @MainActor-isolated APIs (subscribe() and $current) from a nonisolated context, causing build failures.

This wraps the state subscription in Task { @MainActor }, which is the canonical Swift concurrency migration pattern.

Build errors without fix:

  • RowndPlugin.swift:46:68 — call to main actor-isolated instance method 'subscribe' in a synchronous nonisolated context
  • RowndPlugin.swift:48:58 — main actor-isolated property '$current' can not be referenced from a nonisolated context
$ swift --version
swift-driver version: 1.127.14.1 Apple Swift version 6.2.3 (swiftlang-6.2.3.3.21 clang-1700.6.3.2)
Target: arm64-apple-macosx26.0

$ xcodebuild -version
Xcode 26.2
Build version 17C52

Summary by Sourcery

Bug Fixes:

  • Ensure Rownd state subscription and sink setup are executed on the main actor to comply with Swift 6 actor isolation rules and prevent build failures.

Xcode 26 (Swift 6.2) enforces @mainactor isolation even in Swift 5
language mode. The configure method accesses @MainActor-isolated APIs
(subscribe() and $current) from a nonisolated context, causing build
failures.

This wraps the state subscription in Task { @mainactor }, which is the
canonical Swift concurrency migration pattern.

Build errors without fix:
- RowndPlugin.swift:46:68 — call to main actor-isolated instance
  method 'subscribe' in a synchronous nonisolated context
- RowndPlugin.swift:48:58 — main actor-isolated property '$current'
  can not be referenced from a nonisolated context
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Feb 26, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Wraps Rownd state subscription setup in a @mainactor Task to satisfy Swift 6 actor isolation rules while preserving the existing subscription behavior and avoiding duplicate initialization.

Sequence diagram for RowndPlugin.configure state subscription with @mainactor Task

sequenceDiagram
    actor ReactNativeApp
    participant RowndPlugin
    participant MainActorTask
    participant Rownd
    participant RowndState
    participant RowndPluginEventEmitter

    ReactNativeApp->>RowndPlugin: configure()
    alt state not yet initialized
        RowndPlugin->>MainActorTask: create Task @MainActor [weak self]
        activate MainActorTask
        MainActorTask->>Rownd: getInstance()
        Rownd-->>MainActorTask: RowndInstance
        MainActorTask->>Rownd: state()
        Rownd-->>MainActorTask: RowndStatePublisher
        MainActorTask->>RowndState: subscribe(identityClosure)
        RowndState-->>MainActorTask: initializedState
        MainActorTask->>RowndPlugin: assign state = initializedState
        MainActorTask->>RowndState: initializedState.$current.sink(newState)
        loop on each newState
            RowndState-->>MainActorTask: newState
            MainActorTask->>RowndPluginEventEmitter: sendEvent(update_state, newState.toDictionary())
        end
        deactivate MainActorTask
    else state already initialized or self deallocated
        RowndPlugin-->>ReactNativeApp: return without changes
    end
Loading

Class diagram for updated RowndPlugin state subscription structure

classDiagram
    class RowndPlugin {
        +state
        +stateCancellable
        +configure()
    }

    class Rownd {
        +getInstance() Rownd
        +state() RowndStatePublisher
    }

    class RowndStatePublisher {
        +subscribe(identityClosure) RowndState
        +$current
    }

    class RowndState {
        +toDictionary() Dictionary
    }

    class RowndPluginEventEmitter {
        +emitter RowndPluginEventEmitter
        +sendEvent(withName, body) void
    }

    RowndPlugin --> Rownd : uses getInstance()
    RowndPlugin --> RowndStatePublisher : uses state()
    RowndStatePublisher --> RowndState : returns
    RowndStatePublisher --> RowndState : $current.sink
    RowndState --> RowndPluginEventEmitter : sendEvent(update_state, body)
    RowndState --> RowndState : toDictionary() is called within sink callback
    RowndPlugin ..> RowndState : holds state
    RowndPlugin ..> RowndStatePublisher : holds stateCancellable
    RowndPlugin ..> RowndPluginEventEmitter : emits update_state events
Loading

File-Level Changes

Change Details Files
Move state subscription initialization into a @mainactor Task with safe self-capture to comply with main-actor isolation.
  • Replaced direct state subscription in configure() with an asynchronous Task annotated with @mainactor.
  • Captured self weakly in the Task and early-return if self is deallocated or state is already initialized inside the Task.
  • Initialized the Rownd state via subscribe and assigned self.state within the Task on the main actor.
  • Reattached the $current sink and event emission logic unchanged inside the Task, maintaining existing behavior while running on the main actor.
ios/RowndPlugin.swift

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • If configure() is always expected to be called from the main actor, consider marking the method @MainActor instead of spawning a Task { @MainActor … }, which would avoid the extra async hop and keep the call flow simpler and more predictable.
  • By moving the subscription setup into an async Task, configure() now returns before state and stateCancellable are initialized; if callers rely on synchronous initialization, it may be worth documenting or revisiting that behavior (e.g., by making configure async or explicitly decoupling it into a two-phase setup).
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- If `configure()` is always expected to be called from the main actor, consider marking the method `@MainActor` instead of spawning a `Task { @MainActor … }`, which would avoid the extra async hop and keep the call flow simpler and more predictable.
- By moving the subscription setup into an async `Task`, `configure()` now returns before `state` and `stateCancellable` are initialized; if callers rely on synchronous initialization, it may be worth documenting or revisiting that behavior (e.g., by making `configure` async or explicitly decoupling it into a two-phase setup).

## Individual Comments

### Comment 1
<location path="ios/RowndPlugin.swift" line_range="46" />
<code_context>
-                            withName: "update_state", body: try newState.toDictionary())
-                    } catch {
-                        print("Failed to encode Rownd state: \(String(describing: error))")
+                Task { @MainActor [weak self] in
+                    guard let self = self, self.state == nil else { return }
+                    let initializedState = Rownd.getInstance().state().subscribe { $0 }
</code_context>
<issue_to_address>
**issue (bug_risk):** The placement of `@MainActor` relative to the capture list is likely invalid Swift syntax.

In closure syntax, attributes must come before the capture list: `{ @MainActor [weak self] in ... }` is invalid. This will not compile as written. Consider either `Task { @MainActor in ... }` (no capture list) or `Task { [weak self] @MainActor in ... }`, which preserves both main-actor isolation and the weak capture.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread ios/RowndPlugin.swift
withName: "update_state", body: try newState.toDictionary())
} catch {
print("Failed to encode Rownd state: \(String(describing: error))")
Task { @MainActor [weak self] in
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): The placement of @MainActor relative to the capture list is likely invalid Swift syntax.

In closure syntax, attributes must come before the capture list: { @MainActor [weak self] in ... } is invalid. This will not compile as written. Consider either Task { @MainActor in ... } (no capture list) or Task { [weak self] @MainActor in ... }, which preserves both main-actor isolation and the weak capture.

@leonelgalan
Copy link
Copy Markdown
Contributor Author

I don't think we need to make the changes in the review:

  • Re: @mainactor on configure() — RN bridge methods are called from a background queue. Marking the whole method @mainactor would force everything onto the main thread. Task { @mainactor in } only dispatches the subscription code that touches @MainActor-isolated APIs.
  • Re: async initialization — configure() is already async — Rownd.configure() on line 37 is in a Task and resolve() fires before it completes. The subscription is event-driven (Combine sink), synchronous init isn’t required.
  • Re: syntax — Task { @mainactor [weak self] in } is valid Swift. Attributes come before the capture list per the grammar. Compiles on both Swift 6.2.1 and 6.2.3.

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.

1 participant