Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Sources/EffectView/EffectView/EffectViewInput.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Foundation

/// A `Sendable` handle for dispatching events into the effect engine.
///
/// `EffectViewInput` provides three dispatch strategies with different semantics:
Expand All @@ -22,17 +24,20 @@
/// - `Event`: The event type dispatched into the state machine.
/// - `Output`: The value returned by ``request(_:)``.
/// Use `Void` when no return value is needed.
public struct EffectViewInput<Event, Output>: TransducerInput, Sendable {
public struct EffectViewInput<Event, Output>: TransducerInput, Identifiable, Sendable {

@MainActor
init(_ send: Send<Event, EffectViewInput, Output>) {
self._send = { @MainActor (event, input, continuation) async throws -> Void in
try await send(event, input: input, continuation: continuation)
}
id = send.id
}

let _send: @MainActor (Event, EffectViewInput, Continuation<Output>?) async throws -> Void

public let id: UUID


/// Sends the given event into the transducer.
///
Expand Down Expand Up @@ -162,3 +167,9 @@ public struct EffectViewInput<Event, Output>: TransducerInput, Sendable {
}
}


extension EffectViewInput: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.id == rhs.id
}
}
7 changes: 6 additions & 1 deletion Sources/EffectView/Transducer/SendFunc.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import Foundation

/// Low-level runtime handle for routing events and control messages.
///
/// Most feature code should use higher-level entry points such as
/// ``EffectView/Input-swift.struct`` or ``EffectObservable/Input-swift.struct``.
public struct Send<Event, Input, Output>
public struct Send<Event, Input, Output>: Identifiable
where Input: TransducerInput<Event, Output> & Sendable {
typealias TaggedEvent = EffectView::TaggedEvent<Event>
typealias SendFunc = (isolated any Actor, Event, Input?, Continuation<Output>?) async throws -> Void
Expand All @@ -14,7 +16,10 @@ where Input: TransducerInput<Event, Output> & Sendable {
init(send: @escaping SendFunc, control: @escaping ControlFunc) {
self.send = send
self.control = control
self.id = .init()
}

public let id: UUID

/// Sends `event` through the runtime using the provided `input` handle.
///
Expand Down
36 changes: 36 additions & 0 deletions Tests/EffectViewTests/EffectViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,42 @@ struct EffectViewTests {
}
}

@Test func inputIdentityRemainsStableAcrossRerenders() async throws {
enum T: Transducer {
struct State: Equatable { var count = 0 }
enum Event: Sendable { case increment }

static func update(_ state: inout State, event: Event) -> Effect? {
state.count += 1
return nil
}
}

var capturedInputs: [EffectViewInput<T.Event, T.Output>] = []
let rerenderExpectation = Expectation()
let timeout: UInt64 = 5_000_000_000

try await testView(initialState: T.State()) { binding in
EffectView(of: T.self, state: binding) { state, input in
Text("\(state.count)")
.onAppear {
capturedInputs.append(input)
}
.onChange(of: state.count) { _, _ in
capturedInputs.append(input)
rerenderExpectation.fulfill()
}
}
} expect: {
#expect(capturedInputs.count == 1)
try await capturedInputs[0].send(.increment)
try await rerenderExpectation.await(nanoseconds: timeout)
#expect(capturedInputs.count == 2)
#expect(capturedInputs[0] == capturedInputs[1])
#expect(capturedInputs[0].id == capturedInputs[1].id)
}
}

// MARK: - initialEvent

@Test func initialEventFiresOnAppear() async throws {
Expand Down
Loading