Skip to content

RF-9688 - Remove ReactiveSwift from Workflow public interface #360

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

mjohnson12
Copy link
Collaborator

@mjohnson12 mjohnson12 commented Jul 17, 2025

This is a breaking change to Workflow that removes ReactiveSwift from the public interface of Workflow and Workflow UI.
There are 3 changes:

  1. In WorkflowHost rendering is now a Combine publisher that has a value property for the current rendering. In searching the register code base there were very few places we were using a Signal/SignalProducer from the rendering property. The plan would be to change those consumers to use sink. The value property is the same so all references to rendering.value will work.
  2. In WorkflowHost output has been renamed to outputPublisher. It is now a Combine Publisher that can be used to subscribe to Workflow output. Per Andrew's suggestion I added a new protocol WorkflowOutputPubisher that exposes the outputPublisher. The output property is used in a lot of places in register. To fix those places I added an extension on WorkflowOutputPubisher in WorkflowReactiveSwift that re-exposes output as a Signal. All the places that use output will just need to import WorkflowReactiveSwift and they will continue to work.
  3. WorkflowHostingController in WorkflowUI now implements WorkflowOutputPublisher. Consumers using output will just need to import WorkflowReactiveSwift to continue to work.

Note: Even though this removes ReactiveSwift as a dependency from the Workflow and WorkflowUI targets the Workflow Package has to continue to have ReactiveSwift as a dependency since it's used by WorkflowReactiveSwift. But because register uses bazel if you import Workflow/WorkflowUI in your module it does not import (directly or transitively) ReactiveSwift.

Checklist

  • Unit Tests
  • UI Tests
  • Snapshot Tests (iOS only)
  • I have made corresponding changes to the documentation

@mjohnson12 mjohnson12 force-pushed the markj/reactive_swift_removal branch 7 times, most recently from 72b41ed to bd84d2e Compare July 17, 2025 18:08
@mjohnson12 mjohnson12 force-pushed the markj/reactive_swift_removal branch from bd84d2e to 13f4761 Compare July 17, 2025 18:11
Changed WorkflowHost and WorkflowHostingController to conform to WorkflowOutputPublisher.
Added extension in WorkflowReactiveSwift to WorkflowOutputPublisher for output signal
Removed unnecssary extensions on WorkflowHost and WorkflowHostingController
@mjohnson12 mjohnson12 marked this pull request as ready for review July 31, 2025 14:19
@mjohnson12 mjohnson12 requested review from a team as code owners July 31, 2025 14:20
@mjohnson12 mjohnson12 changed the title Remove ReactiveSwift from Workflow public interface RF-9688 - Remove ReactiveSwift from Workflow public interface Jul 31, 2025

/// Represents the `Rendering` produced by the root workflow in the hierarchy. New `Rendering` values are produced
/// as state transitions occur within the hierarchy.
public let rendering: Property<WorkflowType.Rendering>
public let rendering: ReadOnlyCurrentValueSubject<WorkflowType.Rendering, Never>
Copy link

@blevasseur-block blevasseur-block Aug 12, 2025

Choose a reason for hiding this comment

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

suggestion:

@Published private(set) var rendering: WorkflowType.Rendering`
public var renderingPublisher: AnyPublisher<WorkflowType.Rendering, Never> {
    $rendering.eraseToAnyPublisher()
}
    private let rendering: CurrentValueSubject<Rendering, Never>

    var renderingPublisher: AnyPublisher<Rendering, Never> {
        rendering.eraseToAnyPublisher()
    }

There's likely a few ways we could go about this without creating a custom subject. It's worth noting that @Published is part of Combine and use-able anywhere as well. There's a bit of utility that comes with that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

AnyPublisher isn't backward compatible with what we have now for rendering since you can't ask for the current value.

Copy link
Collaborator Author

@mjohnson12 mjohnson12 Aug 12, 2025

Choose a reason for hiding this comment

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

Oops I read private not private(set). rendering has to be Public to keep clients working.

Copy link

@blevasseur-block blevasseur-block Aug 12, 2025

Choose a reason for hiding this comment

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

Yup, the above with @Published should make it essentially a public read only version of CurrentValueSubject.

I sort of figured with the AnyPublisher but figured I'd post it anyways just in case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

From what I've gathered reading in the swift forums setting the value on CurrentValueSubject is thread safe. I don't about @Published. I'd have to check on that.

Copy link

@blevasseur-block blevasseur-block Aug 12, 2025

Choose a reason for hiding this comment

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

To my knowledge, both CurrentValueSubject does not guarentee thread safety nor is either documented to be so. You might gain some form of queued up items, but not necessarily the end results you're expecting.

https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-3.html#thread-safety

https://forums.swift.org/t/thread-safety-for-combine-publishers/29491/17

What you're probably looking at is this comment, but there's quite a few undocumented assumptions that come with this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It was a while ago when we looked into CurrentValueSubject. For sure it's not documented anywhere on thread safety (thanks Apple). Property is what we are replacing (I sure wish we hadn't exposed this in the first place) we need something that supports the same semantics.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

After doing some testing it looks like @Published and CurrentValueSubject behave the same.
I do like not having to have an implementation detail exposed with the subject.

import ReactiveSwift
import Workflow

extension WorkflowOutputPublisher {

Choose a reason for hiding this comment

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

question: Is this just a duplicated version of

import Combine
import Foundation
import ReactiveSwift

extension Publisher {
    /// Create a ReactiveSignal
    public func toSignal() -> Signal<Output, Failure> {
        Signal.unserialized { observer, lifetime in
            let cancellable = self.sink(
                receiveCompletion: { completion in
                    switch completion {
                    case .finished:
                        observer.sendCompleted()

                    case .failure(let error):
                        observer.send(error: error)
                    }
                },
                receiveValue: { value in
                    observer.send(value: value)
                }
            )
            lifetime.observeEnded {
                cancellable.cancel()
            }
        }
    }
}

In our ReactiveSwiftCombineBridging library?

Do we have a way of collapsing these if so - or is this more of a temporary transitional item needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We can't reference anything in register in Workflow. I don't think it would be worth adding a new public library just to share that.

Copy link

@blevasseur-block blevasseur-block Aug 12, 2025

Choose a reason for hiding this comment

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

No it wouldn't, but we also need to be careful about fragmented/duplicated public utilities. If this was internal and guaranteed to be 100% unique, I might be more ok with it. Both are public and almost 1-1 duplications meaning fragmentation and potential collisions at some point.

Is this just temporary though as ReactiveSwift is being removed from Workflow or is this a more or less long term duplicated utility?

For the consuming app, I do imagine outputPublisher.toSignal() would work all the same leaving us with outputPublisher.toSignal() and/or outputPublisher.output essentially producing the same thing. I would imagine ouptuPublisher.output could be internal to Workflow and outputPublisher.toSignal() would be the application utility.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We have a lot of usage of the current output signal in the codebase so this keeps all those working by just adding an import statement.
I guess it's as temporary as we have consumers wanting a ReactiveSwift Signal. After we have the combine publisher in the consumers can definitely switch to using that and we could remove this extension.
The overall goal of this change is remove ReactiveSwift without breaking or having to change all the workflows in register.

Choose a reason for hiding this comment

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

Cool - totally get it given it's a migration path strategy.

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.

4 participants