From ad6aea5414743b2e7b39c37cd5ec262441855454 Mon Sep 17 00:00:00 2001 From: Red Davis Date: Wed, 2 Mar 2022 06:43:50 +0000 Subject: [PATCH] Readonly view model value binding (#16) * Expose state so people can easily build their own bindings * Add a readonly binding --- RedUx/Source/ViewModel.swift | 31 ++++++++++++++++++++++++++- RedUxTests/Tests/ViewModelTests.swift | 26 ++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/RedUx/Source/ViewModel.swift b/RedUx/Source/ViewModel.swift index 2497e2d..3734ec6 100644 --- a/RedUx/Source/ViewModel.swift +++ b/RedUx/Source/ViewModel.swift @@ -42,7 +42,7 @@ import SwiftUI public final class ViewModel: ObservableObject { /// The state of the store. - @Published var state: State + @Published public var state: State // Private private var stateTask: Task? @@ -172,4 +172,33 @@ extension ViewModel { event: { _ in event } ) } + + /// Creates a readonly `Binding`. + /// + /// This makes working with SwiftUI components easier. + /// + /// For example: + /// + /// ```swift + /// .fullScreenCover( + /// isPresented: self.viewModel.binding( + /// value: { !$0.isLoggedIn } + /// ), + /// onDismiss: nil, + /// content: { + /// Text("Logged out") + /// } + /// ) + /// ``` + /// - Parameters: + /// - value: A function to extract the value from the state. + /// - Returns: A readonly binding. + public func binding( + value: @escaping (State) -> ScopedState + ) -> Binding { + Binding( + get: { value(self.state) }, + set: { _, _ in } + ) + } } diff --git a/RedUxTests/Tests/ViewModelTests.swift b/RedUxTests/Tests/ViewModelTests.swift index ff53458..1227540 100644 --- a/RedUxTests/Tests/ViewModelTests.swift +++ b/RedUxTests/Tests/ViewModelTests.swift @@ -99,6 +99,8 @@ final class ViewModelTests: XCTestCase { ) } + // MARK: Bindings + func testBindingWithValue() { let binding = self.viewModel.binding( value: \.value, @@ -190,4 +192,28 @@ final class ViewModelTests: XCTestCase { [.setValue(self.value)] ) } + + func testReadonlyBindingReceivesValueChange() { + let binding = self.viewModel.binding( + value: \.value + ) + + XCTAssertNil(binding.wrappedValue) + self.viewModel.send(.setValue(self.value)) + XCTAssertEventuallyEqual( + binding.wrappedValue, + self.value + ) + } + + func testReadonlyBindingDoesNotEmitsEventOnWrappedValueChange() { + let binding = self.viewModel.binding( + value: \.value + ) + binding.wrappedValue = "whatever" + + XCTAssertNil(binding.wrappedValue) + XCTAssertNil(self.store.state.value) + XCTAssertNil(self.viewModel.state.value) + } }