Skip to content
This repository has been archived by the owner on Oct 23, 2023. It is now read-only.

Commit

Permalink
State Helpers - ValueStatus & ActionStatus (#22)
Browse files Browse the repository at this point in the history
* Update CI

* Tidy up

* Add ActionStatus and ValueStatus
  • Loading branch information
reddavis authored May 9, 2022
1 parent 83e2a2b commit 93e2a0b
Show file tree
Hide file tree
Showing 24 changed files with 321 additions and 90 deletions.
32 changes: 8 additions & 24 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,12 @@ on:
branches: [ main ]

jobs:
build:
name: Test default scheme using any available iPhone simulator
runs-on: macos-latest

ios-latest:
name: Unit Test - iOS 15.4, Xcode 13.3.1
runs-on: macOS-12
env:
DEVELOPER_DIR: /Applications/Xcode_13.3.1.app/Contents/Developer
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Checkout
uses: actions/checkout@v2
- name: Set Default Scheme
run: |
scheme_list=$(xcodebuild -list -json | tr -d "\n")
default=$(echo $scheme_list | ruby -e "require 'json'; puts JSON.parse(STDIN.gets)['project']['targets'][0]")
echo $default | cat >default
echo Using default scheme: $default
- name: Test
env:
scheme: ${{ 'default' }}
run: |
device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}'`
if [ $scheme = default ]; then scheme=$(cat default); fi
if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi
file_to_build=`echo $file_to_build | awk '{$1=$1;print}'`
xcodebuild test -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=iOS Simulator,name=iPhone 13 Pro,OS=15.2"
- uses: actions/checkout@v2
- name: Run Tests
run: Scripts/test -d "OS=15.4,name=iPhone 13 Pro"
20 changes: 0 additions & 20 deletions .github/workflows/swiftlint.yml

This file was deleted.

3 changes: 1 addition & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import PackageDescription


let package = Package(
name: "RedUx",
platforms: [
Expand All @@ -24,7 +23,7 @@ let package = Package(
name: "RedUx",
dependencies: ["Asynchrone"],
path: "RedUx",
exclude: ["Supporting Files/RedUx.docc"]
exclude: []
),
.testTarget(
name: "RedUxTests",
Expand Down
28 changes: 28 additions & 0 deletions RedUx.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
A40EE2A327A02D7600663E6C /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40EE2A227A02D7600663E6C /* ViewModel.swift */; };
A40EE2A527A044FE00663E6C /* ViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40EE2A427A044FE00663E6C /* ViewModelTests.swift */; };
A425FB70275F9769002AFD72 /* ReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A425FB6F275F9769002AFD72 /* ReducerTests.swift */; };
A4773C442827B10800828A14 /* ActionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4773C432827B10800828A14 /* ActionStatus.swift */; };
A4773C462827B12800828A14 /* ValueStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4773C452827B12800828A14 /* ValueStatus.swift */; };
A4773C492827C44600828A14 /* ActionStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4773C482827C44600828A14 /* ActionStatusTests.swift */; };
A4773C4B2827C86A00828A14 /* ValueStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4773C4A2827C86A00828A14 /* ValueStatusTests.swift */; };
A47BE5FB27BC467A0011ECE6 /* Assertions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A48E085C2743D1FE008090E5 /* Assertions.swift */; };
A47BE66927CBE38B0011ECE6 /* AnyAsyncSequenceable+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A47BE66827CBE38B0011ECE6 /* AnyAsyncSequenceable+Helpers.swift */; };
A47BE66C27CBED5F0011ECE6 /* AnyAsyncSequenceableExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A47BE66B27CBED5F0011ECE6 /* AnyAsyncSequenceableExtensionTests.swift */; };
Expand All @@ -35,6 +39,7 @@
A4B9E5A727A269590000ED07 /* RedUxable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B9E5A627A269590000ED07 /* RedUxable.swift */; };
A4C3619A276A086A00511525 /* AppStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C36199276A086A00511525 /* AppStore.swift */; };
A4D2CFA026C0FC34008D25DE /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4D2CF9F26C0FC34008D25DE /* View+Extension.swift */; };
A4E7638D2826D8EE00B99B38 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4E7638C2826D8EE00B99B38 /* Status.swift */; };
A4EE8D352763B4ED00BE7F55 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4EE8D342763B4ED00BE7F55 /* Models.swift */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -82,6 +87,10 @@
A40EE2A227A02D7600663E6C /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = "<group>"; };
A40EE2A427A044FE00663E6C /* ViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelTests.swift; sourceTree = "<group>"; };
A425FB6F275F9769002AFD72 /* ReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReducerTests.swift; sourceTree = "<group>"; };
A4773C432827B10800828A14 /* ActionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionStatus.swift; sourceTree = "<group>"; };
A4773C452827B12800828A14 /* ValueStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValueStatus.swift; sourceTree = "<group>"; };
A4773C482827C44600828A14 /* ActionStatusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionStatusTests.swift; sourceTree = "<group>"; };
A4773C4A2827C86A00828A14 /* ValueStatusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValueStatusTests.swift; sourceTree = "<group>"; };
A47BE66827CBE38B0011ECE6 /* AnyAsyncSequenceable+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyAsyncSequenceable+Helpers.swift"; sourceTree = "<group>"; };
A47BE66B27CBED5F0011ECE6 /* AnyAsyncSequenceableExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyAsyncSequenceableExtensionTests.swift; sourceTree = "<group>"; };
A47E64A2267A1054005E265C /* RedUx.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RedUx.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -110,6 +119,7 @@
A4C36199276A086A00511525 /* AppStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStore.swift; sourceTree = "<group>"; };
A4D2CF9F26C0FC34008D25DE /* View+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = "<group>"; };
A4D2CFA526C16A69008D25DE /* Package.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
A4E7638C2826D8EE00B99B38 /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
A4EE8D342763B4ED00BE7F55 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -165,6 +175,15 @@
path = Details;
sourceTree = "<group>";
};
A4773C422827B0F600828A14 /* Status */ = {
isa = PBXGroup;
children = (
A4773C432827B10800828A14 /* ActionStatus.swift */,
A4773C452827B12800828A14 /* ValueStatus.swift */,
);
path = Status;
sourceTree = "<group>";
};
A47BE5FC27BC46DD0011ECE6 /* Supporting Files */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -239,7 +258,9 @@
children = (
A47BE66A27CBED280011ECE6 /* Extensions */,
A4A12B5827B661DD0094B270 /* Middleware */,
A4773C422827B0F600828A14 /* Status */,
A4A12B5327B51EBC0094B270 /* SwiftUI */,
A4E7638C2826D8EE00B99B38 /* Status.swift */,
A47E64BF267A10E4005E265C /* Store.swift */,
A47E64BE267A10E4005E265C /* Reducer.swift */,
A4B9E5A627A269590000ED07 /* RedUxable.swift */,
Expand Down Expand Up @@ -323,10 +344,12 @@
A4D2CFA326C1486E008D25DE /* Tests */ = {
isa = PBXGroup;
children = (
A4773C482827C44600828A14 /* ActionStatusTests.swift */,
A47BE66B27CBED5F0011ECE6 /* AnyAsyncSequenceableExtensionTests.swift */,
A425FB6F275F9769002AFD72 /* ReducerTests.swift */,
A47E64C4267A10F3005E265C /* StoreTests.swift */,
A40EE2A427A044FE00663E6C /* ViewModelTests.swift */,
A4773C4A2827C86A00828A14 /* ValueStatusTests.swift */,
);
path = Tests;
sourceTree = "<group>";
Expand Down Expand Up @@ -528,7 +551,10 @@
A47BE66927CBE38B0011ECE6 /* AnyAsyncSequenceable+Helpers.swift in Sources */,
A4B9E5A727A269590000ED07 /* RedUxable.swift in Sources */,
A4A12B5C27B6CCC70094B270 /* AnyMiddlewareable.swift in Sources */,
A4773C442827B10800828A14 /* ActionStatus.swift in Sources */,
A4773C462827B12800828A14 /* ValueStatus.swift in Sources */,
A4A12B5A27B6A77D0094B270 /* Middlewareable.swift in Sources */,
A4E7638D2826D8EE00B99B38 /* Status.swift in Sources */,
A40EE2A327A02D7600663E6C /* ViewModel.swift in Sources */,
A47E64C2267A10E4005E265C /* Store.swift in Sources */,
A4D2CFA026C0FC34008D25DE /* View+Extension.swift in Sources */,
Expand All @@ -544,8 +570,10 @@
files = (
A47BE66C27CBED5F0011ECE6 /* AnyAsyncSequenceableExtensionTests.swift in Sources */,
A425FB70275F9769002AFD72 /* ReducerTests.swift in Sources */,
A4773C492827C44600828A14 /* ActionStatusTests.swift in Sources */,
A40EE2A527A044FE00663E6C /* ViewModelTests.swift in Sources */,
A47E64C5267A10F3005E265C /* StoreTests.swift in Sources */,
A4773C4B2827C86A00828A14 /* ValueStatusTests.swift in Sources */,
A47BE5FB27BC467A0011ECE6 /* Assertions.swift in Sources */,
A4EE8D352763B4ED00BE7F55 /* Models.swift in Sources */,
);
Expand Down
4 changes: 0 additions & 4 deletions RedUx/Source/Extensions/AnyAsyncSequenceable+Helpers.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import Asynchrone


extension AnyAsyncSequenceable {

// MARK: Builders

/// Create a simple effect that emits a single event.
/// - Parameter closure: An async closure that returns an event.
/// - Returns: A type erased async sequence.
Expand Down
3 changes: 0 additions & 3 deletions RedUx/Source/Middleware/Middlewareable.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Asynchrone


/// Middleware provides a way to build more complicated application logic that doesn't belong inside
/// the reducer.
///
Expand Down Expand Up @@ -31,11 +30,9 @@ public protocol Middlewareable {
func execute(event: InputEvent, state: () -> State) async
}


// MARK: Pull

extension Middlewareable {

/// Transforms or "Pulls" a scoped middleware into a global middleware.
///
/// Similar to reducers, it may make sense to scope a middleware. For example,
Expand Down
2 changes: 0 additions & 2 deletions RedUx/Source/RedUxable.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import SwiftUI


/// A protocol to help guide structuring a SwiftUI view to use RedUx.
///
/// The use of this protocol isn't required in order to use RedUx. It can be used as a
Expand Down Expand Up @@ -117,7 +116,6 @@ public protocol RedUxable {
// MAKR: Default implementation

extension RedUxable {

/// Create a "live" RedUxable view with a store.
/// - Parameter store: The store
/// - Returns: A view.
Expand Down
4 changes: 0 additions & 4 deletions RedUx/Source/Reducer.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import Asynchrone
import Foundation


/// A reducer is responsible for taking an event and deciding how the state should be changed and
/// whether any effects should be executed.
public struct Reducer<State, Event, Environment> {

// Static

/// An empty reducer. Useful for SwiftUI's previews.
/// - Returns: A reducer.
public static var empty: Reducer<State, Event, Environment> {
Expand Down
1 change: 1 addition & 0 deletions RedUx/Source/Status.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

57 changes: 57 additions & 0 deletions RedUx/Source/Status/ActionStatus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Foundation

/// `ActionStatus` is a helpful enum which can be used to describe the state of a
/// long running action such as; submitting a form, manipulating data, running migrations,
/// executing API requests etc.
///
/// An example of what this could look like being used.
/// ```swift
/// struct AppState: Equatable {
/// var migrationStatus: ActionStatus<MigrationError> = .idle
/// }
/// ```
public enum ActionStatus<ErrorType>: Equatable where ErrorType: Error & Equatable {
/// Action is in an idle state and hasn't yet started.
case idle

/// Action is in a loading state.
case loading

/// Action has been successfully completed.
case complete

/// Action has failed.
case failed(ErrorType)

// MARK: Helpers

/// Indicates whether ActionStatus is in an idle state.
public var isIdle: Bool {
guard case .idle = self else { return false }
return true
}

/// Indicates whether ActionStatus is in a loading state.
public var isLoading: Bool {
guard case .loading = self else { return false }
return true
}

/// Indicates whether ActionStatus is in a complete state.
public var isComplete: Bool {
guard case .complete = self else { return false }
return true
}

/// Indicates whether ActionStatus is in a failed state.
public var isFailed: Bool {
guard case .failed = self else { return false }
return true
}

/// Convenience accessor to the error if the status is in a failed state.
public var error: ErrorType? {
guard case .failed(let error) = self else { return nil }
return error
}
}
100 changes: 100 additions & 0 deletions RedUx/Source/Status/ValueStatus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import Foundation

/// `ValueStatus` is a helpful enum which can be used to describe the state of a
/// long running action that involves a value such as; fetching data from an API.
///
/// An example of what this could look like being used.
/// ```swift
/// struct AppState: Equatable {
/// var blogPosts: ValueStatus<[BlogPost], APIError> = .idle
/// }
/// ```
public enum ValueStatus<Value, ErrorType>: Equatable
where Value: Equatable, ErrorType: Error & Equatable {
/// Status is in an idle state and hasn't yet started.
case idle

/// Status is in a loading state.
///
/// The loading status can provide access to previously loaded
/// value. This can be used to display the previously loaded value
/// along with a loading indicator.
case loading(Value?)

/// Status is complete.
case complete(Value)

/// Status has failed.
///
/// The failed status can provide access to previously loaded
/// value. This can be used to display the previously loaded value
/// along with a error message.
case failed(ErrorType, Value?)

// MARK: Helpers

/// Indicates whether ValueStatus is in an idle state.
public var isIdle: Bool {
guard case .idle = self else { return false }
return true
}

/// Indicates whether ValueStatus is in a loading state.
public var isLoading: Bool {
guard case .loading = self else { return false }
return true
}

/// Indicates whether ValueStatus is in a complete state.
public var isComplete: Bool {
guard case .complete = self else { return false }
return true
}

/// Indicates whether ValueStatus is in a failed state.
public var isFailed: Bool {
guard case .failed = self else { return false }
return true
}

/// Convenience accessor to the error if the status is in a failed state.
public var error: ErrorType? {
guard case .failed(let error, _) = self else { return nil }
return error
}

/// Convenience accessor to the value if the status is in a complete state.
public var value: Value? {
guard case let .complete(value) = self else { return nil }
return value
}

/// Convenience accessor to the latest value of the status.
///
/// This slightly differs from `value` because the value could be coming
/// from `loading` or `failed`, depending on the current status.
///
/// Example:
/// - `.idle`
/// - `latestValue` => nil
/// - `value` => nil
/// - `.loading(nil)`
/// - `latestValue` => nil
/// - `value` => nil
/// - `.complete("value")`
/// - `latestValue` => "value"
/// - `value` => "value"
/// - `.loading("value")`
/// - `latestValue` => "value"
/// - `value` => nil
public var latestValue: Value? {
switch self {
case .complete(let value):
return value
case .loading(let value), .failed(_, let value):
return value
default:
return nil
}
}
}
Loading

0 comments on commit 93e2a0b

Please sign in to comment.