Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make TaskResult a type independent of Result's typealias #38

Merged
merged 3 commits into from
Oct 3, 2023
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
Binary file removed .DS_Store
Binary file not shown.
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@
"version" : "509.0.0"
}
},
{
"identity" : "swiftui-navigation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swiftui-navigation.git",
"state" : {
"revision" : "6eb293c49505d86e9e24232cb6af6be7fff93bd5",
"version" : "1.0.2"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
Expand Down
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ let package = Package(
.package(url: "https://github.com/pointfreeco/swift-case-paths.git", exact: "1.0.0"),
.package(url: "https://github.com/pointfreeco/swift-custom-dump.git", exact: "1.0.0"),
.package(url: "https://github.com/pointfreeco/swift-dependencies.git", exact: "1.0.0"),
.package(url: "https://github.com/pointfreeco/swift-macro-testing", exact: "0.1.0"),
.package(url: "https://github.com/pointfreeco/swiftui-navigation.git", exact: "1.0.2"),
.package(url: "https://github.com/pointfreeco/swift-macro-testing.git", exact: "0.1.0"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
Expand All @@ -36,6 +37,7 @@ let package = Package(
.product(name: "CasePaths", package: "swift-case-paths"),
.product(name: "CustomDump", package: "swift-custom-dump"),
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "SwiftUINavigation", package: "swiftui-navigation"),
]
),
.macro(
Expand Down
29 changes: 29 additions & 0 deletions Sources/SimplexArchitecture/ActionSendable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,35 @@ public extension ActionSendable {
}
}

@discardableResult
func send(_ action: consuming Reducer.Action, animation: Animation?) -> SendTask {
threadCheck()
return if store.container == nil {
withAnimation(animation) {
store.sendAction(action, target: self)
}
} else {
withAnimation(animation) {
store.sendIfNeeded(action)
}
}
}

/// Send an action to the store with transaction
@discardableResult
func send(_ action: consuming Reducer.Action, transaction: Transaction) -> SendTask {
threadCheck()
return if store.container == nil {
withTransaction(transaction) {
store.sendAction(action, target: self)
}
} else {
withTransaction(transaction) {
store.sendIfNeeded(action)
}
}
}

@inline(__always)
func threadCheck() {
#if DEBUG
Expand Down
17 changes: 15 additions & 2 deletions Sources/SimplexArchitecture/Effect/SideEffect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,23 @@ public extension SideEffect {
@inlinable
static func run(
priority: TaskPriority? = nil,
_ operation: @Sendable @escaping (_ send: Send<Reducer>) async throws -> Void,
_ operation: @Sendable @escaping (_ send: Send<Reducer>) async throws -> Void,
catch: (@Sendable (_ error: any Error, _ send: Send<Reducer>) async -> Void)? = nil
) -> Self {
.init(effectKind: .run(priority: priority, operation: operation, catch: `catch`))
// If the .dependency modifier is used, the dependency must be conveyed to the escape context.
withEscapedDependencies { continuation in
.init(
effectKind: .run(
priority: priority,
operation: { send in
try await continuation.yield {
try await operation(send)
}
},
catch: `catch`
)
)
}
}

@inlinable
Expand Down
13 changes: 13 additions & 0 deletions Sources/SimplexArchitecture/Internal/Dependencies+isTesting.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

enum IsTestingKey: DependencyKey {
static let liveValue: Bool = _XCTIsTesting
static let testValue: Bool = _XCTIsTesting
}

extension DependencyValues {
var isTesting: Bool {
get { self[IsTestingKey.self] }
set { self[IsTestingKey.self] = newValue }
}
}
2 changes: 2 additions & 0 deletions Sources/SimplexArchitecture/Internal/Exported.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@_exported import SwiftUINavigation
@_exported import Dependencies
10 changes: 8 additions & 2 deletions Sources/SimplexArchitecture/Internal/TestOnly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@ import XCTestDynamicOverlay
struct TestOnly<T> {
private var _value: T

@Dependency(\.isTesting) var isTesting

var wrappedValue: T {
_read {
if !_XCTIsTesting {
#if DEBUG
if !isTesting {
runtimeWarning("\(Self.self) is accessible only during Unit tests")
}
#endif
yield _value
}
set {
if _XCTIsTesting {
#if DEBUG
if isTesting {
_value = newValue
}
#endif
}
}

Expand Down
9 changes: 7 additions & 2 deletions Sources/SimplexArchitecture/StateContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public final class StateContainer<Target: ActionSendable> {
var _reducerState: Target.Reducer.ReducerState?
var entity: Target
@TestOnly var viewState: Target.ViewState?
@Dependency(\.isTesting) private var isTesting

init(
_ entity: consuming Target,
Expand All @@ -25,21 +26,25 @@ public final class StateContainer<Target: ActionSendable> {

public subscript<U>(dynamicMember keyPath: WritableKeyPath<Target.ViewState, U>) -> U {
_read {
guard !_XCTIsTesting else {
#if DEBUG
guard !isTesting else {
yield viewState![keyPath: keyPath]
return
}
#endif
if let viewKeyPath = Target.ViewState.keyPathMap[keyPath] as? WritableKeyPath<Target, U> {
yield entity[keyPath: viewKeyPath]
} else {
fatalError()
}
}
_modify {
guard !_XCTIsTesting else {
#if DEBUG
guard !isTesting else {
yield &viewState![keyPath: keyPath]
return
}
#endif
if let viewKeyPath = Target.ViewState.keyPathMap[keyPath] as? WritableKeyPath<Target, U> {
yield &entity[keyPath: viewKeyPath]
} else {
Expand Down
8 changes: 7 additions & 1 deletion Sources/SimplexArchitecture/Store/Store+send.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,12 @@ extension Store {
}
}


let sideEffect: SideEffect<Reducer>
// If Unit Testing is in progress and an action is sent from SideEffect
if _XCTIsTesting, let effectContext = EffectContext.id {
#if DEBUG
@Dependency(\.isTesting) var isTesting
if let effectContext = EffectContext.id, isTesting {
let before = container.copy()
sideEffect = reduce(container, action)
sentFromEffectActions.append(
Expand All @@ -86,6 +89,9 @@ extension Store {
} else {
sideEffect = reduce(container, action)
}
#else
sideEffect = reduce(container, action)
#endif

if case .none = sideEffect.kind {
return .never
Expand Down
66 changes: 66 additions & 0 deletions Sources/SimplexArchitecture/SwiftUI/Alert.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import SwiftUI

@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *)
public extension View {
func alert<Target: ActionSendable>(
target: Target,
unwrapping value: Binding<AlertState<Target.Reducer.Action>?>
) -> some View {
self.alert(
(value.wrappedValue?.title).map(Text.init) ?? Text(""),
isPresented: value.isPresent(),
presenting: value.wrappedValue,
actions: { alertState in
ForEach(alertState.buttons) { button in
Button(role: button.role.map(ButtonRole.init)) {
switch button.action.type {
case let .send(action):
if let action = action {
target.send(action)
}
case let .animatedSend(action, animation):
if let action = action {
target.send(action, animation: animation)
}
}
} label: {
Text(button.label)
}
}
},
message: { $0.message.map { Text($0) } }
)
}

func alert<Target: ActionSendable>(
target: Target,
unwrapping value: Binding<AlertState<Target.Reducer.ReducerAction>?>
) -> some View {
self.alert(
(value.wrappedValue?.title).map(Text.init) ?? Text(""),
isPresented: value.isPresent(),
presenting: value.wrappedValue,
actions: { alertState in
ForEach(alertState.buttons) { button in
Button(role: button.role.map(ButtonRole.init)) {
switch button.action.type {
case let .send(action):
if let action = action {
_ = target.store.sendAction(action, target: target)
}
case let .animatedSend(action, animation):
if let action = action {
_ = withAnimation(animation) {
target.store.sendAction(action, target: target)
}
}
}
} label: {
Text(button.label)
}
}
},
message: { $0.message.map { Text($0) } }
)
}
}
68 changes: 68 additions & 0 deletions Sources/SimplexArchitecture/SwiftUI/ConfirmationDialog.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import SwiftUI

@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *)
public extension View {
func confirmationDialog<Target: ActionSendable>(
target: Target,
unwrapping value: Binding<ConfirmationDialogState<Target.Reducer.Action>?>
) -> some View {
self.confirmationDialog(
value.wrappedValue.flatMap { Text($0.title) } ?? Text(""),
isPresented: value.isPresent(),
titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic,
presenting: value.wrappedValue,
actions: { confirmationDialogState in
ForEach(confirmationDialogState.buttons) { button in
Button(role: button.role.map(ButtonRole.init)) {
switch button.action.type {
case let .send(action):
if let action = action {
target.send(action)
}
case let .animatedSend(action, animation):
if let action = action {
target.send(action, animation: animation)
}
}
} label: {
Text(button.label)
}
}
},
message: { $0.message.map { Text($0) } }
)
}

func confirmationDialog<Target: ActionSendable>(
target: Target,
unwrapping value: Binding<ConfirmationDialogState<Target.Reducer.ReducerAction>?>
) -> some View {
self.confirmationDialog(
value.wrappedValue.flatMap { Text($0.title) } ?? Text(""),
isPresented: value.isPresent(),
titleVisibility: value.wrappedValue.map { .init($0.titleVisibility) } ?? .automatic,
presenting: value.wrappedValue,
actions: { confirmationDialogState in
ForEach(confirmationDialogState.buttons) { button in
Button(role: button.role.map(ButtonRole.init)) {
switch button.action.type {
case let .send(action):
if let action = action {
_ = target.store.sendAction(action, target: target)
}
case let .animatedSend(action, animation):
if let action = action {
_ = withAnimation(animation) {
target.store.sendAction(action, target: target)
}
}
}
} label: {
Text(button.label)
}
}
},
message: { $0.message.map { Text($0) } }
)
}
}
3 changes: 2 additions & 1 deletion Sources/SimplexArchitecture/TaskResult+VoidSuccess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ public struct VoidSuccess: Codable, Sendable, Hashable {
}

/// An extension on `TaskResult` where the success type is `VoidSuccess`.
public extension TaskResult where Success == VoidSuccess, Failure == any Error {
public extension TaskResult where Success == VoidSuccess {
/// Creates a new task result by evaluating an async throwing closure.
///
/// This initializer is used to handle asynchronous operations that produce a result of type `Void`.
Expand All @@ -15,6 +15,7 @@ public extension TaskResult where Success == VoidSuccess, Failure == any Error {
/// This initializer is often used within an async effect that's returned from a reducer.
///
/// - Parameter body: An async, throwing closure.
@_disfavoredOverload
init(catching body: @Sendable () async throws -> Void) async {
do {
try await body()
Expand Down
Loading