diff --git a/Sources/ScreenStatetKit/States/StateUpdatable.swift b/Sources/ScreenStatetKit/States/StateUpdatable.swift index c65a80d..658232b 100644 --- a/Sources/ScreenStatetKit/States/StateUpdatable.swift +++ b/Sources/ScreenStatetKit/States/StateUpdatable.swift @@ -8,17 +8,17 @@ import SwiftUI @MainActor -public protocol StateUpdatable { +public protocol StateUpdatable: Sendable { func updateState(withAnimation animation: Animation?, - _ updateBlock: @MainActor (_ state: Self) -> Void) + _ updateBlock: @MainActor @Sendable (_ state: Self) -> Void) } extension StateUpdatable { - + public func updateState(withAnimation animation: Animation? = .smooth, - _ updateBlock: @MainActor (_ state: Self) -> Void) { + _ updateBlock: @MainActor @Sendable (_ state: Self) -> Void) { var transaction = Transaction() transaction.animation = animation transaction.disablesAnimations = animation == .none @@ -26,4 +26,24 @@ extension StateUpdatable { updateBlock(self) } } + + /// Reads several `@MainActor` values in a single hop and returns them as a tuple. + /// + /// let values: (queue: [Item], isShowing: Bool) = await state.readState { state in + /// state.queue + /// state.isShowing + /// } + public func readState(@StateValueBuilder _ readBlock: @MainActor @Sendable (_ state: Self) -> T) -> T { + readBlock(self) + } +} + + +@resultBuilder +public enum StateValueBuilder { + + /// One value returns that value; multiple values return a tuple of matching arity. + public static func buildBlock(_ value: repeat each Value) -> (repeat each Value) { + (repeat each value) + } } diff --git a/Tests/ScreenStatetKitTests/States/StateUpdatableTests.swift b/Tests/ScreenStatetKitTests/States/StateUpdatableTests.swift index c66e82d..dee89f7 100644 --- a/Tests/ScreenStatetKitTests/States/StateUpdatableTests.swift +++ b/Tests/ScreenStatetKitTests/States/StateUpdatableTests.swift @@ -32,6 +32,69 @@ struct StateUpdatableTests { #expect(sut.name == "New Name") #expect(sut.count == 42) } + + // MARK: - readState() Tests + + @Test("readState returns a single value") + func test_readState_returnsSingleValue() { + let sut = TestState() + sut.name = "Solo" + + let name: String = sut.readState { state in + state.name + } + + #expect(name == "Solo") + } + + @Test("readState returns a tuple of multiple values") + func test_readState_returnsTupleOfMultipleValues() { + let sut = TestState() + sut.name = "Multi" + sut.count = 7 + + let values: (String, Int) = sut.readState { state in + state.name + state.count + } + + #expect(values.0 == "Multi") + #expect(values.1 == 7) + } + + @Test("readState returns a tuple preserving order and arity") + func test_readState_returnsTuplePreservingArity() { + let sut = TestState() + sut.name = "Triple" + sut.count = 3 + + let values: (String, Int, Bool) = sut.readState { state in + state.name + state.count + state.count > 0 + } + + #expect(values.0 == "Triple") + #expect(values.1 == 3) + #expect(values.2 == true) + } + + @Test("readState reflects values mutated via updateState") + func test_readState_reflectsUpdatedState() { + let sut = TestState() + sut.updateState { state in + state.name = "Fresh" + state.count = 99 + } + + let values: (String, Int) = sut.readState { state in + state.name + state.count + } + + #expect(values.0 == "Fresh") + #expect(values.1 == 99) + } } // MARK: - Helpers