Skip to content

Commit

Permalink
Added new method to StateContainer that takes a StateSequence instace (
Browse files Browse the repository at this point in the history
…#48)

* Added new method to StateContainer that takes a StateSequence instance. This is different from the method that takes an async closure that returns a StateSequence.

* Update Sources/VSM/StateContainer/StateObserving.swift

Co-authored-by: Albert Bori <[email protected]>

* Update Sources/VSM/StateContainer/StateObserving.swift

Co-authored-by: Albert Bori <[email protected]>

* Update Sources/VSM/StateContainer/StateObserving.swift

Co-authored-by: Albert Bori <[email protected]>

* Update Sources/VSM/StateContainer/StateObserving.swift

Co-authored-by: Albert Bori <[email protected]>

---------

Co-authored-by: Bill Dunay <[email protected]>
Co-authored-by: Albert Bori <[email protected]>
  • Loading branch information
3 people authored Jun 16, 2024
1 parent 680e245 commit 1a83386
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 6 deletions.
26 changes: 26 additions & 0 deletions Sources/VSM/StateContainer/StateContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,20 @@ public extension StateContainer {
cancelRunningObservations()
setStateOnMainThread(to: nextState)
}

// See StateObserving for details
func observe<SomeAsyncSequence: AsyncSequence>(_ stateSequence: SomeAsyncSequence) where SomeAsyncSequence.Element == State {
cancelRunningObservations()
// A weak-self declaration is required on the `Task` closure to break an unexpected strong self retention, despite not directly invoking self ¯\_(ツ)_/¯
stateTask = Task { [weak self] in
for try await newState in stateSequence {
guard !Task.isCancelled else { break }
// GCD is used here instead of `MainActor` to avoid back-ported Swift Concurrency crashes relating to `MainActor` usage
// In a future iOS 15+ version, this class will be converted fully to the `MainActor` paradigm
self?.setStateOnMainThread(to: newState)
}
}
}
}

// MARK: - Observe Debounce Function Overloads
Expand Down Expand Up @@ -194,4 +208,16 @@ public extension StateContainer {
}
debounce(action: debounceableAction)
}

// See StateObserving for details
func observe<SomeAsyncSequence: AsyncSequence>(
_ stateSequence: SomeAsyncSequence,
debounced dueTime: DispatchQueue.SchedulerTimeType.Stride,
identifier: AnyHashable
) where SomeAsyncSequence.Element == State {
let debounceableAction = DebounceableAction(identifier: identifier, dueTime: dueTime) { [weak self] in
self?.observe(stateSequence)
}
debounce(action: debounceableAction)
}
}
42 changes: 36 additions & 6 deletions Sources/VSM/StateContainer/StateObserving.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@ public protocol StateObserving<State> {
func observe(_ statePublisher: some Publisher<State, Never>)

/// Renders the next state on the view.
/// - Parameter nextState: The next view state to render
/// - Parameter nextState: The next view state to render.
func observe(_ nextState: State)

/// Renders an asynchronous sequence states returned on the view.
/// - Parameter stateSequence: A sequence of states to render.
func observe<SomeAsyncSequence: AsyncSequence>(_ stateSequence: SomeAsyncSequence) where SomeAsyncSequence.Element == State

/// Asynchronously renders the next state on the view.
/// - Parameter nextState: The next view state to render
/// - Parameter nextState: An async closure that returns the next state to render.
func observeAsync(_ nextState: @escaping () async -> State)

/// Asynchronously renders the sequence of states on the view.
/// - Parameter stateSequence: The sequence of states to render
/// Calls an async closure that returns an asynchronous sequence of states. Those states are rendered by the view in the order received.
/// - Parameter stateSequence: An async closure that returns a sequence of states.
func observeAsync<SomeAsyncSequence: AsyncSequence>(_ stateSequence: @escaping () async -> SomeAsyncSequence) where SomeAsyncSequence.Element == State

// MARK: - Debounce
Expand Down Expand Up @@ -54,6 +58,18 @@ public protocol StateObserving<State> {
identifier: AnyHashable
)

/// Renders an asynchronous sequence of states returned on the view.
/// Calls to this function are debounced to prevent excessive execution from noisy events.
/// - Parameters:
/// - stateSequence: The action to be debounced before invoking
/// - dueTime: The amount of time required to pass before invoking the most recent action
/// - identifier: (optional) The identifier for grouping actions for debouncing
func observe<SomeAsyncSequence: AsyncSequence>(
_ stateSequence: SomeAsyncSequence,
debounced dueTime: DispatchQueue.SchedulerTimeType.Stride,
identifier: AnyHashable
) where SomeAsyncSequence.Element == State

/// Asynchronously renders the next state on the view.
/// Calls to this function are debounced to prevent excessive execution from noisy events.
/// - Parameters:
Expand All @@ -66,7 +82,7 @@ public protocol StateObserving<State> {
identifier: AnyHashable
)

/// Asynchronously renders the sequence of states on the view.
/// Calls an async closure that returns an asynchronous sequence of states. Those states are rendered by the view in the order received.
/// Calls to this function are debounced to prevent excessive execution from noisy events.
/// - Parameters:
/// - stateSequence: The action to be debounced before invoking
Expand Down Expand Up @@ -109,6 +125,20 @@ public extension StateObserving {
observe(nextState(), debounced: dueTime, identifier: HashedIdentifier(file, line))
}

/// Renders the sequence of asynchronous states returned on the view.
/// Calls to this function are debounced to prevent excessive execution from noisy events.
/// - Parameters:
/// - stateSequence: The action to be debounced before invoking
/// - dueTime: The amount of time required to pass before invoking the most recent action
func observe<SomeAsyncSequence: AsyncSequence>(
_ stateSequence: SomeAsyncSequence,
debounced dueTime: DispatchQueue.SchedulerTimeType.Stride,
file: String = #file,
line: UInt = #line
) where SomeAsyncSequence.Element == State {
observe(stateSequence, debounced: dueTime, identifier: HashedIdentifier(file, line))
}

/// Asynchronously renders the next state on the view.
/// Calls to this function are debounced to prevent excessive execution from noisy events.
/// - Parameters:
Expand All @@ -123,7 +153,7 @@ public extension StateObserving {
observeAsync(nextState, debounced: dueTime, identifier: HashedIdentifier(file, line))
}

/// Asynchronously renders the sequence of states on the view.
/// Calls an async closure that returns an asynchronous sequence of states. Those states are rendered by the view in the order received.
/// Calls to this function are debounced to prevent excessive execution from noisy events.
/// - Parameters:
/// - stateSequence: The action to be debounced before invoking
Expand Down
9 changes: 9 additions & 0 deletions Sources/VSM/ViewState/RenderedViewState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,11 @@ extension RenderedViewState.RenderedContainer: StateObserving & StatePublishing
container.observeAsync(stateSequence)
}

public func observe<SomeAsyncSequence>(_ stateSequence: SomeAsyncSequence)
where SomeAsyncSequence : AsyncSequence, State == SomeAsyncSequence.Element {
container.observe(stateSequence)
}

// MARK: StateObserving Implementation - Debounce
// For more information about these members, view the protocol definition

Expand All @@ -297,6 +302,10 @@ extension RenderedViewState.RenderedContainer: StateObserving & StatePublishing
container.observe(nextState(), debounced: dueTime, identifier: identifier)
}

public func observe<SomeAsyncSequence>(_ stateSequence: SomeAsyncSequence, debounced dueTime: DispatchQueue.SchedulerTimeType.Stride, identifier: AnyHashable) where SomeAsyncSequence : AsyncSequence, State == SomeAsyncSequence.Element {
container.observe(stateSequence, debounced: dueTime, identifier: identifier)
}

public func observeAsync(
_ nextState: @escaping () async -> State,
debounced dueTime: DispatchQueue.SchedulerTimeType.Stride,
Expand Down
23 changes: 23 additions & 0 deletions Tests/VSMTests/StateObservingTests/StateObservingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,27 @@ class StateObservingTests: XCTestCase {
.expect(.grault)
.waitForExpectations(timeout: 5)
}

func testObserveStateSequence() {
let test = statePublisher
.collect(3)
.expect([.foo, .bar, .baz])
subject.observe(StateSequence<MockState>({ .bar }, { .baz }))
XCTAssertEqual(state, .foo)
test.waitForExpectations(timeout: 5)
}

func testObserveStateSequence_Debounced() {
func thunk(state: MockState) {
subject.observe(StateSequence<MockState>({ state }), debounced: 0.0000001)
}
thunk(state: .bar)
thunk(state: .baz)
thunk(state: .grault)
XCTAssertEqual(state, .foo)
statePublisher
.dropFirst()
.expect(.grault)
.waitForExpectations(timeout: 5)
}
}

0 comments on commit 1a83386

Please sign in to comment.