diff --git a/Package.resolved b/Package.resolved index 7d5a11f5..95a98a37 100644 --- a/Package.resolved +++ b/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" + "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", + "version" : "1.1.3" } } ], diff --git a/Tests/StateCopyTests.swift b/Tests/StateCopyTests.swift index f6627939..2348cc3e 100644 --- a/Tests/StateCopyTests.swift +++ b/Tests/StateCopyTests.swift @@ -40,8 +40,8 @@ final class StateCopyTests: XCTestCase { } } - func testStateCopying() async throws { - let store = try await XCTestStore(initial: AppState()) + func testStateCopying() async { + let store = await XCTestStore(initial: AppState()) await store.dispatch(Actions.UpdateFormField(keyPath: \SomeForm.item, value: .init(text: "new item text"))) let item = await store.state.someForm.item diff --git a/Tests/SwiftUI-UDF-ConcurrencyTests/AppStateInitialSetupTests.swift b/Tests/SwiftUI-UDF-ConcurrencyTests/AppStateInitialSetupTests.swift index f0ff924f..b2d67e52 100644 --- a/Tests/SwiftUI-UDF-ConcurrencyTests/AppStateInitialSetupTests.swift +++ b/Tests/SwiftUI-UDF-ConcurrencyTests/AppStateInitialSetupTests.swift @@ -34,8 +34,8 @@ final class AppStateInitialSetupTests: XCTestCase { } } - func test_initialSetups() async throws { - let store = try await XCTestStore(initial: AppState()) + func test_initialSetups() async { + let store = await XCTestStore(initial: AppState()) let title = await store.state.form1.title XCTAssertEqual(title, "new title") diff --git a/Tests/SwiftUI-UDF-ConcurrencyTests/ContainersRedrawing/ContainerScopeTests.swift b/Tests/SwiftUI-UDF-ConcurrencyTests/ContainersRedrawing/ContainerScopeTests.swift index 6f8bae48..029ee6e3 100644 --- a/Tests/SwiftUI-UDF-ConcurrencyTests/ContainersRedrawing/ContainerScopeTests.swift +++ b/Tests/SwiftUI-UDF-ConcurrencyTests/ContainersRedrawing/ContainerScopeTests.swift @@ -48,8 +48,8 @@ final class ContainerScopeTests: XCTestCase { var isUserLoggedIn: Bool = false } - func test_componentRenderingAfterStateMutation() async throws { - let store = try EnvironmentStore(initial: AppState(), logger: TestStoreLogger()) + func test_componentRenderingAfterStateMutation() async { + let store = EnvironmentStore(initial: AppState(), logger: TestStoreLogger()) let itemsContainer = ItemsListContainer() let window = await MainActor.run { @@ -73,8 +73,8 @@ final class ContainerScopeTests: XCTestCase { XCTAssertEqual(itemsContainer.renderingNumber, 2) } - func test_rootComponentRendering() async throws { - let store = try EnvironmentStore(initial: AppState(), logger: TestStoreLogger()) + func test_rootComponentRendering() async { + let store = EnvironmentStore(initial: AppState(), logger: TestStoreLogger()) let rootContainer = RootContainer() let window = await MainActor.run { diff --git a/Tests/SwiftUI-UDF-ConcurrencyTests/ContainersRedrawing/ContainerWithAppStateAsScopeTests.swift b/Tests/SwiftUI-UDF-ConcurrencyTests/ContainersRedrawing/ContainerWithAppStateAsScopeTests.swift index 19c12502..ebea5187 100644 --- a/Tests/SwiftUI-UDF-ConcurrencyTests/ContainersRedrawing/ContainerWithAppStateAsScopeTests.swift +++ b/Tests/SwiftUI-UDF-ConcurrencyTests/ContainersRedrawing/ContainerWithAppStateAsScopeTests.swift @@ -56,8 +56,8 @@ final class ContainerWithAppStateAsScopeTests: XCTestCase { } } - func test_rootComponentRendering() async throws { - let store = try EnvironmentStore(initial: AppState(), logger: TestStoreLogger()) + func test_rootComponentRendering() async { + let store = EnvironmentStore(initial: AppState(), logger: TestStoreLogger()) let rootContainer = RootContainer() let window = await createWindow(with: rootContainer) @@ -94,8 +94,8 @@ final class ContainerWithAppStateAsScopeTests: XCTestCase { XCTAssertEqual(rootContainer.renderingNumber, 5) } - func test_noneScope() async throws { - let store = try EnvironmentStore(initial: AppState(), logger: TestStoreLogger()) + func test_noneScope() async { + let store = EnvironmentStore(initial: AppState(), logger: TestStoreLogger()) let noneScopeContainer = NoneScopeContainer() let window = await createWindow(with: noneScopeContainer) diff --git a/Tests/SwiftUI-UDF-ConcurrencyTests/DispatchActionsTests.swift b/Tests/SwiftUI-UDF-ConcurrencyTests/DispatchActionsTests.swift index 46b0c967..3dfa2510 100644 --- a/Tests/SwiftUI-UDF-ConcurrencyTests/DispatchActionsTests.swift +++ b/Tests/SwiftUI-UDF-ConcurrencyTests/DispatchActionsTests.swift @@ -18,8 +18,8 @@ final class DispatchActionsTests: XCTestCase { var title: String = "" } - func test_UpdateFormFieldDispatch() async throws { - let store = try InternalStore(initial: AppState(), loggers: []) + func test_UpdateFormFieldDispatch() async { + let store = InternalStore(initial: AppState(), loggers: []) var formTitle = await store.state.plainForm.title XCTAssertEqual(formTitle, "") @@ -39,7 +39,7 @@ final class DispatchActionsTests: XCTestCase { XCTAssertTrue(messageInternalUnwrappedAction.silent) - let testStore = try await XCTestStore(initial: AppState()) + let testStore = await XCTestStore(initial: AppState()) await testStore.dispatch(Actions.Message(id: "1")) await testStore.dispatch(Actions.Message(id: "2").silent()) await testStore.dispatch(Actions.Message(id: "3")) diff --git a/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/ConcurrencyMiddlewareCancellationTests.swift b/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/ConcurrencyMiddlewareCancellationTests.swift index 3b1cb082..6b51de8b 100644 --- a/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/ConcurrencyMiddlewareCancellationTests.swift +++ b/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/ConcurrencyMiddlewareCancellationTests.swift @@ -49,8 +49,8 @@ final class ConcurrencyMiddlewareCancellationTests: XCTestCase { } } - func testObservableMiddlewareCancellation() async throws { - let store = try await XCTestStore(initial: AppState()) + func testObservableMiddlewareCancellation() async { + let store = await XCTestStore(initial: AppState()) await store.subscribe(ObservableMiddlewareToCancel.self) await store.dispatch(Actions.Loading()) diff --git a/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/MiddlewareCancellationTests.swift b/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/MiddlewareCancellationTests.swift index 2cae643a..1d21ffb5 100644 --- a/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/MiddlewareCancellationTests.swift +++ b/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/MiddlewareCancellationTests.swift @@ -55,8 +55,8 @@ final class MiddlewareCancellationTests: XCTestCase { } } - func testObservableMiddlewareCancellation() async throws { - let store = try await XCTestStore(initial: AppState()) + func testObservableMiddlewareCancellation() async { + let store = await XCTestStore(initial: AppState()) await store.subscribe(ObservableMiddlewareToCancel.self) await store.dispatch(Actions.Loading()) @@ -70,8 +70,8 @@ final class MiddlewareCancellationTests: XCTestCase { XCTAssertEqual(middlewareFlow, .didCancel) } - func testObservableRunMiddlewareToCancel() async throws { - let store = try await XCTestStore(initial: AppState()) + func testObservableRunMiddlewareToCancel() async { + let store = await XCTestStore(initial: AppState()) await store.subscribe(ObservableRunMiddlewareToCancel.self) await store.dispatch(Actions.Loading()) @@ -86,8 +86,8 @@ final class MiddlewareCancellationTests: XCTestCase { XCTAssertEqual(middlewareFlow, .didCancel) } - func testReducibleMiddlewareToCancel() async throws { - let store = try await XCTestStore(initial: AppState()) + func testReducibleMiddlewareToCancel() async { + let store = await XCTestStore(initial: AppState()) await store.subscribe(ReducibleMiddlewareToCancel.self) await store.dispatch(Actions.Loading()) diff --git a/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/MiddlewareMapErrorTests.swift b/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/MiddlewareMapErrorTests.swift index dfadc8d9..38701ee0 100644 --- a/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/MiddlewareMapErrorTests.swift +++ b/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/MiddlewareMapErrorTests.swift @@ -73,8 +73,8 @@ final class MiddlewareMapErrorTests: XCTestCase { } } - func testMapError() async throws { - let store = try await XCTestStore(initial: AppState()) + func testMapError() async { + let store = await XCTestStore(initial: AppState()) await store.subscribe(LoadingMiddleware.self) await store.dispatch(Actions.StartLoading()) diff --git a/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/MiddlewareSubscriptionTests.swift b/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/MiddlewareSubscriptionTests.swift index 6e292bcd..81c15b81 100644 --- a/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/MiddlewareSubscriptionTests.swift +++ b/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/MiddlewareSubscriptionTests.swift @@ -12,8 +12,8 @@ fileprivate extension Actions { @available(iOS 16.0.0, *) final class MiddlewareSubscriptionTests: XCTestCase { - func testMiddlewareSubscriptions() async throws { - let store = try await XCTestStore(initial: AppState()) + func testMiddlewareSubscriptions() async { + let store = await XCTestStore(initial: AppState()) await store.subscribe(build: { store in ObservableMiddleware.self @@ -34,8 +34,8 @@ final class MiddlewareSubscriptionTests: XCTestCase { XCTAssertEqual(type, .reducible) } - func testEnvironmentMiddlewareSubscription() async throws { - let store = try await XCTestStore(initial: AppState()) + func testEnvironmentMiddlewareSubscription() async { + let store = await XCTestStore(initial: AppState()) await store.subscribe { store in EnvironmentMiddleware.self @@ -46,8 +46,8 @@ final class MiddlewareSubscriptionTests: XCTestCase { XCTAssertEqual(middlewareId, .testEnvironment) } - func liveEnvironmentMiddlewareSubscription() async throws { - let store = try await XCTestStore(initial: AppState()) + func liveEnvironmentMiddlewareSubscription() async { + let store = await XCTestStore(initial: AppState()) setLiveEnvironment() diff --git a/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/NewObservableMiddlewareDDosProtectionTests.swift b/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/NewObservableMiddlewareDDosProtectionTests.swift index 0ded1b14..6688158e 100644 --- a/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/NewObservableMiddlewareDDosProtectionTests.swift +++ b/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/NewObservableMiddlewareDDosProtectionTests.swift @@ -95,8 +95,8 @@ final class NewObservableMiddlewareDDosProtectionTests: XCTestCase { } } - func testObservableMiddlewareDDDos() async throws { - let store = try await XCTestStore(initial: AppState()) + func testObservableMiddlewareDDDos() async { + let store = await XCTestStore(initial: AppState()) await store.subscribe(SendMessageMiddleware.self) await store.wait() diff --git a/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/NewReducibleMiddlewareTests.swift b/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/NewReducibleMiddlewareTests.swift index 7340ab5c..490d07a9 100644 --- a/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/NewReducibleMiddlewareTests.swift +++ b/Tests/SwiftUI-UDF-ConcurrencyTests/Middlewares/NewReducibleMiddlewareTests.swift @@ -60,8 +60,8 @@ final class NewReducibleMiddlewareTests: XCTestCase { } } - func testReducibleMiddleware() async throws { - let store = try await XCTestStore(initial: AppState()) + func testReducibleMiddleware() async { + let store = await XCTestStore(initial: AppState()) await store.subscribe(SendMessageMiddleware.self) var formTitle = await store.state.testForm.title diff --git a/Tests/SwiftUI-UDF-ConcurrencyTests/Store/StoreInitializationTests.swift b/Tests/SwiftUI-UDF-ConcurrencyTests/Store/StoreInitializationTests.swift index d16e8e88..76c54fce 100644 --- a/Tests/SwiftUI-UDF-ConcurrencyTests/Store/StoreInitializationTests.swift +++ b/Tests/SwiftUI-UDF-ConcurrencyTests/Store/StoreInitializationTests.swift @@ -86,7 +86,7 @@ final class StoreInitializationTests: XCTestCase { var store: InternalStore! override func setUpWithError() throws { - store = try InternalStore(initial: AppState(), loggers: []) + store = InternalStore(initial: AppState(), loggers: []) } func test_middlewareAsyncSubscription() async { diff --git a/Tests/ActionGroup/ActionGroupBuilderTests.swift b/Tests/SwiftUI-UDF-Tests/ActionGroup/ActionGroupBuilderTests.swift similarity index 90% rename from Tests/ActionGroup/ActionGroupBuilderTests.swift rename to Tests/SwiftUI-UDF-Tests/ActionGroup/ActionGroupBuilderTests.swift index 2f835051..2a664eb4 100644 --- a/Tests/ActionGroup/ActionGroupBuilderTests.swift +++ b/Tests/SwiftUI-UDF-Tests/ActionGroup/ActionGroupBuilderTests.swift @@ -8,18 +8,6 @@ import XCTest @testable import UDF -fileprivate extension Actions { - struct Message: Action { - public var message: String? - public var id: AnyHashable - - public init(message: String? = nil, id: Id) { - self.message = message?.isEmpty == true ? nil : message - self.id = AnyHashable(id) - } - } -} - final class ActionGroupBuilderTests: XCTestCase { func test_WhenVoid_ActionGroupShouldBeEmpty() { @@ -135,4 +123,16 @@ final class ActionGroupBuilderTests: XCTestCase { XCTAssertEqual(group.actions.count, 5) } + + func test_OptionalAction() { + let optionalActionWithValue: (any Action)? = Actions.Message(message: "m1", id: "m1") + let optionalActionNil: (any Action)? = nil + + let group = ActionGroup { + optionalActionWithValue + optionalActionNil + } + + XCTAssertEqual(group.actions.count, 1) + } } diff --git a/Tests/SwiftUI-UDF-Tests/Alert/AlertActionBuilderTests.swift b/Tests/SwiftUI-UDF-Tests/Alert/AlertActionBuilderTests.swift new file mode 100644 index 00000000..c2b026a9 --- /dev/null +++ b/Tests/SwiftUI-UDF-Tests/Alert/AlertActionBuilderTests.swift @@ -0,0 +1,37 @@ + +import XCTest +@testable import UDF + +final class AlertActionBuilderTests: XCTestCase { + func test_WhenVoid_ActionGroupShouldBeEmpty() { + let style = AlertBuilder.AlertStyle.init(title: "", text: "") { + () + } + + let alertType = style.type + + switch alertType { + case .customActions(_, _, let actions): + XCTAssertTrue(actions().isEmpty, "An Alert should have no action when there is some Void in the builder") + + default: + XCTFail("Alert type should be custom") + } + } + + func test_AlertButton() { + let style = AlertBuilder.AlertStyle.init(title: "", text: "") { + AlertButton.cancel("Cancel") + } + + let alertType = style.type + + switch alertType { + case .customActions(_, _, let actions): + XCTAssertEqual(actions().count, 1) + + default: + XCTFail("Alert type should be custom") + } + } +} diff --git a/Tests/SwiftUI-UDF-Tests/AlertTests.swift b/Tests/SwiftUI-UDF-Tests/AlertTests.swift index 9f31378d..02bc1d00 100644 --- a/Tests/SwiftUI-UDF-Tests/AlertTests.swift +++ b/Tests/SwiftUI-UDF-Tests/AlertTests.swift @@ -9,9 +9,9 @@ fileprivate extension Actions { extension AlertBuilder.AlertStyle { static func alertWithAction(_ action: @escaping () -> Void) -> Self { .init(title: "Custom alert title with action", text: "Custom alert text with action") { - AlertAction(title: "Action button", action: action) + AlertButton(title: "Action button", action: action) - AlertAction(title: "Cancel") + AlertButton(title: "Cancel") .role(.cancel) } } @@ -41,8 +41,8 @@ final class AlertTests: XCTestCase { } } - func test_WhenAlerBuilderRegistered_AlertCanBePresentedById() async throws { - let store = try await XCTestStore(initial: AppState()) + func test_WhenAlerBuilderRegistered_AlertCanBePresentedById() async { + let store = await XCTestStore(initial: AppState()) var status = await store.state.form.alert.status XCTAssertEqual(status, .dismissed) diff --git a/Tests/SwiftUI-UDF-Tests/Cached/CachedTests.swift b/Tests/SwiftUI-UDF-Tests/Cached/CachedTests.swift index 376b3f4f..0398bd92 100644 --- a/Tests/SwiftUI-UDF-Tests/Cached/CachedTests.swift +++ b/Tests/SwiftUI-UDF-Tests/Cached/CachedTests.swift @@ -58,8 +58,8 @@ class CachedTests: XCTestCase { } } - func testItemsCaching() async throws { - var store = try await XCTestStore(initial: AppState()) + func testItemsCaching() async { + var store = await XCTestStore(initial: AppState()) let items = (0...3).map { Item(id: .init(value: $0)) } await store.dispatch(Actions.DidLoadItems(items: items, id: "items")) @@ -72,7 +72,7 @@ class CachedTests: XCTestCase { await fulfill(description: "waiting for cache syncing", sleep: 1.5) - store = try await .init(initial: AppState()) + store = await .init(initial: AppState()) isEmpty = await store.state.nestedForm.items.isEmpty XCTAssertFalse(isEmpty) @@ -81,8 +81,8 @@ class CachedTests: XCTestCase { XCTAssertEqual(count, 4) } - func testResetCache() async throws { - let store = try await XCTestStore(initial: AppState()) + func testResetCache() async { + let store = await XCTestStore(initial: AppState()) let items = (0...3).map { Item(id: .init(value: $0)) } await store.dispatch(Actions.DidLoadItems(items: items, id: "items")) @@ -99,8 +99,8 @@ class CachedTests: XCTestCase { XCTAssertTrue(isEmpty) } - func testSingleObjectCaching() async throws { - let store = try await XCTestStore(initial: AppState()) + func testSingleObjectCaching() async { + let store = await XCTestStore(initial: AppState()) var selectedItem = await store.state.nestedForm.selectedItem XCTAssertNil(selectedItem) @@ -117,7 +117,7 @@ class CachedTests: XCTestCase { } func testRemoveItemFromCacheById() async throws { - let store = try await XCTestStore(initial: AppState()) + let store = await XCTestStore(initial: AppState()) await store.dispatch(Actions.ResetCache()) let items = [Item(id: .init(value: 0))] diff --git a/Tests/SwiftUI-UDF-Tests/ContainerLifecycleTests.swift b/Tests/SwiftUI-UDF-Tests/ContainerLifecycleTests.swift index 3f9eafb7..3107921b 100644 --- a/Tests/SwiftUI-UDF-Tests/ContainerLifecycleTests.swift +++ b/Tests/SwiftUI-UDF-Tests/ContainerLifecycleTests.swift @@ -29,8 +29,8 @@ final class ContainerLifecycleTests: XCTestCase { } @MainActor - func test_ContainerLifecycle() async throws { - let store = try EnvironmentStore(initial: AppState(), logger: .consoleDebug) + func test_ContainerLifecycle() async { + let store = EnvironmentStore(initial: AppState(), logger: .consoleDebug) let rootContainer = RootContainer() var window: UIWindow? = await MainActor.run { diff --git a/Tests/SwiftUI-UDF-Tests/Mergeable/MergeableAppStateTests.swift b/Tests/SwiftUI-UDF-Tests/Mergeable/MergeableAppStateTests.swift index 8c17be68..67065d34 100644 --- a/Tests/SwiftUI-UDF-Tests/Mergeable/MergeableAppStateTests.swift +++ b/Tests/SwiftUI-UDF-Tests/Mergeable/MergeableAppStateTests.swift @@ -47,7 +47,7 @@ class MergeableAppStateTests: XCTestCase { } func testItemMerging() async throws { - let store = try await XCTestStore(initial: AppState()) + let store = await XCTestStore(initial: AppState()) var item = Item(id: .init(value: 1), title: "original") await store.dispatch(Actions.DidLoadItem(item: item)) diff --git a/Tests/SwiftUI-UDF-Tests/Middlewares/MiddlewareCancellationTests.swift b/Tests/SwiftUI-UDF-Tests/Middlewares/MiddlewareCancellationTests.swift index 28321c4e..51498e3e 100644 --- a/Tests/SwiftUI-UDF-Tests/Middlewares/MiddlewareCancellationTests.swift +++ b/Tests/SwiftUI-UDF-Tests/Middlewares/MiddlewareCancellationTests.swift @@ -57,8 +57,8 @@ final class MiddlewareCancellationTests: XCTestCase { } } - func testObservableMiddlewareCancellation() async throws { - let store = try await XCTestStore(initial: AppState()) + func testObservableMiddlewareCancellation() async { + let store = await XCTestStore(initial: AppState()) await store.subscribe(ObservableMiddlewareToCancel.self) await store.dispatch(Actions.Loading()) @@ -73,8 +73,8 @@ final class MiddlewareCancellationTests: XCTestCase { XCTAssertEqual(middlewareFlow, .none) } - func testObservableRunMiddlewareToCancel() async throws { - let store = try await XCTestStore(initial: AppState()) + func testObservableRunMiddlewareToCancel() async { + let store = await XCTestStore(initial: AppState()) await store.subscribe(ObservableRunMiddlewareToCancel.self) await store.dispatch(Actions.Loading()) @@ -92,8 +92,8 @@ final class MiddlewareCancellationTests: XCTestCase { XCTAssertEqual(middlewareFlow, .none) } - func testReducibleMiddlewareToCancel() async throws { - let store = try await XCTestStore(initial: AppState()) + func testReducibleMiddlewareToCancel() async { + let store = await XCTestStore(initial: AppState()) await store.subscribe(ReducibleMiddlewareToCancel.self) await store.dispatch(Actions.Loading()) diff --git a/Tests/SwiftUI-UDF-Tests/Middlewares/ObservableMiddlewareTests.swift b/Tests/SwiftUI-UDF-Tests/Middlewares/ObservableMiddlewareTests.swift index a27326a9..631f6a7b 100644 --- a/Tests/SwiftUI-UDF-Tests/Middlewares/ObservableMiddlewareTests.swift +++ b/Tests/SwiftUI-UDF-Tests/Middlewares/ObservableMiddlewareTests.swift @@ -87,8 +87,8 @@ class ObservableMiddlewareTests: XCTestCase { } } - func testObservableMiddlewareOnceCalling() async throws { - let store = try await XCTestStore(initial: AppState()) + func testObservableMiddlewareOnceCalling() async { + let store = await XCTestStore(initial: AppState()) await store.subscribe(SendMessageMiddleware.self) let message = "Flow message 1" diff --git a/Tests/SwiftUI-UDF-Tests/Middlewares/ReducibleMiddlewareTests.swift b/Tests/SwiftUI-UDF-Tests/Middlewares/ReducibleMiddlewareTests.swift index a8985c95..a77959d1 100644 --- a/Tests/SwiftUI-UDF-Tests/Middlewares/ReducibleMiddlewareTests.swift +++ b/Tests/SwiftUI-UDF-Tests/Middlewares/ReducibleMiddlewareTests.swift @@ -75,8 +75,8 @@ class ReducibleMiddlewareTests: XCTestCase { } } - func testReducibleMiddleware() async throws { - let store = try await XCTestStore(initial: AppState()) + func testReducibleMiddleware() async { + let store = await XCTestStore(initial: AppState()) await store.subscribe(ServiceMiddleware.self) let message = "Service title 1" diff --git a/Tests/SwiftUI-UDF-Tests/NestedReducerTests.swift b/Tests/SwiftUI-UDF-Tests/NestedReducerTests.swift index 133ac9d5..54f3d735 100644 --- a/Tests/SwiftUI-UDF-Tests/NestedReducerTests.swift +++ b/Tests/SwiftUI-UDF-Tests/NestedReducerTests.swift @@ -65,8 +65,8 @@ final class NestedReducerTests: XCTestCase { var cancellation: AnyCancellable? = nil - func testAppState() async throws { - let store = try await XCTestStore(initial: AppState()) + func testAppState() async { + let store = await XCTestStore(initial: AppState()) await store.dispatch(Actions.UpdateFormField(keyPath: \TestForm.title, value: "temp")) await store.dispatch(Actions.UpdateFormField(keyPath: \TestForm.title, value: "temp_21")) diff --git a/Tests/SwiftUI-UDF-Tests/PaginatorTests.swift b/Tests/SwiftUI-UDF-Tests/PaginatorTests.swift index f7f8cf90..49d1e0e0 100644 --- a/Tests/SwiftUI-UDF-Tests/PaginatorTests.swift +++ b/Tests/SwiftUI-UDF-Tests/PaginatorTests.swift @@ -135,8 +135,8 @@ class PaginatorTests: XCTestCase { XCTAssertEqual(paginator.items.count, 10) } - func testPaginatorLoading() async throws { - let store = try await XCTestStore(initial: AppState()) + func testPaginatorLoading() async { + let store = await XCTestStore(initial: AppState()) await store.dispatch(Actions.LoadPage(id: ItemFlow.id)) let isLoading = await store.state.itemsForm.paginator.isLoading diff --git a/Tests/VerifierTests.swift b/Tests/VerifierTests.swift deleted file mode 100644 index c57aa7d1..00000000 --- a/Tests/VerifierTests.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// VerifierTests.swift -// SwiftUI-UDFTests -// -// Created by Max Kuznetsov on 12.04.2022. -// - -import XCTest -@testable import UDF - -fileprivate extension Actions { - struct Message: Action { - public var message: String? - public var id: AnyHashable - - public init(message: String? = nil, id: Id) { - self.message = message?.isEmpty == true ? nil : message - self.id = AnyHashable(id) - } - } -} - -class VerifierTests: XCTestCase { - - struct AppState: AppReducer {} - - var store: XCTestStore! - - func testExample() async throws { - store = try await XCTestStore(initial: AppState()) - - //NOTE: Should not be crashed - await store.dispatch(Actions.Message(id: "message")) - } -} diff --git a/UDF/Common/Debouncer.swift b/UDF/Common/Debouncer/Debouncer.swift similarity index 99% rename from UDF/Common/Debouncer.swift rename to UDF/Common/Debouncer/Debouncer.swift index fcb5eb61..036ac0e5 100644 --- a/UDF/Common/Debouncer.swift +++ b/UDF/Common/Debouncer/Debouncer.swift @@ -52,4 +52,3 @@ private extension Debouncer { if let value = self.value { callbacks.forEach { $0(value) } } } } - diff --git a/UDF/Common/Debouncer/UserInputDebouncer.swift b/UDF/Common/Debouncer/UserInputDebouncer.swift new file mode 100644 index 00000000..012c6acd --- /dev/null +++ b/UDF/Common/Debouncer/UserInputDebouncer.swift @@ -0,0 +1,21 @@ + +import Foundation +import Combine + +final class UserInputDebouncer: ObservableObject { + @Published var debouncedValue: T + @Published var value: T + + private var cancelation: AnyCancellable! + + init(defaultValue: T, debounceTime: TimeInterval = 0.1) { + value = defaultValue + debouncedValue = defaultValue + + cancelation = $value + .debounce(for: .seconds(debounceTime), scheduler: DispatchQueue.main) + .sink { [weak self] value in + self?.debouncedValue = value + } + } +} diff --git a/UDF/Common/Extensions/Dictionary+Storage.swift b/UDF/Common/Extensions/Dictionary+Storage.swift index 3cb5d6b3..6b99f698 100644 --- a/UDF/Common/Extensions/Dictionary+Storage.swift +++ b/UDF/Common/Extensions/Dictionary+Storage.swift @@ -28,3 +28,19 @@ public extension Dictionary { append(values.map(\.id), by: key) } } + +public extension Dictionary { + mutating func append(_ value: Value, by key: Key) where Value == Dictionary { + var dict: Value = self[key] ?? [:] + dict.merge(dict: value) + self[key] = dict + } +} + +public extension Dictionary { + mutating func merge(dict: [Key: Value]){ + for (k, v) in dict { + updateValue(v, forKey: k) + } + } +} diff --git a/UDF/Common/Extensions/NavigationDestination+Router.swift b/UDF/Common/Extensions/NavigationDestination+Router.swift deleted file mode 100644 index de06a7fd..00000000 --- a/UDF/Common/Extensions/NavigationDestination+Router.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import SwiftUI - -#if os(iOS) -public extension View { - func navigationDestination(router: Router) -> some View where R.Route: Hashable { - self - .navigationDestination( - for: R.Route.self, - destination: router.view(for:) - ) - } -} -#endif diff --git a/UDF/Common/Extensions/View+AlertStatus.swift b/UDF/Common/Extensions/View+AlertStatus.swift index 9f183c2d..27f86dc1 100644 --- a/UDF/Common/Extensions/View+AlertStatus.swift +++ b/UDF/Common/Extensions/View+AlertStatus.swift @@ -73,15 +73,21 @@ private struct AlertModifier: ViewModifier { alertStatus = .dismissed } }).isPresented(), actions: { - ForEach(alertActions(for: type), id: \.id) { action in - Button(action.title, role: action.role, action: action.action) + let actions = alertActions(for: type) + ForEach(Array(actions.enumerated()), id: \.offset) { _, action in + switch action { + case let action as AlertButton: action.id(action.hashValue) + case let action as AlertTextField: action + default: + EmptyView() + } } }, message: { Text(texts.text()) }) } - func alertActions(for type: AlertBuilder.AlertStyle.AlertType) -> [AlertAction] { + func alertActions(for type: AlertBuilder.AlertStyle.AlertType) -> [any AlertAction] { switch type { case let .custom(_, _, primaryButton, secondaryButton): if primaryButton.role == .destructive { @@ -99,7 +105,7 @@ private struct AlertModifier: ViewModifier { return actions() default: - return [AlertAction(id: "default_action", title: NSLocalizedString("Ok", comment: "Ok"))] + return [AlertButton(title: NSLocalizedString("Ok", comment: "Ok"))] } } diff --git a/UDF/Common/Extensions/View+NavigationDestination.swift b/UDF/Common/Extensions/View+NavigationDestination.swift index 4899c4c7..5b039c07 100644 --- a/UDF/Common/Extensions/View+NavigationDestination.swift +++ b/UDF/Common/Extensions/View+NavigationDestination.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Foundation #if os(iOS) public extension View { @@ -16,7 +17,12 @@ public extension View { } } } + + func navigationDestination(router: Router) -> some View where R.Route: Hashable { + modifier(GlobalRoutingModifier(router: router)) + } } + #endif fileprivate extension Binding { diff --git a/UDF/Common/Merging/Mergeable+Dictionary.swift b/UDF/Common/Merging/Mergeable+Dictionary.swift new file mode 100644 index 00000000..25a11457 --- /dev/null +++ b/UDF/Common/Merging/Mergeable+Dictionary.swift @@ -0,0 +1,43 @@ + +import Foundation + +public extension Dictionary where Value: Mergeable { + + subscript(key: Key) -> Value { + get { + preconditionFailure("You have to use optional subscript") + } + set { + self[key] = self[key]?.merging(newValue) ?? newValue + } + } +} + +public extension Dictionary where Value: Identifiable, Key == Value.ID { + + mutating func insert(items: [Value]) { + items.forEach { item in + self[item.id] = item + } + } + + mutating func insert(item: Value) { + self[item.id] = item + } +} + +public typealias MI = Mergeable & Identifiable +public extension Dictionary where Value: MI, Key == Value.ID { + + mutating func insert(items: [Value]) { + items.forEach { item in + self[item.id] = self[item.id]?.merging(item) ?? item + } + } + + mutating func insert(item: Value) { + self[item.id] = self[item.id]?.merging(item) ?? item + } +} + + diff --git a/UDF/Common/Merging/Mergeable+OrderedDictionary.swift b/UDF/Common/Merging/Mergeable+OrderedDictionary.swift new file mode 100644 index 00000000..5fb366e7 --- /dev/null +++ b/UDF/Common/Merging/Mergeable+OrderedDictionary.swift @@ -0,0 +1,42 @@ + +import Foundation +import OrderedCollections + +public extension OrderedDictionary where Value: Mergeable { + + subscript(key: Key) -> Value { + get { + preconditionFailure("You have to use optional subscript") + } + set { + self[key] = self[key]?.merging(newValue) ?? newValue + } + } +} + +public extension OrderedDictionary where Value: Identifiable, Key == Value.ID { + + mutating func insert(items: [Value]) { + items.forEach { item in + self[item.id] = item + } + } + + mutating func insert(item: Value) { + self[item.id] = item + } +} + +public typealias OMI = Mergeable & Identifiable +public extension OrderedDictionary where Value: MI, Key == Value.ID { + + mutating func insert(items: [Value]) { + items.forEach { item in + self[item.id] = self[item.id]?.merging(item) ?? item + } + } + + mutating func insert(item: Value) { + self[item.id] = self[item.id]?.merging(item) ?? item + } +} diff --git a/UDF/Common/Merging/Mergeable.swift b/UDF/Common/Merging/Mergeable.swift index e02dea65..36ecf8f0 100644 --- a/UDF/Common/Merging/Mergeable.swift +++ b/UDF/Common/Merging/Mergeable.swift @@ -6,42 +6,3 @@ public protocol Mergeable { func filled(from value: Self, mutate: (_ filled: inout Self, _ old: Self) -> Void) -> Self } - -public extension Dictionary where Value: Mergeable { - - subscript(key: Key) -> Value { - get { - preconditionFailure("You have to use optional subscript") - } - set { - self[key] = self[key]?.merging(newValue) ?? newValue - } - } -} - -public extension Dictionary where Value: Identifiable, Key == Value.ID { - - mutating func insert(items: [Value]) { - items.forEach { item in - self[item.id] = item - } - } - - mutating func insert(item: Value) { - self[item.id] = item - } -} - -public typealias MI = Mergeable & Identifiable -public extension Dictionary where Value: MI, Key == Value.ID { - - mutating func insert(items: [Value]) { - items.forEach { item in - self[item.id] = self[item.id]?.merging(item) ?? item - } - } - - mutating func insert(item: Value) { - self[item.id] = self[item.id]?.merging(item) ?? item - } -} diff --git a/UDF/Common/WeakObject.swift b/UDF/Common/WeakObject.swift new file mode 100644 index 00000000..0223d0ef --- /dev/null +++ b/UDF/Common/WeakObject.swift @@ -0,0 +1,15 @@ + +import Foundation + +final class Weak { + weak var value: AnyObject? + init (value: AnyObject) { + self.value = value + } +} + +extension Array where Element: Weak { + mutating func reap () { + self = self.filter { $0.value != nil } + } +} diff --git a/UDF/Store/Action/ActionGroup/ActionGroupBuilder.swift b/UDF/Store/Action/ActionGroup/ActionGroupBuilder.swift index d0776250..64592101 100644 --- a/UDF/Store/Action/ActionGroup/ActionGroupBuilder.swift +++ b/UDF/Store/Action/ActionGroup/ActionGroupBuilder.swift @@ -85,6 +85,10 @@ public enum ActionGroupBuilder { [] } + public static func buildExpression(_ expression: (any Equatable)?) -> [any Equatable] { + [expression].compactMap({ $0 }) + } + public static func buildOptional(_ component: [any Equatable]?) -> [any Equatable] { component ?? [] } diff --git a/UDF/Store/Action/Actions.swift b/UDF/Store/Action/Actions.swift index a41daed4..b2872cb0 100644 --- a/UDF/Store/Action/Actions.swift +++ b/UDF/Store/Action/Actions.swift @@ -354,3 +354,90 @@ public extension Actions { } } } + +// MARK: - Global Navigation +public extension Actions { + struct Navigate: Action { + public static func == (lhs: Actions.Navigate, rhs: Actions.Navigate) -> Bool { + true + } + + public let to: [any Hashable] + + public init(to: any Hashable) { + self.to = [to] + } + + public init(path: [any Hashable]) { + self.to = path + } + } + + struct NavigateResetStack: Action { + public static func == (lhs: Actions.NavigateResetStack, rhs: Actions.NavigateResetStack) -> Bool { + true + } + + public let to: [any Hashable] + + public init(to: any Hashable) { + self.to = [to] + } + + public init(path: [any Hashable]) { + self.to = path + } + } + + struct NavigationBackToRoot: Action { + public init() {} + } + + struct NavigateBack: Action { + public init() {} + } +} + + +// MARK: - Global Navigation Typed +public extension Actions { + struct NavigateTyped: Action { + public static func == (lhs: Actions.NavigateTyped, rhs: Actions.NavigateTyped) -> Bool { + true + } + + public let to: [any Hashable] + + public init(to: any Hashable) { + self.to = [to] + } + + public init(path: [any Hashable]) { + self.to = path + } + } + + struct NavigateResetStackTyped: Action { + public static func == (lhs: Actions.NavigateResetStackTyped, rhs: Actions.NavigateResetStackTyped) -> Bool { + true + } + + public let to: [any Hashable] + + public init(to: any Hashable) { + self.to = [to] + } + + public init(path: [any Hashable]) { + self.to = path + } + } + + struct NavigationBackToRootTyped: Action { + public init() {} + } + + struct NavigateBackTyped: Action { + public init() {} + } +} diff --git a/UDF/Store/EnvironmentStore.swift b/UDF/Store/EnvironmentStore.swift index 174e93c1..9bee7910 100644 --- a/UDF/Store/EnvironmentStore.swift +++ b/UDF/Store/EnvironmentStore.swift @@ -12,11 +12,11 @@ public final class EnvironmentStore { private let subscribersCoordinator: SubscribersCoordinator> = SubscribersCoordinator() private let storeQueue: DispatchQueue = .init(label: "EnvironmentStore") - public init(initial state: State, loggers: [ActionLogger]) throws { + public init(initial state: State, loggers: [ActionLogger]) { var mutableState = state mutableState.initialSetup() - let store = try InternalStore(initial: mutableState, loggers: loggers) + let store = InternalStore(initial: mutableState, loggers: loggers) self.store = store self._state = .init(wrappedValue: mutableState, store: store) @@ -24,8 +24,8 @@ public final class EnvironmentStore { GlobalValue.set(self) } - public convenience init(initial state: State, logger: ActionLogger) throws { - try self.init(initial: state, loggers: [logger]) + public convenience init(initial state: State, logger: ActionLogger) { + self.init(initial: state, loggers: [logger]) } public func dispatch( diff --git a/UDF/Store/InternalStore.swift b/UDF/Store/InternalStore.swift index 9a009a1f..4730c207 100644 --- a/UDF/Store/InternalStore.swift +++ b/UDF/Store/InternalStore.swift @@ -19,7 +19,7 @@ actor InternalStore: Store { private let storeQueue: StoreQueue = .init() private let logDistributor: LogDistributor - init(initial state: State, loggers: [ActionLogger]) throws { + init(initial state: State, loggers: [ActionLogger]) { self.loggers = loggers self.state = state self.logDistributor = LogDistributor(loggers: loggers) diff --git a/UDF/Store/XCTestStore/XCTestStore.swift b/UDF/Store/XCTestStore/XCTestStore.swift index 12aa0a9c..fde4ace7 100644 --- a/UDF/Store/XCTestStore/XCTestStore.swift +++ b/UDF/Store/XCTestStore/XCTestStore.swift @@ -30,7 +30,7 @@ public final class XCTestStore { private var store: InternalStore private var cancelation: Cancellable? = nil - public init(initial state: State) throws { + public init(initial state: State) { guard ProcessInfo.processInfo.xcTest else { fatalError("XCTestStore is only for using in Test targets") } @@ -38,7 +38,7 @@ public final class XCTestStore { var mutableState = state mutableState.initialSetup() - let store = try InternalStore(initial: mutableState, loggers: [TestStoreLogger()]) + let store = InternalStore(initial: mutableState, loggers: [TestStoreLogger()]) self.store = store self._state = .init(wrappedValue: mutableState, store: store) diff --git a/UDF/View/AlertBuilder/AlertAction.swift b/UDF/View/AlertBuilder/AlertAction.swift deleted file mode 100644 index 3bfd0a75..00000000 --- a/UDF/View/AlertBuilder/AlertAction.swift +++ /dev/null @@ -1,72 +0,0 @@ - -import Foundation -import SwiftUI - -public struct AlertAction: Identifiable { - public static func == (lhs: AlertAction, rhs: AlertAction) -> Bool { - return lhs.id == rhs.id - } - - public var id: AnyHashable - public var title: String - public var role: ButtonRole? - public var action: () -> () - - public init( - id: ID, - title: String, - action: @escaping () -> Void = {} - ) { - self.id = AnyHashable(id) - self.title = title - self.action = action - } - - public init( - title: String, - action: @escaping () -> Void = {} - ) { - self.id = AnyHashable(UUID()) - self.title = title - self.action = action - } - - public func role(_ role: ButtonRole) -> Self { - var newAction = self - newAction.role = role - return newAction - } -} - -public extension AlertAction { - static func `default`(_ title: String, action: @escaping () -> Void = {}) -> AlertAction { - AlertAction(title: title, action: action) - } - - @available(*, deprecated, message: "use `default` with String instead of Text") - static func `default`(_ text: Text, action: @escaping () -> Void = {}) -> AlertAction { - AlertAction(title: text.content ?? "", action: action) - } - - static func cancel(_ title: String, action: @escaping () -> Void = {}) -> AlertAction { - AlertAction(title: title, action: action) - .role(.cancel) - } - - @available(*, deprecated, message: "use `cancel` with String instead of Text") - static func cancel(_ text: Text, action: @escaping () -> Void = {}) -> AlertAction { - AlertAction(title: text.content ?? "", action: action) - .role(.cancel) - } - - static func destructive(_ title: String, action: @escaping () -> Void = {}) -> AlertAction { - AlertAction(title: title, action: action) - .role(.destructive) - } - - @available(*, deprecated, message: "use `destructive` with String instead of Text") - static func destructive(_ text: Text, action: @escaping () -> Void = {}) -> AlertAction { - AlertAction(title: text.content ?? "", action: action) - .role(.destructive) - } -} diff --git a/UDF/View/AlertBuilder/AlertAction/AlertAction.swift b/UDF/View/AlertBuilder/AlertAction/AlertAction.swift new file mode 100644 index 00000000..3f33ecd5 --- /dev/null +++ b/UDF/View/AlertBuilder/AlertAction/AlertAction.swift @@ -0,0 +1,13 @@ + +import Foundation +import SwiftUI + +public protocol AlertAction: Hashable {} + +extension AlertAction { + func mutate(_ block: (inout Self) -> Void) -> Self { + var copy = self + block(©) + return copy + } +} diff --git a/UDF/View/AlertBuilder/AlertAction/AlertButton.swift b/UDF/View/AlertBuilder/AlertAction/AlertButton.swift new file mode 100644 index 00000000..fdc502d8 --- /dev/null +++ b/UDF/View/AlertBuilder/AlertAction/AlertButton.swift @@ -0,0 +1,83 @@ + +import Foundation +import SwiftUI + +public struct AlertButton: AlertAction { + public var title: String + public var role: ButtonRole? + public var disabled: Bool = false + public var action: () -> () + + public static func == (lhs: AlertButton, rhs: AlertButton) -> Bool { + lhs.title == rhs.title && lhs.role == rhs.role && lhs.disabled == rhs.disabled + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(title) + hasher.combine(disabled) + } + + public init( + title: String, + action: @escaping () -> Void = {} + ) { + self.title = title + self.action = action + } + + public var body: some View { + Button(title, role: role, action: action) + .disabled(disabled) + } +} + +extension AlertButton: View {} + +// MARK: - Modifiers +public extension AlertButton { + func role(_ role: ButtonRole) -> AlertButton { + mutate { button in + button.role = role + } + } + + func disabled(_ disabled: Bool) -> AlertButton { + mutate { button in + button.disabled = disabled + } + } +} + +// MARK: - Predifined buttons +public extension AlertAction where Self == AlertButton { + static func `default`(_ title: String, action: @escaping () -> Void = {}) -> Self { + AlertButton(title: title, action: action) + } + + @available(*, deprecated, message: "use `default` with String instead of Text") + static func `default`(_ text: Text, action: @escaping () -> Void = {}) -> Self { + AlertButton(title: text.content ?? "", action: action) + } + + static func cancel(_ title: String, action: @escaping () -> Void = {}) -> Self { + AlertButton(title: title, action: action) + .role(.cancel) + } + + @available(*, deprecated, message: "use `cancel` with String instead of Text") + static func cancel(_ text: Text, action: @escaping () -> Void = {}) -> Self { + AlertButton(title: text.content ?? "", action: action) + .role(.cancel) + } + + static func destructive(_ title: String, action: @escaping () -> Void = {}) -> Self { + AlertButton(title: title, action: action) + .role(.destructive) + } + + @available(*, deprecated, message: "use `destructive` with String instead of Text") + static func destructive(_ text: Text, action: @escaping () -> Void = {}) -> Self { + AlertButton(title: text.content ?? "", action: action) + .role(.destructive) + } +} diff --git a/UDF/View/AlertBuilder/AlertAction/AlertTextField.swift b/UDF/View/AlertBuilder/AlertAction/AlertTextField.swift new file mode 100644 index 00000000..40994f6a --- /dev/null +++ b/UDF/View/AlertBuilder/AlertAction/AlertTextField.swift @@ -0,0 +1,59 @@ + +import Foundation +import SwiftUI + +public struct AlertTextField: AlertAction { + public var title: String + public var text: Binding + public var textInputAutocapitalization: TextInputAutocapitalization? = nil + public var submitLabel: SubmitLabel = .done + + @StateObject private var debouncer: UserInputDebouncer + + public static func == (lhs: AlertTextField, rhs: AlertTextField) -> Bool { + lhs.title == rhs.title + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(title) + } + + public init(title: String, text: Binding) { + self.title = title + self.text = text + + self._debouncer = .init(wrappedValue: .init(defaultValue: text.wrappedValue)) + } + + public var body: some View { + TextField(title, text: $debouncer.value) + .textInputAutocapitalization(textInputAutocapitalization) + .submitLabel(submitLabel) + .onReceive(debouncer.$debouncedValue.dropFirst()) { value in + self.text.wrappedValue = value + } + .onChange(of: text.wrappedValue) { newValue in + if debouncer.value.isEmpty, !newValue.isEmpty { + debouncer.value = newValue + } + } + } +} + +extension AlertTextField: View {} + +// MARK: - Modifiers +public extension AlertTextField { + + func textInputAutocapitalization(_ textInputAutocapitalization: TextInputAutocapitalization?) -> AlertTextField { + mutate { field in + field.textInputAutocapitalization = textInputAutocapitalization + } + } + + func submitLabel(_ submitLabel: SubmitLabel) -> AlertTextField { + mutate { field in + field.submitLabel = submitLabel + } + } +} diff --git a/UDF/View/AlertBuilder/AlertActionsBuilder.swift b/UDF/View/AlertBuilder/AlertActionsBuilder.swift index 84daae47..df2c90e6 100644 --- a/UDF/View/AlertBuilder/AlertActionsBuilder.swift +++ b/UDF/View/AlertBuilder/AlertActionsBuilder.swift @@ -3,35 +3,35 @@ import Foundation @resultBuilder public enum AlertActionsBuilder { - public static func buildEither(first component: [AlertAction]) -> [AlertAction] { + public static func buildEither(first component: [any AlertAction]) -> [any AlertAction] { component } - public static func buildEither(second component: [AlertAction]) -> [AlertAction] { + public static func buildEither(second component: [any AlertAction]) -> [any AlertAction] { component } - public static func buildOptional(_ component: [AlertAction]?) -> [AlertAction] { + public static func buildOptional(_ component: [any AlertAction]?) -> [any AlertAction] { component ?? [] } - public static func buildExpression(_ expression: AlertAction) -> [AlertAction] { + public static func buildExpression(_ expression: some AlertAction) -> [any AlertAction] { [expression] } - public static func buildExpression(_ expression: ()) -> [AlertAction] { + public static func buildExpression(_ expression: ()) -> [any AlertAction] { [] } - static func buildLimitedAvailability(_ components: [AlertAction]) -> [AlertAction] { + static func buildLimitedAvailability(_ components: [any AlertAction]) -> [any AlertAction] { components } - public static func buildBlock(_ components: [AlertAction]...) -> [AlertAction] { + public static func buildBlock(_ components: [any AlertAction]...) -> [any AlertAction] { components.flatMap { $0 } } - public static func buildArray(_ components: [[AlertAction]]) -> [AlertAction] { + public static func buildArray(_ components: [[any AlertAction]]) -> [any AlertAction] { components.flatMap { $0 } } } diff --git a/UDF/View/AlertBuilder/AlertBuilder.swift b/UDF/View/AlertBuilder/AlertBuilder.swift index d170885b..a07ffb4a 100644 --- a/UDF/View/AlertBuilder/AlertBuilder.swift +++ b/UDF/View/AlertBuilder/AlertBuilder.swift @@ -99,13 +99,13 @@ public enum AlertBuilder { case message(text: () -> String) case messageTitle(title: () -> String, message: () -> String) - @available(*, deprecated, message: "use custom(title:text:actions) case instead") - case custom(title: () -> String, text: () -> String, primaryButton: AlertAction, secondaryButton: AlertAction) + @available(*, deprecated, message: "use customActions(title:text:actions) case instead") + case custom(title: () -> String, text: () -> String, primaryButton: AlertButton, secondaryButton: AlertButton) - @available(*, deprecated, message: "use custom(title:text:actions) case instead") - case customDismiss(title: () -> String, text: () -> String, dismissButton: AlertAction) + @available(*, deprecated, message: "use customActions(title:text:actions) case instead") + case customDismiss(title: () -> String, text: () -> String, dismissButton: AlertButton) - case customActions(title: () -> String, text: () -> String, actions: () -> [AlertAction]) + case customActions(title: () -> String, text: () -> String, actions: () -> [any AlertAction]) } public init(validationError text: String) { @@ -154,32 +154,32 @@ public enum AlertBuilder { } @available(*, deprecated, message: "use init(title:text:actions) instead") - public init(title: String, text: String, primaryButton: AlertAction, secondaryButton: AlertAction) { + public init(title: String, text: String, primaryButton: AlertButton, secondaryButton: AlertButton) { self.init(title: { title }, text: { text }, primaryButton: primaryButton, secondaryButton: secondaryButton) } @available(*, deprecated, message: "use init(title:text:actions) instead") - public init(title: @escaping () -> String, text: @escaping () -> String, primaryButton: AlertAction, secondaryButton: AlertAction) { + public init(title: @escaping () -> String, text: @escaping () -> String, primaryButton: AlertButton, secondaryButton: AlertButton) { id = UUID() type = .custom(title: title, text: text, primaryButton: primaryButton, secondaryButton: secondaryButton) } @available(*, deprecated, message: "use init(title:text:actions) instead") - public init(title: String, text: String, dismissButton: AlertAction) { + public init(title: String, text: String, dismissButton: AlertButton) { self.init(title: { title }, text: { text }, dismissButton: dismissButton) } @available(*, deprecated, message: "use init(title:text:actions) instead") - public init(title: @escaping () -> String, text: @escaping () -> String, dismissButton: AlertAction) { + public init(title: @escaping () -> String, text: @escaping () -> String, dismissButton: AlertButton) { id = UUID() type = .customDismiss(title: title, text: text, dismissButton: dismissButton) } - public init(title: String, text: String, @AlertActionsBuilder actions: @escaping () -> [AlertAction]) { + public init(title: String, text: String, @AlertActionsBuilder actions: @escaping () -> [any AlertAction]) { self.init(title: { title }, text: { text }, actions: actions) } - public init(title: @escaping () -> String, text: @escaping () -> String, @AlertActionsBuilder actions: @escaping () -> [AlertAction]) { + public init(title: @escaping () -> String, text: @escaping () -> String, @AlertActionsBuilder actions: @escaping () -> [any AlertAction]) { id = UUID() type = .customActions(title: title, text: text, actions: actions) } diff --git a/UDF/View/Router/GlobalRouter/GlobalRouter.swift b/UDF/View/Router/GlobalRouter/GlobalRouter.swift new file mode 100644 index 00000000..a74c2474 --- /dev/null +++ b/UDF/View/Router/GlobalRouter/GlobalRouter.swift @@ -0,0 +1,71 @@ + +import Foundation +import SwiftUI + +public final class GlobalRouter { + private var routingPath: Binding + private var routers: [Weak] = [] + + public init(path: Binding) { + self.routingPath = path + } + + func add(router: Router) { + routers.reap() + routers.append(.init(value: router)) + } + + public func navigate(to route: R.Route, with router: Router) where R.Route: Hashable { + let registeredRoute = routers.first { obj in + guard let value = obj.value else { + return false + } + + return ObjectIdentifier(value) == ObjectIdentifier(router) + } + + guard registeredRoute != nil else { + fatalError("Router: \(router) is not attached to the view hierarchy. Use `navigationDestination(router:)` to add router") + } + + routers.reap() + routingPath.wrappedValue.append(route) + } + + public func backToRoot() { + guard !routingPath.wrappedValue.isEmpty else { + return + } + routingPath.wrappedValue.removeLast(routingPath.wrappedValue.count) + } + + public func back() { + guard !routingPath.wrappedValue.isEmpty else { + return + } + routingPath.wrappedValue.removeLast() + } + + public func back(stepsCount: Int) { + guard !routingPath.wrappedValue.isEmpty else { + return + } + routingPath.wrappedValue.removeLast(stepsCount) + } + + public func resetStack(to route: R.Route, with router: Router) where R.Route: Hashable { + routingPath.wrappedValue.removeLast(routingPath.wrappedValue.count) + navigate(to: route, with: router) + } +} + +private struct GlobalRouterKey: EnvironmentKey { + static var defaultValue: GlobalRouter = GlobalRouter(path: .constant(NavigationPath())) +} + +public extension EnvironmentValues { + var globalRouter: GlobalRouter { + get { self[GlobalRouterKey.self] } + set { self[GlobalRouterKey.self] = newValue } + } +} diff --git a/UDF/View/Router/GlobalRouter/GlobalRoutingModifier.swift b/UDF/View/Router/GlobalRouter/GlobalRoutingModifier.swift new file mode 100644 index 00000000..5ddbe6b7 --- /dev/null +++ b/UDF/View/Router/GlobalRouter/GlobalRoutingModifier.swift @@ -0,0 +1,22 @@ + +import Foundation +import SwiftUI + +struct GlobalRoutingModifier: ViewModifier where R.Route: Hashable { + @Environment(\.globalRouter) var globalRouter + + var router: Router + + init(router: Router) { + self.router = router + } + + func body(content: Content) -> some View { + let _ = self.globalRouter.add(router: router) + content + .navigationDestination( + for: R.Route.self, + destination: router.view(for:) + ) + } +}