diff --git a/ComposableArchitecture.xcworkspace/contents.xcworkspacedata b/ComposableArchitecture.xcworkspace/contents.xcworkspacedata
index 5cc16120ad09..c94a9453f398 100644
--- a/ComposableArchitecture.xcworkspace/contents.xcworkspacedata
+++ b/ComposableArchitecture.xcworkspace/contents.xcworkspacedata
@@ -7,6 +7,9 @@
+
+
diff --git a/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved
index e6d6ed0296a2..6fa49342688f 100644
--- a/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -1,106 +1,122 @@
{
- "object": {
- "pins": [
- {
- "package": "combine-schedulers",
- "repositoryURL": "https://github.com/pointfreeco/combine-schedulers",
- "state": {
- "branch": null,
- "revision": "882ac01eb7ef9e36d4467eb4b1151e74fcef85ab",
- "version": "0.9.1"
- }
- },
- {
- "package": "swift-argument-parser",
- "repositoryURL": "https://github.com/apple/swift-argument-parser",
- "state": {
- "branch": null,
- "revision": "9f39744e025c7d377987f30b03770805dcb0bcd1",
- "version": "1.1.4"
- }
- },
- {
- "package": "Benchmark",
- "repositoryURL": "https://github.com/google/swift-benchmark",
- "state": {
- "branch": null,
- "revision": "8163295f6fe82356b0bcf8e1ab991645de17d096",
- "version": "0.1.2"
- }
- },
- {
- "package": "swift-case-paths",
- "repositoryURL": "https://github.com/pointfreeco/swift-case-paths",
- "state": {
- "branch": null,
- "revision": "bb436421f57269fbcfe7360735985321585a86e5",
- "version": "0.10.1"
- }
- },
- {
- "package": "swift-clocks",
- "repositoryURL": "https://github.com/pointfreeco/swift-clocks",
- "state": {
- "branch": null,
- "revision": "692ec4f5429a667bdd968c7260dfa2b23adfeffc",
- "version": "0.1.4"
- }
- },
- {
- "package": "swift-collections",
- "repositoryURL": "https://github.com/apple/swift-collections",
- "state": {
- "branch": null,
- "revision": "f504716c27d2e5d4144fa4794b12129301d17729",
- "version": "1.0.3"
- }
- },
- {
- "package": "swift-custom-dump",
- "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump",
- "state": {
- "branch": null,
- "revision": "819d9d370cd721c9d87671e29d947279292e4541",
- "version": "0.6.0"
- }
- },
- {
- "package": "SwiftDocCPlugin",
- "repositoryURL": "https://github.com/apple/swift-docc-plugin",
- "state": {
- "branch": null,
- "revision": "3303b164430d9a7055ba484c8ead67a52f7b74f6",
- "version": "1.0.0"
- }
- },
- {
- "package": "swift-identified-collections",
- "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections",
- "state": {
- "branch": null,
- "revision": "bfb0d43e75a15b6dfac770bf33479e8393884a36",
- "version": "0.4.1"
- }
- },
- {
- "package": "swiftui-navigation",
- "repositoryURL": "https://github.com/pointfreeco/swiftui-navigation",
- "state": {
- "branch": null,
- "revision": "46acf5ecc1cabdb28d7fe03289f6c8b13a023f52",
- "version": "0.4.5"
- }
- },
- {
- "package": "xctest-dynamic-overlay",
- "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay",
- "state": {
- "branch": null,
- "revision": "16e6409ee82e1b81390bdffbf217b9c08ab32784",
- "version": "0.5.0"
- }
+ "pins" : [
+ {
+ "identity" : "combine-schedulers",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/combine-schedulers",
+ "state" : {
+ "revision" : "882ac01eb7ef9e36d4467eb4b1151e74fcef85ab",
+ "version" : "0.9.1"
}
- ]
- },
- "version": 1
+ },
+ {
+ "identity" : "swift-argument-parser",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-argument-parser",
+ "state" : {
+ "revision" : "4ad606ba5d7673ea60679a61ff867cc1ff8c8e86",
+ "version" : "1.2.1"
+ }
+ },
+ {
+ "identity" : "swift-benchmark",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/swift-benchmark",
+ "state" : {
+ "revision" : "8163295f6fe82356b0bcf8e1ab991645de17d096",
+ "version" : "0.1.2"
+ }
+ },
+ {
+ "identity" : "swift-case-paths",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-case-paths",
+ "state" : {
+ "revision" : "c3a42e8d1a76ff557cf565ed6d8b0aee0e6e75af",
+ "version" : "0.11.0"
+ }
+ },
+ {
+ "identity" : "swift-clocks",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-clocks",
+ "state" : {
+ "revision" : "20b25ca0dd88ebfb9111ec937814ddc5a8880172",
+ "version" : "0.2.0"
+ }
+ },
+ {
+ "identity" : "swift-collections",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-collections",
+ "state" : {
+ "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
+ "version" : "1.0.4"
+ }
+ },
+ {
+ "identity" : "swift-custom-dump",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-custom-dump",
+ "state" : {
+ "revision" : "ead7d30cc224c3642c150b546f4f1080d1c411a8",
+ "version" : "0.6.1"
+ }
+ },
+ {
+ "identity" : "swift-dependencies",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-dependencies",
+ "state" : {
+ "revision" : "8282b0c59662eb38946afe30eb403663fc2ecf76",
+ "version" : "0.1.4"
+ }
+ },
+ {
+ "identity" : "swift-docc-plugin",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-docc-plugin",
+ "state" : {
+ "revision" : "10bc670db657d11bdd561e07de30a9041311b2b1",
+ "version" : "1.1.0"
+ }
+ },
+ {
+ "identity" : "swift-docc-symbolkit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-docc-symbolkit",
+ "state" : {
+ "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34",
+ "version" : "1.0.0"
+ }
+ },
+ {
+ "identity" : "swift-identified-collections",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-identified-collections",
+ "state" : {
+ "revision" : "fd34c544ad27f3ba6b19142b348005bfa85b6005",
+ "version" : "0.6.0"
+ }
+ },
+ {
+ "identity" : "swiftui-navigation",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swiftui-navigation",
+ "state" : {
+ "revision" : "bf0fb9d53019cbde1a1e0cf290b560a0a0411282",
+ "version" : "0.6.0"
+ }
+ },
+ {
+ "identity" : "xctest-dynamic-overlay",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
+ "state" : {
+ "revision" : "16b23a295fa322eb957af98037f86791449de60f",
+ "version" : "0.8.1"
+ }
+ }
+ ],
+ "version" : 2
}
diff --git a/ComposableArchitecture.xcworkspace/xcshareddata/xcschemes/ComposableArchitecture.xcscheme b/ComposableArchitecture.xcworkspace/xcshareddata/xcschemes/ComposableArchitecture.xcscheme
index ed07195d3bd1..b5e120b94df2 100644
--- a/ComposableArchitecture.xcworkspace/xcshareddata/xcschemes/ComposableArchitecture.xcscheme
+++ b/ComposableArchitecture.xcworkspace/xcshareddata/xcschemes/ComposableArchitecture.xcscheme
@@ -39,16 +39,6 @@
ReferencedContainer = "container:">
-
-
-
-
.streamWithContinuation()
let messages = AsyncStream>.streamWithContinuation()
- store.dependencies.continuousClock = ImmediateClock()
- store.dependencies.webSocket.open = { _, _, _ in actions.stream }
- store.dependencies.webSocket.receive = { _ in messages.stream }
- store.dependencies.webSocket.send = { _, _ in }
- store.dependencies.webSocket.sendPing = { _ in try await Task.never() }
+ let store = TestStore(
+ initialState: WebSocket.State(),
+ reducer: WebSocket()
+ ) {
+ $0.continuousClock = ImmediateClock()
+ $0.webSocket.open = { _, _, _ in actions.stream }
+ $0.webSocket.receive = { _ in messages.stream }
+ $0.webSocket.send = { _, _ in }
+ $0.webSocket.sendPing = { _ in try await Task.never() }
+ }
// Connect to the socket
await store.send(.connectButtonTapped) {
@@ -58,22 +58,22 @@ final class WebSocketTests: XCTestCase {
}
func testWebSocketSendFailure() async {
- let store = TestStore(
- initialState: WebSocket.State(),
- reducer: WebSocket()
- )
-
let actions = AsyncStream.streamWithContinuation()
let messages = AsyncStream>.streamWithContinuation()
- store.dependencies.continuousClock = ImmediateClock()
- store.dependencies.webSocket.open = { _, _, _ in actions.stream }
- store.dependencies.webSocket.receive = { _ in messages.stream }
- store.dependencies.webSocket.send = { _, _ in
- struct SendFailure: Error, Equatable {}
- throw SendFailure()
+ let store = TestStore(
+ initialState: WebSocket.State(),
+ reducer: WebSocket()
+ ) {
+ $0.continuousClock = ImmediateClock()
+ $0.webSocket.open = { _, _, _ in actions.stream }
+ $0.webSocket.receive = { _ in messages.stream }
+ $0.webSocket.send = { _, _ in
+ struct SendFailure: Error, Equatable {}
+ throw SendFailure()
+ }
+ $0.webSocket.sendPing = { _ in try await Task.never() }
}
- store.dependencies.webSocket.sendPing = { _ in try await Task.never() }
// Connect to the socket
await store.send(.connectButtonTapped) {
@@ -105,19 +105,19 @@ final class WebSocketTests: XCTestCase {
}
func testWebSocketPings() async {
- let store = TestStore(
- initialState: WebSocket.State(),
- reducer: WebSocket()
- )
-
let actions = AsyncStream.streamWithContinuation()
let clock = TestClock()
var pingsCount = 0
- store.dependencies.continuousClock = clock
- store.dependencies.webSocket.open = { _, _, _ in actions.stream }
- store.dependencies.webSocket.receive = { _ in try await Task.never() }
- store.dependencies.webSocket.sendPing = { @MainActor _ in pingsCount += 1 }
+ let store = TestStore(
+ initialState: WebSocket.State(),
+ reducer: WebSocket()
+ ) {
+ $0.continuousClock = clock
+ $0.webSocket.open = { _, _, _ in actions.stream }
+ $0.webSocket.receive = { _ in try await Task.never() }
+ $0.webSocket.sendPing = { @MainActor _ in pingsCount += 1 }
+ }
// Connect to the socket
await store.send(.connectButtonTapped) {
@@ -140,17 +140,17 @@ final class WebSocketTests: XCTestCase {
}
func testWebSocketConnectError() async {
+ let actions = AsyncStream.streamWithContinuation()
+
let store = TestStore(
initialState: WebSocket.State(),
reducer: WebSocket()
- )
-
- let actions = AsyncStream.streamWithContinuation()
-
- store.dependencies.continuousClock = ImmediateClock()
- store.dependencies.webSocket.open = { _, _, _ in actions.stream }
- store.dependencies.webSocket.receive = { _ in try await Task.never() }
- store.dependencies.webSocket.sendPing = { _ in try await Task.never() }
+ ) {
+ $0.continuousClock = ImmediateClock()
+ $0.webSocket.open = { _, _, _ in actions.stream }
+ $0.webSocket.receive = { _ in try await Task.never() }
+ $0.webSocket.sendPing = { _ in try await Task.never() }
+ }
// Attempt to connect to the socket
await store.send(.connectButtonTapped) {
diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-LifecycleTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-LifecycleTests.swift
index 32a3311a32e1..0b006cf1e967 100644
--- a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-LifecycleTests.swift
+++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-LifecycleTests.swift
@@ -6,13 +6,14 @@ import XCTest
@MainActor
final class LifecycleTests: XCTestCase {
func testLifecycle() async {
+ let clock = TestClock()
+
let store = TestStore(
initialState: LifecycleDemo.State(),
reducer: LifecycleDemo()
- )
-
- let clock = TestClock()
- store.dependencies.continuousClock = clock
+ ) {
+ $0.continuousClock = clock
+ }
await store.send(.toggleTimerButtonTapped) {
$0.count = 0
diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-RecursionTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-RecursionTests.swift
index e59b62801118..d61a2bf8a174 100644
--- a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-RecursionTests.swift
+++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-RecursionTests.swift
@@ -9,9 +9,9 @@ final class RecursionTests: XCTestCase {
let store = TestStore(
initialState: Nested.State(id: UUID()),
reducer: Nested()
- )
-
- store.dependencies.uuid = .incrementing
+ ) {
+ $0.uuid = .incrementing
+ }
let id0 = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
let id1 = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!
diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift
index a021c125ffac..01073b06e7d4 100644
--- a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift
+++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift
@@ -15,9 +15,9 @@ final class ReusableComponentsDownloadComponentTests: XCTestCase {
url: URL(string: "https://www.pointfree.co")!
),
reducer: DownloadComponent()
- )
-
- store.dependencies.downloadClient.download = { _ in self.download.stream }
+ ) {
+ $0.downloadClient.download = { _ in self.download.stream }
+ }
await store.send(.buttonTapped) {
$0.mode = .startingToDownload
@@ -43,9 +43,9 @@ final class ReusableComponentsDownloadComponentTests: XCTestCase {
url: URL(string: "https://www.pointfree.co")!
),
reducer: DownloadComponent()
- )
-
- store.dependencies.downloadClient.download = { _ in self.download.stream }
+ ) {
+ $0.downloadClient.download = { _ in self.download.stream }
+ }
await store.send(.buttonTapped) {
$0.mode = .startingToDownload
@@ -83,9 +83,9 @@ final class ReusableComponentsDownloadComponentTests: XCTestCase {
url: URL(string: "https://www.pointfree.co")!
),
reducer: DownloadComponent()
- )
-
- store.dependencies.downloadClient.download = { _ in self.download.stream }
+ ) {
+ $0.downloadClient.download = { _ in self.download.stream }
+ }
let task = await store.send(.buttonTapped) {
$0.mode = .startingToDownload
@@ -122,9 +122,9 @@ final class ReusableComponentsDownloadComponentTests: XCTestCase {
url: URL(string: "https://www.pointfree.co")!
),
reducer: DownloadComponent()
- )
-
- store.dependencies.downloadClient.download = { _ in self.download.stream }
+ ) {
+ $0.downloadClient.download = { _ in self.download.stream }
+ }
await store.send(.buttonTapped) {
$0.alert = AlertState {
diff --git a/Examples/CaseStudies/UIKitCaseStudies/NavigateAndLoad.swift b/Examples/CaseStudies/UIKitCaseStudies/NavigateAndLoad.swift
index 102a51e487fa..6e67fb40ef43 100644
--- a/Examples/CaseStudies/UIKitCaseStudies/NavigateAndLoad.swift
+++ b/Examples/CaseStudies/UIKitCaseStudies/NavigateAndLoad.swift
@@ -42,6 +42,9 @@ struct EagerNavigation: ReducerProtocol {
return .none
}
}
+ .ifLet(\.optionalCounter, action: /Action.optionalCounter) {
+ Counter()
+ }
}
}
diff --git a/Examples/CaseStudies/tvOSCaseStudiesTests/FocusTests.swift b/Examples/CaseStudies/tvOSCaseStudiesTests/FocusTests.swift
index 44056e331245..f104a91fe776 100644
--- a/Examples/CaseStudies/tvOSCaseStudiesTests/FocusTests.swift
+++ b/Examples/CaseStudies/tvOSCaseStudiesTests/FocusTests.swift
@@ -9,9 +9,9 @@ final class tvOSCaseStudiesTests: XCTestCase {
let store = TestStore(
initialState: Focus.State(currentFocus: 1),
reducer: Focus()
- )
-
- store.dependencies.withRandomNumberGenerator = .init(LCRNG())
+ ) {
+ $0.withRandomNumberGenerator = .init(LCRNG())
+ }
await store.send(.randomButtonClicked)
await store.send(.randomButtonClicked) {
diff --git a/Examples/Integration/Integration.xcodeproj/project.pbxproj b/Examples/Integration/Integration.xcodeproj/project.pbxproj
new file mode 100644
index 000000000000..b392af8b7513
--- /dev/null
+++ b/Examples/Integration/Integration.xcodeproj/project.pbxproj
@@ -0,0 +1,512 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ CA595273296DF46D00B5B695 /* NavigationStackBindingTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA595272296DF46D00B5B695 /* NavigationStackBindingTestCase.swift */; };
+ CA595275296DF55A00B5B695 /* NavigationStackBindingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA595274296DF55A00B5B695 /* NavigationStackBindingTests.swift */; };
+ CA595278296DF67E00B5B695 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = CA595277296DF67E00B5B695 /* ComposableArchitecture */; };
+ CAA1CAF5296DEE78000665B1 /* IntegrationApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CAF4296DEE78000665B1 /* IntegrationApp.swift */; };
+ CAA1CAF9296DEE79000665B1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAA1CAF8296DEE79000665B1 /* Assets.xcassets */; };
+ CAA1CAFC296DEE79000665B1 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAA1CAFB296DEE79000665B1 /* Preview Assets.xcassets */; };
+ CAA1CB10296DEE79000665B1 /* ForEachBindingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB0F296DEE79000665B1 /* ForEachBindingTests.swift */; };
+ CAA1CB1F296DEEAC000665B1 /* ForEachBindingTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB1E296DEEAC000665B1 /* ForEachBindingTestCase.swift */; };
+ E9919D3E296E28C800C8716B /* EscapedWithViewStoreTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9919D3D296E28C800C8716B /* EscapedWithViewStoreTestCase.swift */; };
+ E9919D40296E3EF400C8716B /* EscapedWithViewStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9919D3F296E3EF400C8716B /* EscapedWithViewStoreTests.swift */; };
+ E9919D42296E47A400C8716B /* BindingsAnimationsTestBench.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9919D41296E47A400C8716B /* BindingsAnimationsTestBench.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ CAA1CB0C296DEE79000665B1 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = CAA1CAE9296DEE78000665B1 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = CAA1CAF0296DEE78000665B1;
+ remoteInfo = Integration;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+ CA595272296DF46D00B5B695 /* NavigationStackBindingTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStackBindingTestCase.swift; sourceTree = ""; };
+ CA595274296DF55A00B5B695 /* NavigationStackBindingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStackBindingTests.swift; sourceTree = ""; };
+ CA595276296DF66B00B5B695 /* swift-composable-architecture */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "swift-composable-architecture"; path = ../..; sourceTree = ""; };
+ CAA1CAF1296DEE78000665B1 /* Integration.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Integration.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ CAA1CAF4296DEE78000665B1 /* IntegrationApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationApp.swift; sourceTree = ""; };
+ CAA1CAF8296DEE79000665B1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ CAA1CAFB296DEE79000665B1 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
+ CAA1CB0B296DEE79000665B1 /* IntegrationUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ CAA1CB0F296DEE79000665B1 /* ForEachBindingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForEachBindingTests.swift; sourceTree = ""; };
+ CAA1CB1E296DEEAC000665B1 /* ForEachBindingTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForEachBindingTestCase.swift; sourceTree = ""; };
+ E9919D3D296E28C800C8716B /* EscapedWithViewStoreTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EscapedWithViewStoreTestCase.swift; sourceTree = ""; };
+ E9919D3F296E3EF400C8716B /* EscapedWithViewStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EscapedWithViewStoreTests.swift; sourceTree = ""; };
+ E9919D41296E47A400C8716B /* BindingsAnimationsTestBench.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindingsAnimationsTestBench.swift; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ CAA1CAEE296DEE78000665B1 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ CA595278296DF67E00B5B695 /* ComposableArchitecture in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ CAA1CB08296DEE79000665B1 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ CAA1CAE8296DEE78000665B1 = {
+ isa = PBXGroup;
+ children = (
+ CA595276296DF66B00B5B695 /* swift-composable-architecture */,
+ CAA1CAF3296DEE78000665B1 /* Integration */,
+ CAA1CB0E296DEE79000665B1 /* IntegrationUITests */,
+ CAA1CAF2296DEE78000665B1 /* Products */,
+ CAA1CB20296DEEE2000665B1 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ CAA1CAF2296DEE78000665B1 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ CAA1CAF1296DEE78000665B1 /* Integration.app */,
+ CAA1CB0B296DEE79000665B1 /* IntegrationUITests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ CAA1CAF3296DEE78000665B1 /* Integration */ = {
+ isa = PBXGroup;
+ children = (
+ E9919D41296E47A400C8716B /* BindingsAnimationsTestBench.swift */,
+ E9919D3D296E28C800C8716B /* EscapedWithViewStoreTestCase.swift */,
+ CAA1CB1E296DEEAC000665B1 /* ForEachBindingTestCase.swift */,
+ CA595272296DF46D00B5B695 /* NavigationStackBindingTestCase.swift */,
+ CAA1CAF4296DEE78000665B1 /* IntegrationApp.swift */,
+ CAA1CAF8296DEE79000665B1 /* Assets.xcassets */,
+ CAA1CAFA296DEE79000665B1 /* Preview Content */,
+ );
+ path = Integration;
+ sourceTree = "";
+ };
+ CAA1CAFA296DEE79000665B1 /* Preview Content */ = {
+ isa = PBXGroup;
+ children = (
+ CAA1CAFB296DEE79000665B1 /* Preview Assets.xcassets */,
+ );
+ path = "Preview Content";
+ sourceTree = "";
+ };
+ CAA1CB0E296DEE79000665B1 /* IntegrationUITests */ = {
+ isa = PBXGroup;
+ children = (
+ E9919D3F296E3EF400C8716B /* EscapedWithViewStoreTests.swift */,
+ CAA1CB0F296DEE79000665B1 /* ForEachBindingTests.swift */,
+ CA595274296DF55A00B5B695 /* NavigationStackBindingTests.swift */,
+ );
+ path = IntegrationUITests;
+ sourceTree = "";
+ };
+ CAA1CB20296DEEE2000665B1 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ CAA1CAF0296DEE78000665B1 /* Integration */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = CAA1CB15296DEE79000665B1 /* Build configuration list for PBXNativeTarget "Integration" */;
+ buildPhases = (
+ CAA1CAED296DEE78000665B1 /* Sources */,
+ CAA1CAEE296DEE78000665B1 /* Frameworks */,
+ CAA1CAEF296DEE78000665B1 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ CA59527A296DF6D000B5B695 /* PBXTargetDependency */,
+ );
+ name = Integration;
+ packageProductDependencies = (
+ CA595277296DF67E00B5B695 /* ComposableArchitecture */,
+ );
+ productName = Integration;
+ productReference = CAA1CAF1296DEE78000665B1 /* Integration.app */;
+ productType = "com.apple.product-type.application";
+ };
+ CAA1CB0A296DEE79000665B1 /* IntegrationUITests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = CAA1CB1B296DEE79000665B1 /* Build configuration list for PBXNativeTarget "IntegrationUITests" */;
+ buildPhases = (
+ CAA1CB07296DEE79000665B1 /* Sources */,
+ CAA1CB08296DEE79000665B1 /* Frameworks */,
+ CAA1CB09296DEE79000665B1 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ CAA1CB0D296DEE79000665B1 /* PBXTargetDependency */,
+ );
+ name = IntegrationUITests;
+ productName = IntegrationUITests;
+ productReference = CAA1CB0B296DEE79000665B1 /* IntegrationUITests.xctest */;
+ productType = "com.apple.product-type.bundle.ui-testing";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ CAA1CAE9296DEE78000665B1 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1420;
+ LastUpgradeCheck = 1420;
+ TargetAttributes = {
+ CAA1CAF0296DEE78000665B1 = {
+ CreatedOnToolsVersion = 14.2;
+ };
+ CAA1CB0A296DEE79000665B1 = {
+ CreatedOnToolsVersion = 14.2;
+ TestTargetID = CAA1CAF0296DEE78000665B1;
+ };
+ };
+ };
+ buildConfigurationList = CAA1CAEC296DEE78000665B1 /* Build configuration list for PBXProject "Integration" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = CAA1CAE8296DEE78000665B1;
+ productRefGroup = CAA1CAF2296DEE78000665B1 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ CAA1CAF0296DEE78000665B1 /* Integration */,
+ CAA1CB0A296DEE79000665B1 /* IntegrationUITests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ CAA1CAEF296DEE78000665B1 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ CAA1CAFC296DEE79000665B1 /* Preview Assets.xcassets in Resources */,
+ CAA1CAF9296DEE79000665B1 /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ CAA1CB09296DEE79000665B1 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ CAA1CAED296DEE78000665B1 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ CAA1CB1F296DEEAC000665B1 /* ForEachBindingTestCase.swift in Sources */,
+ E9919D42296E47A400C8716B /* BindingsAnimationsTestBench.swift in Sources */,
+ CA595273296DF46D00B5B695 /* NavigationStackBindingTestCase.swift in Sources */,
+ E9919D3E296E28C800C8716B /* EscapedWithViewStoreTestCase.swift in Sources */,
+ CAA1CAF5296DEE78000665B1 /* IntegrationApp.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ CAA1CB07296DEE79000665B1 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E9919D40296E3EF400C8716B /* EscapedWithViewStoreTests.swift in Sources */,
+ CA595275296DF55A00B5B695 /* NavigationStackBindingTests.swift in Sources */,
+ CAA1CB10296DEE79000665B1 /* ForEachBindingTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ CA59527A296DF6D000B5B695 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ productRef = CA595279296DF6D000B5B695 /* ComposableArchitecture */;
+ };
+ CAA1CB0D296DEE79000665B1 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = CAA1CAF0296DEE78000665B1 /* Integration */;
+ targetProxy = CAA1CB0C296DEE79000665B1 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ CAA1CB13296DEE79000665B1 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ CAA1CB14296DEE79000665B1 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ CAA1CB16296DEE79000665B1 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"Integration/Preview Content\"";
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.Integration;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ CAA1CB17296DEE79000665B1 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"Integration/Preview Content\"";
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.Integration;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ CAA1CB1C296DEE79000665B1 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.IntegrationUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_TARGET_NAME = Integration;
+ };
+ name = Debug;
+ };
+ CAA1CB1D296DEE79000665B1 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.IntegrationUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_TARGET_NAME = Integration;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ CAA1CAEC296DEE78000665B1 /* Build configuration list for PBXProject "Integration" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ CAA1CB13296DEE79000665B1 /* Debug */,
+ CAA1CB14296DEE79000665B1 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ CAA1CB15296DEE79000665B1 /* Build configuration list for PBXNativeTarget "Integration" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ CAA1CB16296DEE79000665B1 /* Debug */,
+ CAA1CB17296DEE79000665B1 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ CAA1CB1B296DEE79000665B1 /* Build configuration list for PBXNativeTarget "IntegrationUITests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ CAA1CB1C296DEE79000665B1 /* Debug */,
+ CAA1CB1D296DEE79000665B1 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ CA595277296DF67E00B5B695 /* ComposableArchitecture */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = ComposableArchitecture;
+ };
+ CA595279296DF6D000B5B695 /* ComposableArchitecture */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = ComposableArchitecture;
+ };
+/* End XCSwiftPackageProductDependency section */
+ };
+ rootObject = CAA1CAE9296DEE78000665B1 /* Project object */;
+}
diff --git a/Examples/Integration/Integration.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Examples/Integration/Integration.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 000000000000..919434a6254f
--- /dev/null
+++ b/Examples/Integration/Integration.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Examples/Integration/Integration.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Examples/Integration/Integration.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 000000000000..18d981003d68
--- /dev/null
+++ b/Examples/Integration/Integration.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/Examples/Integration/Integration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Integration/Integration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 000000000000..8b39bacc9fdb
--- /dev/null
+++ b/Examples/Integration/Integration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,113 @@
+{
+ "pins" : [
+ {
+ "identity" : "combine-schedulers",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/combine-schedulers",
+ "state" : {
+ "revision" : "882ac01eb7ef9e36d4467eb4b1151e74fcef85ab",
+ "version" : "0.9.1"
+ }
+ },
+ {
+ "identity" : "swift-argument-parser",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-argument-parser",
+ "state" : {
+ "revision" : "fddd1c00396eed152c45a46bea9f47b98e59301d",
+ "version" : "1.2.0"
+ }
+ },
+ {
+ "identity" : "swift-benchmark",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/swift-benchmark",
+ "state" : {
+ "revision" : "8163295f6fe82356b0bcf8e1ab991645de17d096",
+ "version" : "0.1.2"
+ }
+ },
+ {
+ "identity" : "swift-case-paths",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-case-paths",
+ "state" : {
+ "revision" : "c3a42e8d1a76ff557cf565ed6d8b0aee0e6e75af",
+ "version" : "0.11.0"
+ }
+ },
+ {
+ "identity" : "swift-clocks",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-clocks",
+ "state" : {
+ "revision" : "20b25ca0dd88ebfb9111ec937814ddc5a8880172",
+ "version" : "0.2.0"
+ }
+ },
+ {
+ "identity" : "swift-collections",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-collections",
+ "state" : {
+ "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
+ "version" : "1.0.4"
+ }
+ },
+ {
+ "identity" : "swift-custom-dump",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-custom-dump",
+ "state" : {
+ "revision" : "ead7d30cc224c3642c150b546f4f1080d1c411a8",
+ "version" : "0.6.1"
+ }
+ },
+ {
+ "identity" : "swift-dependencies",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-dependencies",
+ "state" : {
+ "revision" : "e9e82b5302025092ab8358e794f89a0f0397dd9d",
+ "version" : "0.1.2"
+ }
+ },
+ {
+ "identity" : "swift-docc-plugin",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-docc-plugin",
+ "state" : {
+ "revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6",
+ "version" : "1.0.0"
+ }
+ },
+ {
+ "identity" : "swift-identified-collections",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-identified-collections",
+ "state" : {
+ "revision" : "fd34c544ad27f3ba6b19142b348005bfa85b6005",
+ "version" : "0.6.0"
+ }
+ },
+ {
+ "identity" : "swiftui-navigation",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swiftui-navigation",
+ "state" : {
+ "revision" : "ddc01cdcddfd30ef7a966049b2e1d251e224ad93",
+ "version" : "0.5.0"
+ }
+ },
+ {
+ "identity" : "xctest-dynamic-overlay",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
+ "state" : {
+ "revision" : "a9daebf0bf65981fd159c885d504481a65a75f02",
+ "version" : "0.8.0"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/ComposableArchitecture.xcworkspace/xcshareddata/xcschemes/Dependencies.xcscheme b/Examples/Integration/Integration.xcodeproj/xcshareddata/xcschemes/Integration.xcscheme
similarity index 62%
rename from ComposableArchitecture.xcworkspace/xcshareddata/xcschemes/Dependencies.xcscheme
rename to Examples/Integration/Integration.xcodeproj/xcshareddata/xcschemes/Integration.xcscheme
index 880e138ada4c..3ea457f5a629 100644
--- a/ComposableArchitecture.xcworkspace/xcshareddata/xcschemes/Dependencies.xcscheme
+++ b/Examples/Integration/Integration.xcodeproj/xcshareddata/xcschemes/Integration.xcscheme
@@ -1,6 +1,6 @@
+ BlueprintIdentifier = "CAA1CAF0296DEE78000665B1"
+ BuildableName = "Integration.app"
+ BlueprintName = "Integration"
+ ReferencedContainer = "container:Integration.xcodeproj">
@@ -32,10 +32,10 @@
skipped = "NO">
+ BlueprintIdentifier = "CAA1CB0A296DEE79000665B1"
+ BuildableName = "IntegrationUITests.xctest"
+ BlueprintName = "IntegrationUITests"
+ ReferencedContainer = "container:Integration.xcodeproj">
@@ -50,6 +50,16 @@
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
+
+
+
+
-
+
+ BlueprintIdentifier = "CAA1CAF0296DEE78000665B1"
+ BuildableName = "Integration.app"
+ BlueprintName = "Integration"
+ ReferencedContainer = "container:Integration.xcodeproj">
-
+
diff --git a/Examples/Integration/Integration/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/Integration/Integration/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 000000000000..eb8789700816
--- /dev/null
+++ b/Examples/Integration/Integration/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Examples/Integration/Integration/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/Integration/Integration/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 000000000000..13613e3ee1a9
--- /dev/null
+++ b/Examples/Integration/Integration/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,13 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Examples/Integration/Integration/Assets.xcassets/Contents.json b/Examples/Integration/Integration/Assets.xcassets/Contents.json
new file mode 100644
index 000000000000..73c00596a7fc
--- /dev/null
+++ b/Examples/Integration/Integration/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Examples/Integration/Integration/BindingsAnimationsTestBench.swift b/Examples/Integration/Integration/BindingsAnimationsTestBench.swift
new file mode 100644
index 000000000000..2ab2270229c2
--- /dev/null
+++ b/Examples/Integration/Integration/BindingsAnimationsTestBench.swift
@@ -0,0 +1,194 @@
+import ComposableArchitecture
+import SwiftUI
+
+struct BindingsAnimations: ReducerProtocol {
+ func reduce(into state: inout Bool, action: Void) -> EffectTask {
+ state.toggle()
+ return .none
+ }
+}
+
+final class VanillaModel: ObservableObject {
+ @Published var flag = false
+}
+
+let mediumAnimation = Animation.linear(duration: 0.7)
+let fastAnimation = Animation.linear(duration: 0.2)
+
+struct BindingsAnimationsTestBench: View {
+ let viewStore: ViewStoreOf
+ let vanillaModel = VanillaModel()
+
+ init(store: StoreOf) {
+ self.viewStore = ViewStore(store, observe: { $0 })
+ }
+
+ var body: some View {
+ List {
+ Section {
+ SideBySide {
+ AnimatedWithObservation.ObservedObjectBinding()
+ } viewStoreView: {
+ AnimatedWithObservation.ViewStoreBinding()
+ }
+ } header: {
+ Text("Animated with observation.")
+ } footer: {
+ Text("Should animate with the \"medium\" animation.")
+ }
+
+ Section {
+ SideBySide {
+ AnimatedFromBinding.ObservedObjectBinding()
+ } viewStoreView: {
+ AnimatedFromBinding.ViewStoreBinding()
+ }
+ } header: {
+ Text("Animated from binding.")
+ } footer: {
+ Text("Should animate with the \"fast\" animation.")
+ }
+
+ Section {
+ SideBySide {
+ AnimatedFromBindingWithObservation.ObservedObjectBinding()
+ } viewStoreView: {
+ AnimatedFromBindingWithObservation.ViewStoreBinding()
+ }
+ } header: {
+ Text("Animated from binding with observation.")
+ } footer: {
+ Text("Should animate with the \"medium\" animation.")
+ }
+ }
+ .headerProminence(.increased)
+ .environmentObject(viewStore)
+ .environmentObject(vanillaModel)
+ }
+}
+
+struct SideBySide: View {
+ let observedObjectView: ObservedObjectView
+ let viewStoreView: ViewStoreView
+ init(
+ @ViewBuilder observedObjectView: () -> ObservedObjectView,
+ @ViewBuilder viewStoreView: () -> ViewStoreView
+ ) {
+ self.observedObjectView = observedObjectView()
+ self.viewStoreView = viewStoreView()
+ }
+ var body: some View {
+ Grid {
+ GridRow {
+ observedObjectView
+ .frame(width: 100, height: 100)
+ viewStoreView
+ .frame(width: 100, height: 100)
+ }
+ .labelsHidden()
+ GridRow {
+ Text("@ObservedObject")
+ .fixedSize()
+ Text("ViewStore")
+ }
+ .font(.footnote.bold())
+ .monospaced()
+ }
+ .frame(maxWidth: .infinity)
+ }
+}
+
+struct ContentView: View {
+ @Binding var flag: Bool
+
+ var body: some View {
+ ZStack {
+ Circle()
+ .fill(.red.opacity(0.25))
+ Circle()
+ .strokeBorder(.red.opacity(0.5), lineWidth: 2)
+ }
+ .frame(width: flag ? 100 : 75)
+ }
+}
+
+struct AnimatedWithObservation {
+ struct ObservedObjectBinding: View {
+ @EnvironmentObject var vanillaModel: VanillaModel
+ var body: some View {
+ ZStack {
+ ContentView(flag: $vanillaModel.flag)
+ .animation(mediumAnimation, value: vanillaModel.flag)
+ Toggle("", isOn: $vanillaModel.flag)
+ }
+ }
+ }
+
+ struct ViewStoreBinding: View {
+ @EnvironmentObject var viewStore: ViewStoreOf
+ var body: some View {
+ ZStack {
+ ContentView(flag: viewStore.binding(send: ()))
+ .animation(mediumAnimation, value: viewStore.state)
+ Toggle("", isOn: viewStore.binding(send: ()))
+ }
+ }
+ }
+}
+
+struct AnimatedFromBinding {
+ struct ObservedObjectBinding: View {
+ @EnvironmentObject var vanillaModel: VanillaModel
+ var body: some View {
+ ZStack {
+ ContentView(flag: $vanillaModel.flag)
+ Toggle("", isOn: $vanillaModel.flag.animation(fastAnimation))
+ }
+ }
+ }
+
+ struct ViewStoreBinding: View {
+ @EnvironmentObject var viewStore: ViewStoreOf
+ var body: some View {
+ ZStack {
+ ContentView(flag: viewStore.binding(send: ()))
+ Toggle("", isOn: viewStore.binding(send: ()).animation(fastAnimation))
+ }
+ }
+ }
+}
+
+struct AnimatedFromBindingWithObservation {
+ struct ObservedObjectBinding: View {
+ @EnvironmentObject var vanillaModel: VanillaModel
+ var body: some View {
+ ZStack {
+ ContentView(flag: $vanillaModel.flag)
+ .animation(mediumAnimation, value: vanillaModel.flag)
+ Toggle("", isOn: $vanillaModel.flag.animation(fastAnimation))
+ }
+ }
+ }
+
+ struct ViewStoreBinding: View {
+ @EnvironmentObject var viewStore: ViewStoreOf
+ var body: some View {
+ ZStack {
+ ContentView(flag: viewStore.binding(send: ()))
+ .animation(mediumAnimation, value: viewStore.state)
+ Toggle("", isOn: viewStore.binding(send: ()).animation(fastAnimation))
+ }
+ }
+ }
+}
+
+struct BindingsAnimationsTestBench_Previews: PreviewProvider {
+ static var previews: some View {
+ BindingsAnimationsTestBench(
+ store: .init(
+ initialState: false,
+ reducer: BindingsAnimations()
+ )
+ )
+ }
+}
diff --git a/Examples/Integration/Integration/EscapedWithViewStoreTestCase.swift b/Examples/Integration/Integration/EscapedWithViewStoreTestCase.swift
new file mode 100644
index 000000000000..bb8b3f8cf42c
--- /dev/null
+++ b/Examples/Integration/Integration/EscapedWithViewStoreTestCase.swift
@@ -0,0 +1,47 @@
+import ComposableArchitecture
+import SwiftUI
+
+struct EscapedWithViewStoreTestCase: ReducerProtocol {
+ enum Action: Equatable, Sendable {
+ case incr
+ case decr
+ }
+
+ func reduce(into state: inout Int, action: Action) -> EffectTask {
+ switch action {
+ case .incr:
+ state += 1
+ return .none
+ case .decr:
+ state -= 1
+ return .none
+ }
+ }
+}
+
+struct EscapedWithViewStoreTestCaseView: View {
+ let store: StoreOf
+
+ var body: some View {
+ VStack {
+ WithViewStore(store, observe: { $0 }) { viewStore in
+ GeometryReader { proxy in
+ Text("\(viewStore.state)")
+ .accessibilityValue("\(viewStore.state)")
+ .accessibilityLabel("EscapedLabel")
+ }
+ Button("Button", action: { viewStore.send(.incr) })
+ Text("\(viewStore.state)")
+ .accessibilityValue("\(viewStore.state)")
+ .accessibilityLabel("Label")
+ Stepper {
+ Text("Stepper")
+ } onIncrement: {
+ viewStore.send(.incr)
+ } onDecrement: {
+ viewStore.send(.decr)
+ }
+ }
+ }
+ }
+}
diff --git a/Examples/Integration/Integration/ForEachBindingTestCase.swift b/Examples/Integration/Integration/ForEachBindingTestCase.swift
new file mode 100644
index 000000000000..8e29002c435a
--- /dev/null
+++ b/Examples/Integration/Integration/ForEachBindingTestCase.swift
@@ -0,0 +1,55 @@
+import ComposableArchitecture
+import SwiftUI
+
+struct ForEachBindingTestCase: ReducerProtocol {
+ struct State: Equatable {
+ var values = ["A", "B", "C"]
+ }
+ enum Action {
+ case change(offset: Int, value: String)
+ case removeLast
+ }
+
+ func reduce(into state: inout State, action: Action) -> EffectTask {
+ switch action {
+ case let .change(offset: offset, value: value):
+ state.values[offset] = value
+ return .none
+
+ case .removeLast:
+ guard !state.values.isEmpty
+ else { return .none }
+ state.values.removeLast()
+ return .none
+ }
+ }
+}
+
+struct ForEachBindingTestCaseView: View {
+ let store: StoreOf
+
+ var body: some View {
+ WithViewStore(self.store, observe: { $0 }) { viewStore in
+ VStack { // ⚠️ Must use VStack, not List.
+ ForEach(Array(viewStore.values.enumerated()), id: \.offset) { offset, value in
+ HStack { // ⚠️ Must wrap in an HStack.
+ TextField( // ⚠️ Must use a TextField.
+ "\(value)",
+ text: viewStore.binding(
+ get: { $0.values[offset] },
+ send: { .change(offset: offset, value: $0) }
+ )
+ )
+ }
+ }
+ }
+ .toolbar {
+ ToolbarItem {
+ Button("Remove last") {
+ viewStore.send(.removeLast)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Examples/Integration/Integration/IntegrationApp.swift b/Examples/Integration/Integration/IntegrationApp.swift
new file mode 100644
index 000000000000..794b0f72d19f
--- /dev/null
+++ b/Examples/Integration/Integration/IntegrationApp.swift
@@ -0,0 +1,53 @@
+import ComposableArchitecture
+import SwiftUI
+
+@main
+struct IntegrationApp: App {
+ @State var isNavigationStackBindingTestCasePresented = false
+
+ var body: some Scene {
+ WindowGroup {
+ NavigationStack {
+ List {
+ NavigationLink("EscapedWithViewStoreTestCase") {
+ EscapedWithViewStoreTestCaseView(
+ store: Store(
+ initialState: 10,
+ reducer: EscapedWithViewStoreTestCase()
+ )
+ )
+ }
+ NavigationLink("ForEachBindingTestCase") {
+ ForEachBindingTestCaseView(
+ store: Store(
+ initialState: ForEachBindingTestCase.State(),
+ reducer: ForEachBindingTestCase()
+ )
+ )
+ }
+
+ Button("NavigationStackBindingTestCase") {
+ self.isNavigationStackBindingTestCasePresented = true
+ }
+ .sheet(isPresented: self.$isNavigationStackBindingTestCasePresented) {
+ NavigationStackBindingTestCaseView(
+ store: Store(
+ initialState: NavigationStackBindingTestCase.State(),
+ reducer: NavigationStackBindingTestCase()
+ )
+ )
+ }
+
+ NavigationLink("Binding Animations Test Bench") {
+ BindingsAnimationsTestBench(
+ store: Store(
+ initialState: false,
+ reducer: BindingsAnimations()
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Examples/Integration/Integration/NavigationStackBindingTestCase.swift b/Examples/Integration/Integration/NavigationStackBindingTestCase.swift
new file mode 100644
index 000000000000..e1e99cf6a0e0
--- /dev/null
+++ b/Examples/Integration/Integration/NavigationStackBindingTestCase.swift
@@ -0,0 +1,55 @@
+import ComposableArchitecture
+import SwiftUI
+
+struct NavigationStackBindingTestCase: ReducerProtocol {
+ struct State: Equatable {
+ var path: [Destination] = []
+ enum Destination: Equatable {
+ case child
+ }
+ }
+ enum Action: Equatable, Sendable {
+ case goToChild
+ case navigationPathChanged([State.Destination])
+ }
+
+ func reduce(into state: inout State, action: Action) -> EffectTask {
+ switch action {
+ case .goToChild:
+ state.path.append(.child)
+ return .none
+ case let .navigationPathChanged(path):
+ state.path = path
+ return .none
+ }
+ }
+}
+
+struct NavigationStackBindingTestCaseView: View {
+ let store: StoreOf
+
+ var body: some View {
+ WithViewStore(store) { viewStore in
+ NavigationStack(
+ path: viewStore.binding(
+ get: \.path,
+ send: NavigationStackBindingTestCase.Action.navigationPathChanged
+ )
+ ) {
+ VStack {
+ Text("Root")
+ Button("Go to child") {
+ viewStore.send(.goToChild)
+ }
+ }
+ .navigationDestination(
+ for: NavigationStackBindingTestCase.State.Destination.self
+ ) { destination in
+ switch destination {
+ case .child: Text("Child")
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Examples/Integration/Integration/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/Integration/Integration/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 000000000000..73c00596a7fc
--- /dev/null
+++ b/Examples/Integration/Integration/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Examples/Integration/IntegrationUITests/EscapedWithViewStoreTests.swift b/Examples/Integration/IntegrationUITests/EscapedWithViewStoreTests.swift
new file mode 100644
index 000000000000..189ec6524016
--- /dev/null
+++ b/Examples/Integration/IntegrationUITests/EscapedWithViewStoreTests.swift
@@ -0,0 +1,41 @@
+import XCTest
+
+@MainActor
+final class EscapedWithViewStoreTests: XCTestCase {
+
+ override func setUpWithError() throws {
+ continueAfterFailure = false
+ }
+
+ func testExample() async throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ app.collectionViews.buttons["EscapedWithViewStoreTestCase"].tap()
+
+ XCTAssertEqual(app.staticTexts["Label"].value as? String, "10")
+ XCTAssertEqual(app.staticTexts["EscapedLabel"].value as? String, "10")
+
+ app.buttons["Button"].tap()
+
+ XCTAssertEqual(app.staticTexts["Label"].value as? String, "11")
+ XCTAssertEqual(app.staticTexts["EscapedLabel"].value as? String, "11")
+
+ let stepper = app.steppers["Stepper"]
+
+ stepper.buttons["Increment"].tap()
+ stepper.buttons["Increment"].tap()
+ stepper.buttons["Increment"].tap()
+ stepper.buttons["Increment"].tap()
+
+ XCTAssertEqual(app.staticTexts["Label"].value as? String, "15")
+ XCTAssertEqual(app.staticTexts["EscapedLabel"].value as? String, "15")
+
+ stepper.buttons["Decrement"].tap()
+ stepper.buttons["Decrement"].tap()
+ stepper.buttons["Decrement"].tap()
+
+ XCTAssertEqual(app.staticTexts["Label"].value as? String, "12")
+ XCTAssertEqual(app.staticTexts["EscapedLabel"].value as? String, "12")
+ }
+}
diff --git a/Examples/Integration/IntegrationUITests/ForEachBindingTests.swift b/Examples/Integration/IntegrationUITests/ForEachBindingTests.swift
new file mode 100644
index 000000000000..4ac9c615ad30
--- /dev/null
+++ b/Examples/Integration/IntegrationUITests/ForEachBindingTests.swift
@@ -0,0 +1,19 @@
+import XCTest
+
+@MainActor
+final class ForEachBindingTests: XCTestCase {
+
+ override func setUpWithError() throws {
+ continueAfterFailure = false
+ }
+
+ func testExample() async throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ app.collectionViews.buttons["ForEachBindingTestCase"].tap()
+ app.buttons["Remove last"].tap()
+
+ XCTAssertFalse(app.textFields["C"].exists)
+ }
+}
diff --git a/Examples/Integration/IntegrationUITests/NavigationStackBindingTests.swift b/Examples/Integration/IntegrationUITests/NavigationStackBindingTests.swift
new file mode 100644
index 000000000000..116ec13ae8db
--- /dev/null
+++ b/Examples/Integration/IntegrationUITests/NavigationStackBindingTests.swift
@@ -0,0 +1,17 @@
+import XCTest
+
+@MainActor
+final class NavigationStackBindingTests: XCTestCase {
+ override func setUpWithError() throws {
+ continueAfterFailure = false
+ }
+
+ func testExample() async throws {
+ let app = XCUIApplication()
+ app.launch()
+ app.collectionViews.buttons["NavigationStackBindingTestCase"].tap()
+ app.buttons["Go to child"].tap()
+ app.buttons["Back"].tap()
+ XCTAssertTrue(app.buttons["Go to child"].exists)
+ }
+}
diff --git a/Examples/Search/SearchTests/SearchTests.swift b/Examples/Search/SearchTests/SearchTests.swift
index 418a031d0ac7..95fc932d5264 100644
--- a/Examples/Search/SearchTests/SearchTests.swift
+++ b/Examples/Search/SearchTests/SearchTests.swift
@@ -9,9 +9,9 @@ final class SearchTests: XCTestCase {
let store = TestStore(
initialState: Search.State(),
reducer: Search()
- )
-
- store.dependencies.weatherClient.search = { _ in .mock }
+ ) {
+ $0.weatherClient.search = { _ in .mock }
+ }
await store.send(.searchQueryChanged("S")) {
$0.searchQuery = "S"
@@ -30,9 +30,9 @@ final class SearchTests: XCTestCase {
let store = TestStore(
initialState: Search.State(),
reducer: Search()
- )
-
- store.dependencies.weatherClient.search = { _ in throw SomethingWentWrong() }
+ ) {
+ $0.weatherClient.search = { _ in throw SomethingWentWrong() }
+ }
await store.send(.searchQueryChanged("S")) {
$0.searchQuery = "S"
@@ -45,9 +45,9 @@ final class SearchTests: XCTestCase {
let store = TestStore(
initialState: Search.State(),
reducer: Search()
- )
-
- store.dependencies.weatherClient.search = { _ in .mock }
+ ) {
+ $0.weatherClient.search = { _ in .mock }
+ }
let searchQueryChanged = await store.send(.searchQueryChanged("S")) {
$0.searchQuery = "S"
@@ -73,9 +73,9 @@ final class SearchTests: XCTestCase {
let store = TestStore(
initialState: Search.State(results: results),
reducer: Search()
- )
-
- store.dependencies.weatherClient.forecast = { _ in .mock }
+ ) {
+ $0.weatherClient.forecast = { _ in .mock }
+ }
await store.send(.searchResultTapped(specialResult)) {
$0.resultForecastRequestInFlight = specialResult
@@ -123,15 +123,16 @@ final class SearchTests: XCTestCase {
var results = GeocodingSearch.mock.results
results.append(specialResult)
+ let clock = TestClock()
+
let store = TestStore(
initialState: Search.State(results: results),
reducer: Search()
- )
-
- let clock = TestClock()
- store.dependencies.weatherClient.forecast = { _ in
- try await clock.sleep(for: .seconds(0))
- return .mock
+ ) {
+ $0.weatherClient.forecast = { _ in
+ try await clock.sleep(for: .seconds(0))
+ return .mock
+ }
}
await store.send(.searchResultTapped(results.first!)) {
@@ -178,9 +179,9 @@ final class SearchTests: XCTestCase {
let store = TestStore(
initialState: Search.State(results: results),
reducer: Search()
- )
-
- store.dependencies.weatherClient.forecast = { _ in throw SomethingWentWrong() }
+ ) {
+ $0.weatherClient.forecast = { _ in throw SomethingWentWrong() }
+ }
await store.send(.searchResultTapped(results.first!)) {
$0.resultForecastRequestInFlight = results.first!
diff --git a/Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Client.swift b/Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Client.swift
index e0fcadf499b4..8376d99d68ea 100644
--- a/Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Client.swift
+++ b/Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Client.swift
@@ -1,4 +1,3 @@
-import ComposableArchitecture
import Dependencies
import Speech
import XCTestDynamicOverlay
diff --git a/Examples/SpeechRecognition/SpeechRecognitionTests/SpeechRecognitionTests.swift b/Examples/SpeechRecognition/SpeechRecognitionTests/SpeechRecognitionTests.swift
index 5731d645e847..b3001b3aa30d 100644
--- a/Examples/SpeechRecognition/SpeechRecognitionTests/SpeechRecognitionTests.swift
+++ b/Examples/SpeechRecognition/SpeechRecognitionTests/SpeechRecognitionTests.swift
@@ -11,9 +11,9 @@ final class SpeechRecognitionTests: XCTestCase {
let store = TestStore(
initialState: SpeechRecognition.State(),
reducer: SpeechRecognition()
- )
-
- store.dependencies.speechClient.requestAuthorization = { .denied }
+ ) {
+ $0.speechClient.requestAuthorization = { .denied }
+ }
await store.send(.recordButtonTapped) {
$0.isRecording = true
@@ -34,9 +34,9 @@ final class SpeechRecognitionTests: XCTestCase {
let store = TestStore(
initialState: SpeechRecognition.State(),
reducer: SpeechRecognition()
- )
-
- store.dependencies.speechClient.requestAuthorization = { .restricted }
+ ) {
+ $0.speechClient.requestAuthorization = { .restricted }
+ }
await store.send(.recordButtonTapped) {
$0.isRecording = true
@@ -51,11 +51,11 @@ final class SpeechRecognitionTests: XCTestCase {
let store = TestStore(
initialState: SpeechRecognition.State(),
reducer: SpeechRecognition()
- )
-
- store.dependencies.speechClient.finishTask = { self.recognitionTask.continuation.finish() }
- store.dependencies.speechClient.startTask = { _ in self.recognitionTask.stream }
- store.dependencies.speechClient.requestAuthorization = { .authorized }
+ ) {
+ $0.speechClient.finishTask = { self.recognitionTask.continuation.finish() }
+ $0.speechClient.startTask = { _ in self.recognitionTask.stream }
+ $0.speechClient.requestAuthorization = { .authorized }
+ }
let firstResult = SpeechRecognitionResult(
bestTranscription: Transcription(
@@ -95,10 +95,10 @@ final class SpeechRecognitionTests: XCTestCase {
let store = TestStore(
initialState: SpeechRecognition.State(),
reducer: SpeechRecognition()
- )
-
- store.dependencies.speechClient.startTask = { _ in self.recognitionTask.stream }
- store.dependencies.speechClient.requestAuthorization = { .authorized }
+ ) {
+ $0.speechClient.startTask = { _ in self.recognitionTask.stream }
+ $0.speechClient.requestAuthorization = { .authorized }
+ }
await store.send(.recordButtonTapped) {
$0.isRecording = true
@@ -116,10 +116,10 @@ final class SpeechRecognitionTests: XCTestCase {
let store = TestStore(
initialState: SpeechRecognition.State(),
reducer: SpeechRecognition()
- )
-
- store.dependencies.speechClient.startTask = { _ in self.recognitionTask.stream }
- store.dependencies.speechClient.requestAuthorization = { .authorized }
+ ) {
+ $0.speechClient.startTask = { _ in self.recognitionTask.stream }
+ $0.speechClient.requestAuthorization = { .authorized }
+ }
await store.send(.recordButtonTapped) {
$0.isRecording = true
diff --git a/Examples/TicTacToe/tic-tac-toe/Package.swift b/Examples/TicTacToe/tic-tac-toe/Package.swift
index 9b596bdf37ed..188d3a8b272c 100644
--- a/Examples/TicTacToe/tic-tac-toe/Package.swift
+++ b/Examples/TicTacToe/tic-tac-toe/Package.swift
@@ -27,7 +27,8 @@ let package = Package(
.library(name: "TwoFactorUIKit", targets: ["TwoFactorUIKit"]),
],
dependencies: [
- .package(name: "swift-composable-architecture", path: "../../..")
+ .package(name: "swift-composable-architecture", path: "../../.."),
+ .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "0.1.0"),
],
targets: [
.target(
@@ -63,7 +64,7 @@ let package = Package(
.target(
name: "AuthenticationClient",
dependencies: [
- .product(name: "Dependencies", package: "swift-composable-architecture")
+ .product(name: "Dependencies", package: "swift-dependencies")
]
),
.target(
diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/LoginSwiftUI/LoginView.swift b/Examples/TicTacToe/tic-tac-toe/Sources/LoginSwiftUI/LoginView.swift
index 45f74b14940f..4bf23491b15a 100644
--- a/Examples/TicTacToe/tic-tac-toe/Sources/LoginSwiftUI/LoginView.swift
+++ b/Examples/TicTacToe/tic-tac-toe/Sources/LoginSwiftUI/LoginView.swift
@@ -125,13 +125,14 @@ struct LoginView_Previews: PreviewProvider {
store: Store(
initialState: Login.State(),
reducer: Login()
- .dependency(\.authenticationClient.login) { _ in
- AuthenticationResponse(token: "deadbeef", twoFactorRequired: false)
- }
- .dependency(\.authenticationClient.twoFactor) { _ in
- AuthenticationResponse(token: "deadbeef", twoFactorRequired: false)
- }
- )
+ ) {
+ $0.authenticationClient.login = { _ in
+ AuthenticationResponse(token: "deadbeef", twoFactorRequired: false)
+ }
+ $0.authenticationClient.twoFactor = { _ in
+ AuthenticationResponse(token: "deadbeef", twoFactorRequired: false)
+ }
+ }
)
}
}
diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorSwiftUI/TwoFactorView.swift b/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorSwiftUI/TwoFactorView.swift
index 110704c2378d..ce91cc15b977 100644
--- a/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorSwiftUI/TwoFactorView.swift
+++ b/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorSwiftUI/TwoFactorView.swift
@@ -93,13 +93,14 @@ struct TwoFactorView_Previews: PreviewProvider {
store: Store(
initialState: TwoFactor.State(token: "deadbeef"),
reducer: TwoFactor()
- .dependency(\.authenticationClient.login) { _ in
- AuthenticationResponse(token: "deadbeef", twoFactorRequired: false)
- }
- .dependency(\.authenticationClient.twoFactor) { _ in
- AuthenticationResponse(token: "deadbeef", twoFactorRequired: false)
- }
- )
+ ) {
+ $0.authenticationClient.login = { _ in
+ AuthenticationResponse(token: "deadbeef", twoFactorRequired: false)
+ }
+ $0.authenticationClient.twoFactor = { _ in
+ AuthenticationResponse(token: "deadbeef", twoFactorRequired: false)
+ }
+ }
)
}
}
diff --git a/Examples/TicTacToe/tic-tac-toe/Tests/AppCoreTests/AppCoreTests.swift b/Examples/TicTacToe/tic-tac-toe/Tests/AppCoreTests/AppCoreTests.swift
index c786b47e68c6..e4ca35f2bafe 100644
--- a/Examples/TicTacToe/tic-tac-toe/Tests/AppCoreTests/AppCoreTests.swift
+++ b/Examples/TicTacToe/tic-tac-toe/Tests/AppCoreTests/AppCoreTests.swift
@@ -12,10 +12,10 @@ final class AppCoreTests: XCTestCase {
let store = TestStore(
initialState: TicTacToe.State(),
reducer: TicTacToe()
- )
-
- store.dependencies.authenticationClient.login = { _ in
- AuthenticationResponse(token: "deadbeef", twoFactorRequired: false)
+ ) {
+ $0.authenticationClient.login = { _ in
+ AuthenticationResponse(token: "deadbeef", twoFactorRequired: false)
+ }
}
await store.send(.login(.emailChanged("blob@pointfree.co"))) {
@@ -57,13 +57,13 @@ final class AppCoreTests: XCTestCase {
let store = TestStore(
initialState: TicTacToe.State(),
reducer: TicTacToe()
- )
-
- store.dependencies.authenticationClient.login = { _ in
- AuthenticationResponse(token: "deadbeef", twoFactorRequired: true)
- }
- store.dependencies.authenticationClient.twoFactor = { _ in
- AuthenticationResponse(token: "deadbeef", twoFactorRequired: false)
+ ) {
+ $0.authenticationClient.login = { _ in
+ AuthenticationResponse(token: "deadbeef", twoFactorRequired: true)
+ }
+ $0.authenticationClient.twoFactor = { _ in
+ AuthenticationResponse(token: "deadbeef", twoFactorRequired: false)
+ }
}
await store.send(.login(.emailChanged("blob@pointfree.co"))) {
diff --git a/Examples/TicTacToe/tic-tac-toe/Tests/GameSwiftUITests/GameSwiftUITests.swift b/Examples/TicTacToe/tic-tac-toe/Tests/GameSwiftUITests/GameSwiftUITests.swift
index cd88dfbeeb62..65b4e564d524 100644
--- a/Examples/TicTacToe/tic-tac-toe/Tests/GameSwiftUITests/GameSwiftUITests.swift
+++ b/Examples/TicTacToe/tic-tac-toe/Tests/GameSwiftUITests/GameSwiftUITests.swift
@@ -11,9 +11,10 @@ final class GameSwiftUITests: XCTestCase {
oPlayerName: "Blob Jr.",
xPlayerName: "Blob Sr."
),
- reducer: Game()
+ reducer: Game(),
+ observe: GameView.ViewState.init,
+ send: { $0 }
)
- .scope(state: GameView.ViewState.init)
func testFlow_Winner_Quit() async {
await self.store.send(.cellTapped(row: 0, column: 0)) {
diff --git a/Examples/TicTacToe/tic-tac-toe/Tests/LoginCoreTests/LoginCoreTests.swift b/Examples/TicTacToe/tic-tac-toe/Tests/LoginCoreTests/LoginCoreTests.swift
index 76d9a1af266c..929c14dc76e9 100644
--- a/Examples/TicTacToe/tic-tac-toe/Tests/LoginCoreTests/LoginCoreTests.swift
+++ b/Examples/TicTacToe/tic-tac-toe/Tests/LoginCoreTests/LoginCoreTests.swift
@@ -10,13 +10,13 @@ final class LoginCoreTests: XCTestCase {
let store = TestStore(
initialState: Login.State(),
reducer: Login()
- )
-
- store.dependencies.authenticationClient.login = { _ in
- AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: true)
- }
- store.dependencies.authenticationClient.twoFactor = { _ in
- AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: false)
+ ) {
+ $0.authenticationClient.login = { _ in
+ AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: true)
+ }
+ $0.authenticationClient.twoFactor = { _ in
+ AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: false)
+ }
}
await store.send(.emailChanged("2fa@pointfree.co")) {
@@ -59,14 +59,14 @@ final class LoginCoreTests: XCTestCase {
let store = TestStore(
initialState: Login.State(),
reducer: Login()
- )
-
- store.dependencies.authenticationClient.login = { _ in
- AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: true)
- }
- store.dependencies.authenticationClient.twoFactor = { _ in
- try await Task.sleep(nanoseconds: NSEC_PER_SEC)
- return AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: false)
+ ) {
+ $0.authenticationClient.login = { _ in
+ AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: true)
+ }
+ $0.authenticationClient.twoFactor = { _ in
+ try await Task.sleep(nanoseconds: NSEC_PER_SEC)
+ return AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: false)
+ }
}
await store.send(.emailChanged("2fa@pointfree.co")) {
diff --git a/Examples/TicTacToe/tic-tac-toe/Tests/LoginSwiftUITests/LoginSwiftUITests.swift b/Examples/TicTacToe/tic-tac-toe/Tests/LoginSwiftUITests/LoginSwiftUITests.swift
index e0ce671c9004..cdbf6815eee5 100644
--- a/Examples/TicTacToe/tic-tac-toe/Tests/LoginSwiftUITests/LoginSwiftUITests.swift
+++ b/Examples/TicTacToe/tic-tac-toe/Tests/LoginSwiftUITests/LoginSwiftUITests.swift
@@ -10,12 +10,13 @@ final class LoginSwiftUITests: XCTestCase {
func testFlow_Success() async {
let store = TestStore(
initialState: Login.State(),
- reducer: Login()
- )
- .scope(state: LoginView.ViewState.init, action: Login.Action.init)
-
- store.dependencies.authenticationClient.login = { _ in
- AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: false)
+ reducer: Login(),
+ observe: LoginView.ViewState.init,
+ send: Login.Action.init
+ ) {
+ $0.authenticationClient.login = { _ in
+ AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: false)
+ }
}
await store.send(.emailChanged("blob@pointfree.co")) {
@@ -42,12 +43,13 @@ final class LoginSwiftUITests: XCTestCase {
func testFlow_Success_TwoFactor() async {
let store = TestStore(
initialState: Login.State(),
- reducer: Login()
- )
- .scope(state: LoginView.ViewState.init, action: Login.Action.init)
-
- store.dependencies.authenticationClient.login = { _ in
- AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: true)
+ reducer: Login(),
+ observe: LoginView.ViewState.init,
+ send: Login.Action.init
+ ) {
+ $0.authenticationClient.login = { _ in
+ AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: true)
+ }
}
await store.send(.emailChanged("2fa@pointfree.co")) {
@@ -78,12 +80,13 @@ final class LoginSwiftUITests: XCTestCase {
func testFlow_Failure() async {
let store = TestStore(
initialState: Login.State(),
- reducer: Login()
- )
- .scope(state: LoginView.ViewState.init, action: Login.Action.init)
-
- store.dependencies.authenticationClient.login = { _ in
- throw AuthenticationError.invalidUserPassword
+ reducer: Login(),
+ observe: LoginView.ViewState.init,
+ send: Login.Action.init
+ ) {
+ $0.authenticationClient.login = { _ in
+ throw AuthenticationError.invalidUserPassword
+ }
}
await store.send(.emailChanged("blob")) {
diff --git a/Examples/TicTacToe/tic-tac-toe/Tests/NewGameSwiftUITests/NewGameSwiftUITests.swift b/Examples/TicTacToe/tic-tac-toe/Tests/NewGameSwiftUITests/NewGameSwiftUITests.swift
index 2a59037dfea7..63eac2213cc2 100644
--- a/Examples/TicTacToe/tic-tac-toe/Tests/NewGameSwiftUITests/NewGameSwiftUITests.swift
+++ b/Examples/TicTacToe/tic-tac-toe/Tests/NewGameSwiftUITests/NewGameSwiftUITests.swift
@@ -8,9 +8,10 @@ import XCTest
final class NewGameSwiftUITests: XCTestCase {
let store = TestStore(
initialState: NewGame.State(),
- reducer: NewGame()
+ reducer: NewGame(),
+ observe: NewGameView.ViewState.init,
+ send: NewGame.Action.init
)
- .scope(state: NewGameView.ViewState.init, action: NewGame.Action.init)
func testNewGame() async {
await self.store.send(.xPlayerNameChanged("Blob Sr.")) {
diff --git a/Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorCoreTests/TwoFactorCoreTests.swift b/Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorCoreTests/TwoFactorCoreTests.swift
index 5c59cdf2f80a..81de93ffb453 100644
--- a/Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorCoreTests/TwoFactorCoreTests.swift
+++ b/Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorCoreTests/TwoFactorCoreTests.swift
@@ -9,10 +9,10 @@ final class TwoFactorCoreTests: XCTestCase {
let store = TestStore(
initialState: TwoFactor.State(token: "deadbeefdeadbeef"),
reducer: TwoFactor()
- )
-
- store.dependencies.authenticationClient.twoFactor = { _ in
- AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: false)
+ ) {
+ $0.authenticationClient.twoFactor = { _ in
+ AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: false)
+ }
}
await store.send(.codeChanged("1")) {
@@ -44,10 +44,10 @@ final class TwoFactorCoreTests: XCTestCase {
let store = TestStore(
initialState: TwoFactor.State(token: "deadbeefdeadbeef"),
reducer: TwoFactor()
- )
-
- store.dependencies.authenticationClient.twoFactor = { _ in
- throw AuthenticationError.invalidTwoFactor
+ ) {
+ $0.authenticationClient.twoFactor = { _ in
+ throw AuthenticationError.invalidTwoFactor
+ }
}
await store.send(.codeChanged("1234")) {
diff --git a/Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorSwiftUITests/TwoFactorSwiftUITests.swift b/Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorSwiftUITests/TwoFactorSwiftUITests.swift
index f1ee4321ff1a..cc0053d55f6b 100644
--- a/Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorSwiftUITests/TwoFactorSwiftUITests.swift
+++ b/Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorSwiftUITests/TwoFactorSwiftUITests.swift
@@ -10,12 +10,13 @@ final class TwoFactorSwiftUITests: XCTestCase {
func testFlow_Success() async {
let store = TestStore(
initialState: TwoFactor.State(token: "deadbeefdeadbeef"),
- reducer: TwoFactor()
- )
- .scope(state: TwoFactorView.ViewState.init, action: TwoFactor.Action.init)
-
- store.dependencies.authenticationClient.twoFactor = { _ in
- AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: false)
+ reducer: TwoFactor(),
+ observe: TwoFactorView.ViewState.init,
+ send: TwoFactor.Action.init
+ ) {
+ $0.authenticationClient.twoFactor = { _ in
+ AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: false)
+ }
}
await store.send(.codeChanged("1")) {
@@ -50,12 +51,13 @@ final class TwoFactorSwiftUITests: XCTestCase {
func testFlow_Failure() async {
let store = TestStore(
initialState: TwoFactor.State(token: "deadbeefdeadbeef"),
- reducer: TwoFactor()
- )
- .scope(state: TwoFactorView.ViewState.init, action: TwoFactor.Action.init)
-
- store.dependencies.authenticationClient.twoFactor = { _ in
- throw AuthenticationError.invalidTwoFactor
+ reducer: TwoFactor(),
+ observe: TwoFactorView.ViewState.init,
+ send: TwoFactor.Action.init
+ ) {
+ $0.authenticationClient.twoFactor = { _ in
+ throw AuthenticationError.invalidTwoFactor
+ }
}
await store.send(.codeChanged("1234")) {
diff --git a/Examples/Todos/TodosTests/TodosTests.swift b/Examples/Todos/TodosTests/TodosTests.swift
index f66e0b9aefeb..717704c2c349 100644
--- a/Examples/Todos/TodosTests/TodosTests.swift
+++ b/Examples/Todos/TodosTests/TodosTests.swift
@@ -11,9 +11,9 @@ final class TodosTests: XCTestCase {
let store = TestStore(
initialState: Todos.State(),
reducer: Todos()
- )
-
- store.dependencies.uuid = .incrementing
+ ) {
+ $0.uuid = .incrementing
+ }
await store.send(.addTodoButtonTapped) {
$0.todos.insert(
@@ -84,9 +84,9 @@ final class TodosTests: XCTestCase {
let store = TestStore(
initialState: state,
reducer: Todos()
- )
-
- store.dependencies.continuousClock = self.clock
+ ) {
+ $0.continuousClock = self.clock
+ }
await store.send(.todo(id: state.todos[0].id, action: .checkBoxToggled)) {
$0.todos[id: state.todos[0].id]?.isComplete = true
@@ -119,9 +119,9 @@ final class TodosTests: XCTestCase {
let store = TestStore(
initialState: state,
reducer: Todos()
- )
-
- store.dependencies.continuousClock = self.clock
+ ) {
+ $0.continuousClock = self.clock
+ }
await store.send(.todo(id: state.todos[0].id, action: .checkBoxToggled)) {
$0.todos[id: state.todos[0].id]?.isComplete = true
@@ -255,9 +255,9 @@ final class TodosTests: XCTestCase {
let store = TestStore(
initialState: state,
reducer: Todos()
- )
-
- store.dependencies.continuousClock = self.clock
+ ) {
+ $0.continuousClock = self.clock
+ }
await store.send(.editModeChanged(.active)) {
$0.editMode = .active
@@ -302,10 +302,10 @@ final class TodosTests: XCTestCase {
let store = TestStore(
initialState: state,
reducer: Todos()
- )
-
- store.dependencies.continuousClock = self.clock
- store.dependencies.uuid = .incrementing
+ ) {
+ $0.continuousClock = self.clock
+ $0.uuid = .incrementing
+ }
await store.send(.editModeChanged(.active)) {
$0.editMode = .active
diff --git a/Examples/VoiceMemos/VoiceMemos/AudioRecorderClient/AudioRecorderClient.swift b/Examples/VoiceMemos/VoiceMemos/AudioRecorderClient/AudioRecorderClient.swift
index 2f69ea76fca6..1f818f70bc81 100644
--- a/Examples/VoiceMemos/VoiceMemos/AudioRecorderClient/AudioRecorderClient.swift
+++ b/Examples/VoiceMemos/VoiceMemos/AudioRecorderClient/AudioRecorderClient.swift
@@ -1,4 +1,3 @@
-import ComposableArchitecture // Required for `ActorIsolated`
import Dependencies
import Foundation
import XCTestDynamicOverlay
diff --git a/Examples/VoiceMemos/VoiceMemos/AudioRecorderClient/LiveAudioRecorderClient.swift b/Examples/VoiceMemos/VoiceMemos/AudioRecorderClient/LiveAudioRecorderClient.swift
index db55e4454c65..5815f0619a21 100644
--- a/Examples/VoiceMemos/VoiceMemos/AudioRecorderClient/LiveAudioRecorderClient.swift
+++ b/Examples/VoiceMemos/VoiceMemos/AudioRecorderClient/LiveAudioRecorderClient.swift
@@ -1,5 +1,5 @@
import AVFoundation
-import ComposableArchitecture // TODO: Should `UncheckedSendable` live in `Dependencies`?
+import Dependencies
extension AudioRecorderClient: DependencyKey {
static var liveValue: Self {
diff --git a/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift b/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift
index a007708f26dd..ffefa5def436 100644
--- a/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift
+++ b/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift
@@ -11,25 +11,26 @@ final class VoiceMemosTests: XCTestCase {
// NB: Combine's concatenation behavior is different in 13.3
guard #available(iOS 13.4, *) else { return }
+ let didFinish = AsyncThrowingStream.streamWithContinuation()
+
let store = TestStore(
initialState: VoiceMemos.State(),
reducer: VoiceMemos()
- )
-
- let didFinish = AsyncThrowingStream.streamWithContinuation()
- store.dependencies.audioRecorder.currentTime = { 2.5 }
- store.dependencies.audioRecorder.requestRecordPermission = { true }
- store.dependencies.audioRecorder.startRecording = { _ in
- try await didFinish.stream.first { _ in true }!
- }
- store.dependencies.audioRecorder.stopRecording = {
- didFinish.continuation.yield(true)
- didFinish.continuation.finish()
+ ) {
+ $0.audioRecorder.currentTime = { 2.5 }
+ $0.audioRecorder.requestRecordPermission = { true }
+ $0.audioRecorder.startRecording = { _ in
+ try await didFinish.stream.first { _ in true }!
+ }
+ $0.audioRecorder.stopRecording = {
+ didFinish.continuation.yield(true)
+ didFinish.continuation.finish()
+ }
+ $0.date = .constant(Date(timeIntervalSinceReferenceDate: 0))
+ $0.continuousClock = self.clock
+ $0.temporaryDirectory = { URL(fileURLWithPath: "/tmp") }
+ $0.uuid = .constant(UUID(uuidString: "DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF")!)
}
- store.dependencies.date = .constant(Date(timeIntervalSinceReferenceDate: 0))
- store.dependencies.continuousClock = self.clock
- store.dependencies.temporaryDirectory = { URL(fileURLWithPath: "/tmp") }
- store.dependencies.uuid = .constant(UUID(uuidString: "DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF")!)
await store.send(.recordButtonTapped)
await self.clock.advance()
@@ -76,15 +77,14 @@ final class VoiceMemosTests: XCTestCase {
}
func testPermissionDenied() async {
+ var didOpenSettings = false
let store = TestStore(
initialState: VoiceMemos.State(),
reducer: VoiceMemos()
- )
-
- var didOpenSettings = false
-
- store.dependencies.audioRecorder.requestRecordPermission = { false }
- store.dependencies.openSettings = { @MainActor in didOpenSettings = true }
+ ) {
+ $0.audioRecorder.requestRecordPermission = { false }
+ $0.openSettings = { @MainActor in didOpenSettings = true }
+ }
await store.send(.recordButtonTapped)
await store.receive(.recordPermissionResponse(false)) {
@@ -99,23 +99,23 @@ final class VoiceMemosTests: XCTestCase {
}
func testRecordMemoFailure() async {
- let store = TestStore(
- initialState: VoiceMemos.State(),
- reducer: VoiceMemos()
- )
-
struct SomeError: Error, Equatable {}
let didFinish = AsyncThrowingStream.streamWithContinuation()
- store.dependencies.audioRecorder.currentTime = { 2.5 }
- store.dependencies.audioRecorder.requestRecordPermission = { true }
- store.dependencies.audioRecorder.startRecording = { _ in
- try await didFinish.stream.first { _ in true }!
+ let store = TestStore(
+ initialState: VoiceMemos.State(),
+ reducer: VoiceMemos()
+ ) {
+ $0.audioRecorder.currentTime = { 2.5 }
+ $0.audioRecorder.requestRecordPermission = { true }
+ $0.audioRecorder.startRecording = { _ in
+ try await didFinish.stream.first { _ in true }!
+ }
+ $0.continuousClock = self.clock
+ $0.date = .constant(Date(timeIntervalSinceReferenceDate: 0))
+ $0.temporaryDirectory = { URL(fileURLWithPath: "/tmp") }
+ $0.uuid = .constant(UUID(uuidString: "DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF")!)
}
- store.dependencies.continuousClock = self.clock
- store.dependencies.date = .constant(Date(timeIntervalSinceReferenceDate: 0))
- store.dependencies.temporaryDirectory = { URL(fileURLWithPath: "/tmp") }
- store.dependencies.uuid = .constant(UUID(uuidString: "DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF")!)
await store.send(.recordButtonTapped)
await store.receive(.recordPermissionResponse(true)) {
@@ -141,24 +141,24 @@ final class VoiceMemosTests: XCTestCase {
// Demonstration of how to write a non-exhaustive test for recording a memo and it failing to
// record.
func testRecordMemoFailure_NonExhaustive() async {
- let store = TestStore(
- initialState: VoiceMemos.State(),
- reducer: VoiceMemos()
- )
- store.exhaustivity = .off(showSkippedAssertions: true)
-
struct SomeError: Error, Equatable {}
let didFinish = AsyncThrowingStream.streamWithContinuation()
- store.dependencies.audioRecorder.currentTime = { 2.5 }
- store.dependencies.audioRecorder.requestRecordPermission = { true }
- store.dependencies.audioRecorder.startRecording = { _ in
- try await didFinish.stream.first { _ in true }!
+ let store = TestStore(
+ initialState: VoiceMemos.State(),
+ reducer: VoiceMemos()
+ ) {
+ $0.audioRecorder.currentTime = { 2.5 }
+ $0.audioRecorder.requestRecordPermission = { true }
+ $0.audioRecorder.startRecording = { _ in
+ try await didFinish.stream.first { _ in true }!
+ }
+ $0.continuousClock = self.clock
+ $0.date = .constant(Date(timeIntervalSinceReferenceDate: 0))
+ $0.temporaryDirectory = { URL(fileURLWithPath: "/tmp") }
+ $0.uuid = .constant(UUID(uuidString: "DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF")!)
}
- store.dependencies.continuousClock = self.clock
- store.dependencies.date = .constant(Date(timeIntervalSinceReferenceDate: 0))
- store.dependencies.temporaryDirectory = { URL(fileURLWithPath: "/tmp") }
- store.dependencies.uuid = .constant(UUID(uuidString: "DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF")!)
+ store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(.recordButtonTapped)
await store.send(.recordingMemo(.task))
@@ -184,13 +184,13 @@ final class VoiceMemosTests: XCTestCase {
]
),
reducer: VoiceMemos()
- )
-
- store.dependencies.audioPlayer.play = { _ in
- try await self.clock.sleep(for: .milliseconds(1_250))
- return true
+ ) {
+ $0.audioPlayer.play = { _ in
+ try await self.clock.sleep(for: .milliseconds(1_250))
+ return true
+ }
+ $0.continuousClock = self.clock
}
- store.dependencies.continuousClock = self.clock
let task = await store.send(.voiceMemo(id: url, action: .playButtonTapped)) {
$0.voiceMemos[id: url]?.mode = .playing(progress: 0)
@@ -211,6 +211,8 @@ final class VoiceMemosTests: XCTestCase {
}
func testPlayMemoFailure() async {
+ struct SomeError: Error, Equatable {}
+
let url = URL(fileURLWithPath: "pointfreeco/functions.m4a")
let store = TestStore(
initialState: VoiceMemos.State(
@@ -225,12 +227,10 @@ final class VoiceMemosTests: XCTestCase {
]
),
reducer: VoiceMemos()
- )
-
- struct SomeError: Error, Equatable {}
-
- store.dependencies.audioPlayer.play = { _ in throw SomeError() }
- store.dependencies.continuousClock = self.clock
+ ) {
+ $0.audioPlayer.play = { _ in throw SomeError() }
+ $0.continuousClock = self.clock
+ }
let task = await store.send(.voiceMemo(id: url, action: .playButtonTapped)) {
$0.voiceMemos[id: url]?.mode = .playing(progress: 0)
@@ -302,10 +302,10 @@ final class VoiceMemosTests: XCTestCase {
]
),
reducer: VoiceMemos()
- )
-
- store.dependencies.audioPlayer.play = { _ in try await Task.never() }
- store.dependencies.continuousClock = self.clock
+ ) {
+ $0.audioPlayer.play = { _ in try await Task.never() }
+ $0.continuousClock = self.clock
+ }
await store.send(.voiceMemo(id: url, action: .playButtonTapped)) {
$0.voiceMemos[id: url]?.mode = .playing(progress: 0)
diff --git a/Makefile b/Makefile
index adaec430c0aa..2702d5d4b661 100644
--- a/Makefile
+++ b/Makefile
@@ -8,9 +8,7 @@ default: test-all
test-all: test-examples
CONFIG=debug test-library
- CONFIG=release test-library
- CONFIG=debug test-library
- CONFIG=release test-library
+ CONFIG=release test-library
test-library:
for platform in "$(PLATFORM_IOS)" "$(PLATFORM_MACOS)" "$(PLATFORM_MAC_CATALYST)" "$(PLATFORM_TVOS)" "$(PLATFORM_WATCHOS)"; do \
@@ -42,10 +40,10 @@ test-docs:
&& exit 1)
test-examples:
- for scheme in "CaseStudies (SwiftUI)" "CaseStudies (UIKit)" Search SpeechRecognition TicTacToe Todos VoiceMemos; do \
+ for scheme in "CaseStudies (SwiftUI)" "CaseStudies (UIKit)" Integration Search SpeechRecognition TicTacToe Todos VoiceMemos; do \
xcodebuild test \
-scheme "$$scheme" \
- -destination platform="$(PLATFORM_IOS)"; \
+ -destination platform="$(PLATFORM_IOS)" || exit 1; \
done
benchmark:
diff --git a/Package.resolved b/Package.resolved
index 8fe484e034b4..6fa49342688f 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/combine-schedulers",
"state" : {
- "revision" : "aa3e575929f2bcc5bad012bd2575eae716cbcdf7",
- "version" : "0.8.0"
+ "revision" : "882ac01eb7ef9e36d4467eb4b1151e74fcef85ab",
+ "version" : "0.9.1"
}
},
{
@@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
- "revision" : "9f39744e025c7d377987f30b03770805dcb0bcd1",
- "version" : "1.1.4"
+ "revision" : "4ad606ba5d7673ea60679a61ff867cc1ff8c8e86",
+ "version" : "1.2.1"
}
},
{
@@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-case-paths",
"state" : {
- "revision" : "15bba50ebf3a2065388c8d12210debe4f6ada202",
- "version" : "0.10.0"
+ "revision" : "c3a42e8d1a76ff557cf565ed6d8b0aee0e6e75af",
+ "version" : "0.11.0"
}
},
{
@@ -41,8 +41,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-clocks",
"state" : {
- "revision" : "692ec4f5429a667bdd968c7260dfa2b23adfeffc",
- "version" : "0.1.4"
+ "revision" : "20b25ca0dd88ebfb9111ec937814ddc5a8880172",
+ "version" : "0.2.0"
}
},
{
@@ -50,8 +50,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections",
"state" : {
- "revision" : "f504716c27d2e5d4144fa4794b12129301d17729",
- "version" : "1.0.3"
+ "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
+ "version" : "1.0.4"
}
},
{
@@ -59,8 +59,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-custom-dump",
"state" : {
- "revision" : "819d9d370cd721c9d87671e29d947279292e4541",
- "version" : "0.6.0"
+ "revision" : "ead7d30cc224c3642c150b546f4f1080d1c411a8",
+ "version" : "0.6.1"
+ }
+ },
+ {
+ "identity" : "swift-dependencies",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-dependencies",
+ "state" : {
+ "revision" : "8282b0c59662eb38946afe30eb403663fc2ecf76",
+ "version" : "0.1.4"
}
},
{
@@ -68,7 +77,16 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-docc-plugin",
"state" : {
- "revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6",
+ "revision" : "10bc670db657d11bdd561e07de30a9041311b2b1",
+ "version" : "1.1.0"
+ }
+ },
+ {
+ "identity" : "swift-docc-symbolkit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-docc-symbolkit",
+ "state" : {
+ "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34",
"version" : "1.0.0"
}
},
@@ -77,8 +95,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-identified-collections",
"state" : {
- "revision" : "bfb0d43e75a15b6dfac770bf33479e8393884a36",
- "version" : "0.4.1"
+ "revision" : "fd34c544ad27f3ba6b19142b348005bfa85b6005",
+ "version" : "0.6.0"
}
},
{
@@ -86,8 +104,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swiftui-navigation",
"state" : {
- "revision" : "46acf5ecc1cabdb28d7fe03289f6c8b13a023f52",
- "version" : "0.4.5"
+ "revision" : "bf0fb9d53019cbde1a1e0cf290b560a0a0411282",
+ "version" : "0.6.0"
}
},
{
@@ -95,8 +113,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
- "revision" : "16e6409ee82e1b81390bdffbf217b9c08ab32784",
- "version" : "0.5.0"
+ "revision" : "16b23a295fa322eb957af98037f86791449de60f",
+ "version" : "0.8.1"
}
}
],
diff --git a/Package.swift b/Package.swift
index f857d666690b..639133289eb8 100644
--- a/Package.swift
+++ b/Package.swift
@@ -15,32 +15,30 @@ let package = Package(
name: "ComposableArchitecture",
type: .dynamic,
targets: ["ComposableArchitecture"]
- ),
- .library(
- name: "Dependencies",
- targets: ["Dependencies"]
- ),
+ )
],
dependencies: [
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
.package(url: "https://github.com/google/swift-benchmark", from: "0.1.0"),
.package(url: "https://github.com/pointfreeco/combine-schedulers", from: "0.8.0"),
.package(url: "https://github.com/pointfreeco/swift-case-paths", from: "0.10.0"),
- .package(url: "https://github.com/pointfreeco/swift-clocks", from: "0.1.4"),
+ .package(url: "https://github.com/apple/swift-collections", from: "1.0.2"),
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "0.6.0"),
+ .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "0.1.2"),
.package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "0.4.1"),
- .package(url: "https://github.com/pointfreeco/swiftui-navigation", from: "0.4.5"),
+ .package(url: "https://github.com/pointfreeco/swiftui-navigation", from: "0.6.0"),
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "0.5.0"),
],
targets: [
.target(
name: "ComposableArchitecture",
dependencies: [
- "Dependencies",
.product(name: "CasePaths", package: "swift-case-paths"),
.product(name: "CombineSchedulers", package: "combine-schedulers"),
.product(name: "CustomDump", package: "swift-custom-dump"),
+ .product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "IdentifiedCollections", package: "swift-identified-collections"),
+ .product(name: "OrderedCollections", package: "swift-collections"),
.product(name: "_SwiftUINavigationState", package: "swiftui-navigation"),
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
]
@@ -48,22 +46,8 @@ let package = Package(
.testTarget(
name: "ComposableArchitectureTests",
dependencies: [
- "ComposableArchitecture"
- ]
- ),
- .target(
- name: "Dependencies",
- dependencies: [
- .product(name: "Clocks", package: "swift-clocks"),
- .product(name: "CombineSchedulers", package: "combine-schedulers"),
- .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
- ]
- ),
- .testTarget(
- name: "DependenciesTests",
- dependencies: [
+ "_CAsyncSupport",
"ComposableArchitecture",
- "Dependencies",
]
),
.executableTarget(
@@ -73,6 +57,7 @@ let package = Package(
.product(name: "Benchmark", package: "swift-benchmark"),
]
),
+ .systemLibrary(name: "_CAsyncSupport"),
]
)
diff --git a/README.md b/README.md
index 3a7a964b35d2..192eca463fce 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,9 @@
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpointfreeco%2Fswift-composable-architecture%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/pointfreeco/swift-composable-architecture)
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpointfreeco%2Fswift-composable-architecture%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/pointfreeco/swift-composable-architecture)
-The Composable Architecture (TCA, for short) is a library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind. It can be used in SwiftUI, UIKit, and more, and on any Apple platform (iOS, macOS, tvOS, and watchOS).
+The Composable Architecture (TCA, for short) is a library for building applications in a consistent
+and understandable way, with composition, testing, and ergonomics in mind. It can be used in
+SwiftUI, UIKit, and more, and on any Apple platform (iOS, macOS, tvOS, and watchOS).
* [What is the Composable Architecture?](#what-is-the-composable-architecture)
* [Learn more](#learn-more)
@@ -17,28 +19,40 @@ The Composable Architecture (TCA, for short) is a library for building applicati
## What is the Composable Architecture?
-This library provides a few core tools that can be used to build applications of varying purpose and complexity. It provides compelling stories that you can follow to solve many problems you encounter day-to-day when building applications, such as:
+This library provides a few core tools that can be used to build applications of varying purpose and
+complexity. It provides compelling stories that you can follow to solve many problems you encounter
+day-to-day when building applications, such as:
* **State management**
-
How to manage the state of your application using simple value types, and share state across many screens so that mutations in one screen can be immediately observed in another screen.
+
How to manage the state of your application using simple value types, and share state across
+ many screens so that mutations in one screen can be immediately observed in another screen.
* **Composition**
-
How to break down large features into smaller components that can be extracted to their own, isolated modules and be easily glued back together to form the feature.
+
How to break down large features into smaller components that can be extracted to their own,
+ isolated modules and be easily glued back together to form the feature.
* **Side effects**
-
How to let certain parts of the application talk to the outside world in the most testable and understandable way possible.
+
How to let certain parts of the application talk to the outside world in the most testable
+ and understandable way possible.
* **Testing**
-
How to not only test a feature built in the architecture, but also write integration tests for features that have been composed of many parts, and write end-to-end tests to understand how side effects influence your application. This allows you to make strong guarantees that your business logic is running in the way you expect.
+
How to not only test a feature built in the architecture, but also write integration tests
+ for features that have been composed of many parts, and write end-to-end tests to understand how
+ side effects influence your application. This allows you to make strong guarantees that your
+ business logic is running in the way you expect.
* **Ergonomics**
-
How to accomplish all of the above in a simple API with as few concepts and moving parts as possible.
+
How to accomplish all of the above in a simple API with as few concepts and moving parts as
+ possible.
## Learn More
-The Composable Architecture was designed over the course of many episodes on [Point-Free][pointfreeco], a video series exploring functional programming and the Swift language, hosted by [Brandon Williams][mbrandonw] and [Stephen Celis][stephencelis].
+The Composable Architecture was designed over the course of many episodes on
+[Point-Free][pointfreeco], a video series exploring functional programming and the Swift language,
+hosted by [Brandon Williams][mbrandonw] and [Stephen Celis][stephencelis].
-You can watch all of the episodes [here][tca-episode-collection], as well as a dedicated, [multipart tour][tca-tour] of the architecture from scratch.
+You can watch all of the episodes [here][tca-episode-collection], as well as a dedicated, [multipart
+tour][tca-tour] of the architecture from scratch.
@@ -48,7 +62,8 @@ You can watch all of the episodes [here][tca-episode-collection], as well as a d
[![Screen shots of example applications](https://d3rccdn33rt8ze.cloudfront.net/composable-architecture/demos.png)](./Examples)
-This repo comes with _lots_ of examples to demonstrate how to solve common and complex problems with the Composable Architecture. Check out [this](./Examples) directory to see them all, including:
+This repo comes with _lots_ of examples to demonstrate how to solve common and complex problems with
+the Composable Architecture. Check out [this](./Examples) directory to see them all, including:
* [Case Studies](./Examples/CaseStudies)
* Getting started
@@ -64,22 +79,35 @@ This repo comes with _lots_ of examples to demonstrate how to solve common and c
* [Todos](./Examples/Todos)
* [Voice memos](./Examples/VoiceMemos)
-Looking for something more substantial? Check out the source code for [isowords][gh-isowords], an iOS word search game built in SwiftUI and the Composable Architecture.
+Looking for something more substantial? Check out the source code for [isowords][gh-isowords], an
+iOS word search game built in SwiftUI and the Composable Architecture.
## Basic Usage
-To build a feature using the Composable Architecture you define some types and values that model your domain:
+To build a feature using the Composable Architecture you define some types and values that model
+your domain:
-* **State**: A type that describes the data your feature needs to perform its logic and render its UI.
-* **Action**: A type that represents all of the actions that can happen in your feature, such as user actions, notifications, event sources and more.
-* **Reducer**: A function that describes how to evolve the current state of the app to the next state given an action. The reducer is also responsible for returning any effects that should be run, such as API requests, which can be done by returning an `Effect` value.
-* **Store**: The runtime that actually drives your feature. You send all user actions to the store so that the store can run the reducer and effects, and you can observe state changes in the store so that you can update UI.
+* **State**: A type that describes the data your feature needs to perform its logic and render its
+UI.
+* **Action**: A type that represents all of the actions that can happen in your feature, such as
+user actions, notifications, event sources and more.
+* **Reducer**: A function that describes how to evolve the current state of the app to the next
+state given an action. The reducer is also responsible for returning any effects that should be
+run, such as API requests, which can be done by returning an `Effect` value.
+* **Store**: The runtime that actually drives your feature. You send all user actions to the store
+so that the store can run the reducer and effects, and you can observe state changes in the store
+so that you can update UI.
-The benefits of doing this are that you will instantly unlock testability of your feature, and you will be able to break large, complex features into smaller domains that can be glued together.
+The benefits of doing this are that you will instantly unlock testability of your feature, and you
+will be able to break large, complex features into smaller domains that can be glued together.
-As a basic example, consider a UI that shows a number along with "+" and "−" buttons that increment and decrement the number. To make things interesting, suppose there is also a button that when tapped makes an API request to fetch a random fact about that number and then displays the fact in an alert.
+As a basic example, consider a UI that shows a number along with "+" and "−" buttons that increment
+and decrement the number. To make things interesting, suppose there is also a button that when
+tapped makes an API request to fetch a random fact about that number and then displays the fact in
+an alert.
-To implement this feature we create a new type that will house the domain and behavior of the feature by conforming to `ReducerProtocol`:
+To implement this feature we create a new type that will house the domain and behavior of the
+feature by conforming to `ReducerProtocol`:
```swift
import ComposableArchitecture
@@ -88,7 +116,9 @@ struct Feature: ReducerProtocol {
}
```
-In here we need to define a type for the feature's state, which consists of an integer for the current count, as well as an optional string that represents the title of the alert we want to show (optional because `nil` represents not showing an alert):
+In here we need to define a type for the feature's state, which consists of an integer for the
+current count, as well as an optional string that represents the title of the alert we want to show
+(optional because `nil` represents not showing an alert):
```swift
struct Feature: ReducerProtocol {
@@ -99,7 +129,10 @@ struct Feature: ReducerProtocol {
}
```
-We also need to define a type for the feature's actions. There are the obvious actions, such as tapping the decrement button, increment button, or fact button. But there are also some slightly non-obvious ones, such as the action of the user dismissing the alert, and the action that occurs when we receive a response from the fact API request:
+We also need to define a type for the feature's actions. There are the obvious actions, such as
+tapping the decrement button, increment button, or fact button. But there are also some slightly
+non-obvious ones, such as the action of the user dismissing the alert, and the action that occurs
+when we receive a response from the fact API request:
```swift
struct Feature: ReducerProtocol {
@@ -114,7 +147,10 @@ struct Feature: ReducerProtocol {
}
```
-And then we implement the `reduce` method which is responsible for handling the actual logic and behavior for the feature. It describes how to change the current state to the next state, and describes what effects need to be executed. Some actions don't need to execute effects, and they can return `.none` to represent that:
+And then we implement the `reduce` method which is responsible for handling the actual logic and
+behavior for the feature. It describes how to change the current state to the next state, and
+describes what effects need to be executed. Some actions don't need to execute effects, and they
+can return `.none` to represent that:
```swift
struct Feature: ReducerProtocol {
@@ -160,7 +196,10 @@ struct Feature: ReducerProtocol {
}
```
-And then finally we define the view that displays the feature. It holds onto a `StoreOf` so that it can observe all changes to the state and re-render, and we can send all user actions to the store so that state changes. We must also introduce a struct wrapper around the fact alert to make it `Identifiable`, which the `.alert` view modifier requires:
+And then finally we define the view that displays the feature. It holds onto a `StoreOf`
+so that it can observe all changes to the state and re-render, and we can send all user actions to
+the store so that state changes. We must also introduce a struct wrapper around the fact alert to
+make it `Identifiable`, which the `.alert` view modifier requires:
```swift
struct FeatureView: View {
@@ -194,7 +233,9 @@ struct FactAlert: Identifiable {
}
```
-It is also straightforward to have a UIKit controller driven off of this store. You subscribe to the store in `viewDidLoad` in order to update the UI and show alerts. The code is a bit longer than the SwiftUI version, so we have collapsed it here:
+It is also straightforward to have a UIKit controller driven off of this store. You subscribe to the
+store in `viewDidLoad` in order to update the UI and show alerts. The code is a bit longer than the
+SwiftUI version, so we have collapsed it here:
Click to expand!
@@ -258,7 +299,9 @@ It is also straightforward to have a UIKit controller driven off of this store.
```
-Once we are ready to display this view, for example in the app's entry point, we can construct a store. This can be done by specifying the initial state to start the application in, as well as the reducer that will power the application:
+Once we are ready to display this view, for example in the app's entry point, we can construct a
+store. This can be done by specifying the initial state to start the application in, as well as
+the reducer that will power the application:
```swift
import ComposableArchitecture
@@ -278,13 +321,19 @@ struct MyApp: App {
}
```
-And that is enough to get something on the screen to play around with. It's definitely a few more steps than if you were to do this in a vanilla SwiftUI way, but there are a few benefits. It gives us a consistent manner to apply state mutations, instead of scattering logic in some observable objects and in various action closures of UI components. It also gives us a concise way of expressing side effects. And we can immediately test this logic, including the effects, without doing much additional work.
+And that is enough to get something on the screen to play around with. It's definitely a few more
+steps than if you were to do this in a vanilla SwiftUI way, but there are a few benefits. It gives
+us a consistent manner to apply state mutations, instead of scattering logic in some observable
+objects and in various action closures of UI components. It also gives us a concise way of
+expressing side effects. And we can immediately test this logic, including the effects, without
+doing much additional work.
### Testing
> For more in-depth information on testing, see the dedicated [testing][testing-article] article.
-To test use a `TestStore`, which can be created with the same information as the `Store`, but it does extra work to allow you to assert how your feature evolves as actions are sent:
+To test use a `TestStore`, which can be created with the same information as the `Store`, but it
+does extra work to allow you to assert how your feature evolves as actions are sent:
```swift
@MainActor
@@ -296,7 +345,9 @@ func testFeature() async {
}
```
-Once the test store is created we can use it to make an assertion of an entire user flow of steps. Each step of the way we need to prove that state changed how we expect. For example, we can simulate the user flow of tapping on the increment and decrement buttons:
+Once the test store is created we can use it to make an assertion of an entire user flow of steps.
+Each step of the way we need to prove that state changed how we expect. For example, we can
+simulate the user flow of tapping on the increment and decrement buttons:
```swift
// Test that tapping on the increment/decrement buttons changes the count
@@ -308,7 +359,9 @@ await store.send(.decrementButtonTapped) {
}
```
-Further, if a step causes an effect to be executed, which feeds data back into the store, we must assert on that. For example, if we simulate the user tapping on the fact button we expect to receive a fact response back with the fact, which then causes the alert to show:
+Further, if a step causes an effect to be executed, which feeds data back into the store, we must
+assert on that. For example, if we simulate the user tapping on the fact button we expect to
+receive a fact response back with the fact, which then causes the alert to show:
```swift
await store.send(.numberFactButtonTapped)
@@ -320,9 +373,13 @@ await store.receive(.numberFactResponse(.success(???))) {
However, how do we know what fact is going to be sent back to us?
-Currently our reducer is using an effect that reaches out into the real world to hit an API server, and that means we have no way to control its behavior. We are at the whims of our internet connectivity and the availability of the API server in order to write this test.
+Currently our reducer is using an effect that reaches out into the real world to hit an API server,
+and that means we have no way to control its behavior. We are at the whims of our internet
+connectivity and the availability of the API server in order to write this test.
-It would be better for this dependency to be passed to the reducer so that we can use a live dependency when running the application on a device, but use a mocked dependency for tests. We can do this by adding a property to the `Feature` reducer:
+It would be better for this dependency to be passed to the reducer so that we can use a live
+dependency when running the application on a device, but use a mocked dependency for tests. We can
+do this by adding a property to the `Feature` reducer:
```swift
struct Feature: ReducerProtocol {
@@ -340,7 +397,8 @@ case .numberFactButtonTapped:
}
```
-And in the entry point of the application we can provide a version of the dependency that actually interacts with the real world API server:
+And in the entry point of the application we can provide a version of the dependency that actually
+interacts with the real world API server:
```swift
@main
@@ -362,7 +420,8 @@ struct MyApp: App {
}
```
-But in tests we can use a mock dependency that immediately returns a deterministic, predictable fact:
+But in tests we can use a mock dependency that immediately returns a deterministic, predictable
+fact:
```swift
@MainActor
@@ -376,7 +435,9 @@ func testFeature() async {
}
```
-With that little bit of upfront work we can finish the test by simulating the user tapping on the fact button, receiving the response from the dependency to trigger the alert, and then dismissing the alert:
+With that little bit of upfront work we can finish the test by simulating the user tapping on the
+fact button, receiving the response from the dependency to trigger the alert, and then dismissing
+the alert:
```swift
await store.send(.numberFactButtonTapped)
@@ -390,7 +451,11 @@ await store.send(.factAlertDismissed) {
}
```
-We can also improve the ergonomics of using the `numberFact` dependency in our application. Over time the application may evolve into many features, and some of those features may also want access to `numberFact`, and explicitly passing it through all layers can get annoying. There is a process you can follow to “register” dependencies with the library, making them instantly available to any layer in the application.
+We can also improve the ergonomics of using the `numberFact` dependency in our application. Over
+time the application may evolve into many features, and some of those features may also want access
+to `numberFact`, and explicitly passing it through all layers can get annoying. There is a process
+you can follow to “register” dependencies with the library, making them instantly available to any
+layer in the application.
> For more in-depth information on dependency management, see the dedicated [dependencies][dependencies-article] article.
@@ -425,7 +490,8 @@ extension DependencyValues {
}
```
-With that little bit of upfront work done you can instantly start making use of the dependency in any feature:
+With that little bit of upfront work done you can instantly start making use of the dependency in
+any feature:
```swift
struct Feature: ReducerProtocol {
@@ -436,7 +502,10 @@ struct Feature: ReducerProtocol {
}
```
-This code works exactly as it did before, but you no longer have to explicitly pass the dependency when constructing the feature's reducer. When running the app in previews, the simulator or on a device, the live dependency will be provided to the reducer, and in tests the test dependency will be provided.
+This code works exactly as it did before, but you no longer have to explicitly pass the dependency
+when constructing the feature's reducer. When running the app in previews, the simulator or on a
+device, the live dependency will be provided to the reducer, and in tests the test dependency will
+be provided.
This means the entry point to the application no longer needs to construct dependencies:
@@ -454,46 +523,55 @@ struct MyApp: App {
}
```
-And the test store can be constructed without specifying any dependencies, but you can still override any dependency you need to for the purpose of the test:
+And the test store can be constructed without specifying any dependencies, but you can still
+override any dependency you need to for the purpose of the test:
```swift
let store = TestStore(
initialState: Feature.State(),
reducer: Feature()
-)
-
-store.dependencies.numberFact.fetch = { "\($0) is a good number Brent" }
+) {
+ $0.numberFact.fetch = { "\($0) is a good number Brent" }
+}
…
```
-That is the basics of building and testing a feature in the Composable Architecture. There are _a lot_ more things to be explored, such as composition, modularity, adaptability, and complex effects. The [Examples](./Examples) directory has a bunch of projects to explore to see more advanced usages.
+That is the basics of building and testing a feature in the Composable Architecture. There are
+_a lot_ more things to be explored, such as composition, modularity, adaptability, and complex
+effects. The [Examples](./Examples) directory has a bunch of projects to explore to see more
+advanced usages.
## Documentation
The documentation for releases and `main` are available here:
* [`main`](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture)
-* [0.47.0](https://pointfreeco.github.io/swift-composable-architecture/0.47.0/documentation/composablearchitecture/)
+* [0.50.0](https://pointfreeco.github.io/swift-composable-architecture/0.50.0/documentation/composablearchitecture/)
Other versions
+ * [0.49.0](https://pointfreeco.github.io/swift-composable-architecture/0.49.0/documentation/composablearchitecture/)
+ * [0.48.0](https://pointfreeco.github.io/swift-composable-architecture/0.48.0/documentation/composablearchitecture/)
+ * [0.47.0](https://pointfreeco.github.io/swift-composable-architecture/0.47.0/documentation/composablearchitecture/)
* [0.46.0](https://pointfreeco.github.io/swift-composable-architecture/0.46.0/documentation/composablearchitecture/)
* [0.45.0](https://pointfreeco.github.io/swift-composable-architecture/0.45.0/documentation/composablearchitecture/)
* [0.44.0](https://pointfreeco.github.io/swift-composable-architecture/0.44.0/documentation/composablearchitecture/)
* [0.43.0](https://pointfreeco.github.io/swift-composable-architecture/0.43.0/documentation/composablearchitecture/)
* [0.42.0](https://pointfreeco.github.io/swift-composable-architecture/0.42.0/documentation/composablearchitecture/)
- * [0.41.2](https://pointfreeco.github.io/swift-composable-architecture/0.41.0/documentation/composablearchitecture/)
- * [0.40.2](https://pointfreeco.github.io/swift-composable-architecture/0.40.0/documentation/composablearchitecture/)
+ * [0.41.0](https://pointfreeco.github.io/swift-composable-architecture/0.41.0/documentation/composablearchitecture/)
+ * [0.40.0](https://pointfreeco.github.io/swift-composable-architecture/0.40.0/documentation/composablearchitecture/)
* [0.39.0](https://pointfreeco.github.io/swift-composable-architecture/0.39.0/documentation/composablearchitecture/)
+ * [0.38.0](https://pointfreeco.github.io/swift-composable-architecture/0.38.0/documentation/composablearchitecture/)
-There are a number of articles in the documentation that you may find helpful as you become more comfortable with the library:
+There are a number of articles in the documentation that you may find helpful as you become more
+comfortable with the library:
* [Getting started][getting-started-article]
* [Dependency management][dependencies-article]
@@ -508,14 +586,23 @@ There are a number of articles in the documentation that you may find helpful as
You can add ComposableArchitecture to an Xcode project by adding it as a package dependency.
1. From the **File** menu, select **Add Packages...**
- 2. Enter "https://github.com/pointfreeco/swift-composable-architecture" into the package repository URL text field
+ 2. Enter "https://github.com/pointfreeco/swift-composable-architecture" into the package
+ repository URL text field
3. Depending on how your project is structured:
- - If you have a single application target that needs access to the library, then add **ComposableArchitecture** directly to your application.
- - If you want to use this library from multiple Xcode targets, or mix Xcode targets and SPM targets, you must create a shared framework that depends on **ComposableArchitecture** and then depend on that framework in all of your targets. For an example of this, check out the [Tic-Tac-Toe](./Examples/TicTacToe) demo application, which splits lots of features into modules and consumes the static library in this fashion using the **tic-tac-toe** Swift package.
+ - If you have a single application target that needs access to the library, then add
+ **ComposableArchitecture** directly to your application.
+ - If you want to use this library from multiple Xcode targets, or mix Xcode targets and SPM
+ targets, you must create a shared framework that depends on **ComposableArchitecture** and
+ then depend on that framework in all of your targets. For an example of this, check out the
+ [Tic-Tac-Toe](./Examples/TicTacToe) demo application, which splits lots of features into
+ modules and consumes the static library in this fashion using the **tic-tac-toe** Swift
+ package.
## Help
-If you want to discuss the Composable Architecture or have a question about how to use it to solve a particular problem, you can start a topic in the [discussions][gh-discussions] tab of this repo, or ask around on [its Swift forum][swift-forum].
+If you want to discuss the Composable Architecture or have a question about how to use it to solve
+a particular problem, you can start a topic in the [discussions][gh-discussions] tab of this repo,
+or ask around on [its Swift forum][swift-forum].
## Translations
@@ -531,37 +618,61 @@ The following translations of this README have been contributed by members of th
* [Simplified Chinese](https://gist.github.com/sh3l6orrr/10c8f7c634a892a9c37214f3211242ad)
* [Spanish](https://gist.github.com/pitt500/f5e32fccb575ce112ffea2827c7bf942)
-If you'd like to contribute a translation, please [open a PR](https://github.com/pointfreeco/swift-composable-architecture/edit/main/README.md) with a link to a [Gist](https://gist.github.com)!
+If you'd like to contribute a translation, please [open a
+PR](https://github.com/pointfreeco/swift-composable-architecture/edit/main/README.md) with a link
+to a [Gist](https://gist.github.com)!
## FAQ
* How does the Composable Architecture compare to Elm, Redux, and others?
Expand to see answer
- The Composable Architecture (TCA) is built on a foundation of ideas popularized by the Elm Architecture (TEA) and Redux, but made to feel at home in the Swift language and on Apple's platforms.
-
- In some ways TCA is a little more opinionated than the other libraries. For example, Redux is not prescriptive with how one executes side effects, but TCA requires all side effects to be modeled in the `Effect` type and returned from the reducer.
-
- In other ways TCA is a little more lax than the other libraries. For example, Elm controls what kinds of effects can be created via the `Cmd` type, but TCA allows an escape hatch to any kind of effect since `Effect` conforms to the Combine `Publisher` protocol.
-
- And then there are certain things that TCA prioritizes highly that are not points of focus for Redux, Elm, or most other libraries. For example, composition is very important aspect of TCA, which is the process of breaking down large features into smaller units that can be glued together. This is accomplished with reducer builders and operators like `Scope`, and it aids in handling complex features as well as modularization for a better-isolated code base and improved compile times.
+ The Composable Architecture (TCA) is built on a foundation of ideas popularized by the Elm
+ Architecture (TEA) and Redux, but made to feel at home in the Swift language and on Apple's
+ platforms.
+
+ In some ways TCA is a little more opinionated than the other libraries. For example, Redux is
+ not prescriptive with how one executes side effects, but TCA requires all side effects to be
+ modeled in the `Effect` type and returned from the reducer.
+
+ In other ways TCA is a little more lax than the other libraries. For example, Elm controls what
+ kinds of effects can be created via the `Cmd` type, but TCA allows an escape hatch to any kind
+ of effect since `Effect` conforms to the Combine `Publisher` protocol.
+
+ And then there are certain things that TCA prioritizes highly that are not points of focus for
+ Redux, Elm, or most other libraries. For example, composition is very important aspect of TCA,
+ which is the process of breaking down large features into smaller units that can be glued
+ together. This is accomplished with reducer builders and operators like `Scope`, and it aids in
+ handling complex features as well as modularization for a better-isolated code base and improved
+ compile times.
## Credits and thanks
-The following people gave feedback on the library at its early stages and helped make the library what it is today:
+The following people gave feedback on the library at its early stages and helped make the library
+what it is today:
-Paul Colton, Kaan Dedeoglu, Matt Diephouse, Josef Doležal, Eimantas, Matthew Johnson, George Kaimakas, Nikita Leonov, Christopher Liscio, Jeffrey Macko, Alejandro Martinez, Shai Mishali, Willis Plummer, Simon-Pierre Roy, Justin Price, Sven A. Schmidt, Kyle Sherman, Petr Šíma, Jasdev Singh, Maxim Smirnov, Ryan Stone, Daniel Hollis Tavares, and all of the [Point-Free][pointfreeco] subscribers 😁.
+Paul Colton, Kaan Dedeoglu, Matt Diephouse, Josef Doležal, Eimantas, Matthew Johnson, George
+Kaimakas, Nikita Leonov, Christopher Liscio, Jeffrey Macko, Alejandro Martinez, Shai Mishali, Willis
+Plummer, Simon-Pierre Roy, Justin Price, Sven A. Schmidt, Kyle Sherman, Petr Šíma, Jasdev Singh,
+Maxim Smirnov, Ryan Stone, Daniel Hollis Tavares, and all of the [Point-Free][pointfreeco]
+subscribers 😁.
-Special thanks to [Chris Liscio](https://twitter.com/liscio) who helped us work through many strange SwiftUI quirks and helped refine the final API.
+Special thanks to [Chris Liscio](https://twitter.com/liscio) who helped us work through many strange
+SwiftUI quirks and helped refine the final API.
-And thanks to [Shai Mishali](https://github.com/freak4pc) and the [CombineCommunity](https://github.com/CombineCommunity/CombineExt/) project, from which we took their implementation of `Publishers.Create`, which we use in `Effect` to help bridge delegate and callback-based APIs, making it much easier to interface with 3rd party frameworks.
+And thanks to [Shai Mishali](https://github.com/freak4pc) and the
+[CombineCommunity](https://github.com/CombineCommunity/CombineExt/) project, from which we took
+their implementation of `Publishers.Create`, which we use in `Effect` to help bridge delegate and
+callback-based APIs, making it much easier to interface with 3rd party frameworks.
## Other libraries
-The Composable Architecture was built on a foundation of ideas started by other libraries, in particular [Elm](https://elm-lang.org) and [Redux](https://redux.js.org/).
+The Composable Architecture was built on a foundation of ideas started by other libraries, in
+particular [Elm](https://elm-lang.org) and [Redux](https://redux.js.org/).
-There are also many architecture libraries in the Swift and iOS community. Each one of these has their own set of priorities and trade-offs that differ from the Composable Architecture.
+There are also many architecture libraries in the Swift and iOS community. Each one of these has
+their own set of priorities and trade-offs that differ from the Composable Architecture.
* [RIBs](https://github.com/uber/RIBs)
* [Loop](https://github.com/ReactiveCocoa/Loop)
diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/Bindings.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/Bindings.md
index 47b11825acae..7a409b98d487 100644
--- a/Sources/ComposableArchitecture/Documentation.docc/Articles/Bindings.md
+++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/Bindings.md
@@ -177,20 +177,20 @@ struct Settings: ReducerProtocol {
```
This is a _lot_ of boilerplate for something that should be simple. Luckily, we can dramatically
-eliminate this boilerplate using ``BindableState``, ``BindableAction``, and ``BindingReducer``.
+eliminate this boilerplate using ``BindingState``, ``BindableAction``, and ``BindingReducer``.
-First, we can annotate each bindable value of state with the ``BindableState`` property wrapper:
+First, we can annotate each bindable value of state with the ``BindingState`` property wrapper:
```swift
struct Settings: ReducerProtocol {
struct State: Equatable {
- @BindableState var digest = Digest.daily
- @BindableState var displayName = ""
- @BindableState var enableNotifications = false
+ @BindingState var digest = Digest.daily
+ @BindingState var displayName = ""
+ @BindingState var enableNotifications = false
var isLoading = false
- @BindableState var protectMyPosts = false
- @BindableState var sendEmailNotifications = false
- @BindableState var sendMobileNotifications = false
+ @BindingState var protectMyPosts = false
+ @BindingState var sendEmailNotifications = false
+ @BindingState var sendMobileNotifications = false
}
// ...
diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/DependencyManagement.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/DependencyManagement.md
index a5576234ff9f..6d457d16d6bf 100644
--- a/Sources/ComposableArchitecture/Documentation.docc/Articles/DependencyManagement.md
+++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/DependencyManagement.md
@@ -15,443 +15,16 @@ alter the execution context a feature runs in. This means in tests and Xcode pre
provide a mock version of an API client that immediately returns some stubbed data rather than
making a live network request to a server.
-* [The need for controlled dependencies](#The-need-for-controlled-dependencies)
-* [Using library dependencies](#Using-library-dependencies)
-* [Registering your own dependencies](#Registering-your-own-dependencies)
-* [Live, preview and test dependencies](#Live-preview-and-test-dependencies)
-* [Designing dependencies](#Designing-dependencies)
-* [Overriding dependencies](#Overriding-dependencies)
-
-## The need for controlled dependencies
-
-Suppose that you are building a todo application with a `Todo` model that has a UUID identifier:
-
-```swift
-struct Todo: Equatable, Identifiable {
- let id: UUID
- var title = ""
- var isCompleted = false
-}
-```
-
-And suppose you have a reducer that handles an action for when the "Add todo" button is tapped,
-which appends a new todo to the end of the array:
-
-```swift
-struct Todos: ReducerProtocol {
- struct State {
- var todos: IdentifiedArrayOf = []
- // ...
- }
- enum Action {
- case addButtonTapped
- // ...
- }
-
- func reduce(into state: inout State, action: Action) -> EffectTask {
- switch action {
- case .addButtonTapped:
- state.todos.append(Todo(id: UUID())
- return .none
-
- // ...
- }
- }
-}
-```
-
-> Tip: We are using `IdentifiedArray` from our
-[Identified Collections][swift-identified-collections] library because it provides a safe and
-ergonomic API for accessing elements from a stable ID rather than positional indices.
-
-In the reducer we are using the uncontrolled `UUID` initializer from Foundation. Every invocation
-of the initializer produces a fully random UUID. That may seem like what we want, but unfortunately
-it wreaks havoc on our ability to test.
-
-If we tried writing a test for the add todo functionality we will quickly find that we can't
-possibly predict what UUID will be produced for the new todo:
-
-```swift
-@MainActor
-func testAddTodo() async {
- let store = TestStore(
- initialState: Todos.State(),
- reducer: Todos()
- )
-
- await store.send(.addButtonTapped) {
- $0.todos = [
- Todo(id: ???)
- ]
- }
-}
-```
-
-> Tip: Read the article to learn how to write tests for state mutations and effect
-> execution in your features.
-
-There is no way to get this test to pass.
-
-This is why controlling dependencies is important. It allows us to substitute a UUID generator that
-is deterministic in tests, such as one that simply increments by 1 every time it is invoked.
-
-The library comes with a controlled UUID generator and can be accessed by using the
-`@Dependency` property wrapper to add a dependency to the `Todos` reducer:
-
-```swift
-struct Todos: ReducerProtocol {
- @Dependency(\.uuid) var uuid
- // ...
-}
-```
-
-Then when you need a new UUID you should reach for the dependency rather than reaching for the
-uncontrollable UUID initializer:
-
-```swift
-case .addButtonTapped:
- state.todos.append(Todo(id: self.uuid()) // ⬅️
- return .none
-```
-
-If you do this little bit of upfront work you instantly unlock the ability to test the feature by
-providing a controlled, deterministic version of the UUID generator in tests. The library even comes
-with such a version for the UUID generator, and it is called `incrementing`. You can override
-the dependency directly on the ``TestStore`` so that your feature's reducer uses that version
-instead of the live one:
-
-```swift
-@MainActor
-func testAddTodo() async {
- let store = TestStore(
- initialState: Todos.State(),
- reducer: Todos()
- )
-
- store.dependencies.uuid = .incrementing
-
- await store.send(.addButtonTapped) {
- $0.todos = [
- Todo(id: UUID(string: "00000000-0000-0000-0000-000000000000")!)
- ]
- }
-}
-```
-
-This test will pass deterministically, 100% of the time, and this is why it is so important to
-control dependencies that interact with outside systems.
-
-## Using library dependencies
-
-The library comes with many common dependencies that can be used in a controllable manner, such as
-date generators, clocks, random number generators, UUID generators, and more.
-
-For example, suppose you have a feature that needs access to a date initializer, the continuous
-clock for time-based asynchrony, and a UUID initializer. All 3 dependencies can be added to your
-feature's reducer:
-
-```swift
-struct Todos: ReducerProtocol {
- struct State {
- // ...
- }
- enum Action {
- // ...
- }
- @Dependency(\.date) var date
- @Dependency(\.continuousClock) var clock
- @Dependency(\.uuid) var uuid
-
- // ...
-}
-```
-
-Then, all 3 dependencies can easily be overridden with deterministic versions when testing the
-feature:
-
-```swift
-@MainActor
-func testTodos() async {
- let store = TestStore(
- initialState: Todos.State(),
- reducer: Todos()
- )
-
- store.dependencies.date = .constant(Date(timeIntervalSinceReferenceDate: 1234567890))
- store.dependencies.continuousClock = ImmediateClock()
- store.dependencies.uuid = .incrementing
-
- // ...
-}
-```
-
-## Registering your own dependencies
-
-Although the library comes with many controllable dependencies out of the box, there are still
-times when you want to register your own dependencies with the library so that you can use the
-`@Dependency` property wrapper. Doing this is quite similar to registering an
-[environment value][environment-values-docs] in SwiftUI.
-
-First you create a type that conforms to the `DependencyKey` protocol. The minimum implementation
-you must provide is a `liveValue`, which is the value used when running the app in a simulator or
-on device, and so it's appropriate for it to actually make network requests to an external server:
-
-```swift
-private enum APIClientKey: DependencyKey {
- static let liveValue = APIClient.live
-}
-```
-
-> Tip: There are two other values you can provide for a dependency. If you implement `testValue`
-it will be used when testing features in a ``TestStore``, and if you implement `previewValue` it
-will be used while running features in an Xcode preview. You don't need to worry about those
-values when you are just getting started, and instead can
-[add them later](#Live-preview-and-test-dependencies).
-
-Finally, an extension must be made to `DependencyValues` to expose a computed property for the
-dependency:
-
-```swift
-extension DependencyValues {
- var apiClient: APIClient {
- get { self[APIClientKey.self] }
- set { self[APIClientKey.self] = newValue }
- }
-}
-```
-
-With those few steps completed you can instantly access your API client dependency from any
-feature's reducer by using the `@Dependency` property wrapper:
-
-```swift
-struct Todos: ReducerProtocol {
- @Dependency(\.apiClient) var apiClient
- // ...
-}
-```
-
-This will automatically use the live dependency in previews, simulators and devices, and in
-tests you can override any endpoint of the dependency to return mock data:
-
-```swift
-@MainActor
-func testFetchUser() async {
- let store = TestStore(
- initialState: Todos.State(),
- reducer: Todos()
- )
-
- store.dependencies.apiClient.fetchUser = { _ in User(id: 1, name: "Blob") }
-
- await store.send(.loadButtonTapped)
- await store.receive(.userResponse(.success(User(id: 1, name: "Blob")))) {
- $0.loadedUser = User(id: 1, name: "Blob")
- }
-}
-```
-
-Often times it is not necessary to create a whole new type to conform to `DependencyKey`. If the
-dependency you are registering is a type that you own, then you can conform it directly to the
-protocol:
-
-```swift
-extension APIClient: DependencyKey {
- static let liveValue = APIClient.live
-}
-
-extension DependencyValues {
- var apiClient: APIClient {
- get { self[APIClient.self] }
- set { self[APIClient.self] = newValue }
- }
-}
-```
-
-That can save a little bit of boilerplate.
-
-## Live, preview and test dependencies
-
-In the previous section we showed that to conform to `DependencyKey` you must provide _at least_
-a `liveValue`, which is the default version of the dependency that is used when running on a
-device or simulator. The `DependencyKey` protocol inherits from a base protocol,
-`TestDependencyKey`, which has 2 other requirements, `testValue` and `previewValue`. Both are
-optional and delegate to `liveValue` if not implemented.
-
-If you implement a static `testValue` property on your key, that value will be used when running
-your feature in a ``TestStore``. This is a great opportunity to supply a mocked version of the
-dependency that does not reach out to the real world. By doing this you can guarantee that your
-tests will never accidentally make a network request, or track analytics events that are not
-actually tied to user actions, and more.
-
-Further, we highly recommend you consider making your `testValue` dependency into what we like to
-call an "unimplemented" dependency. This is a version of your dependency that performs an `XCTFail`
-in each endpoint so that if it is ever invoked in tests it will cause a test failure. This allows
-you to be more explicit about what dependencies are actually needed to test a particular user
-flow in your feature.
-
-For example, suppose you have an API client with endpoints for fetching a list of users or fetching
-a particular user by id:
-
-```swift
-struct APIClient {
- var fetchUser: (User.ID) async throws -> User
- var fetchUsers: () async throws -> [User]
-}
-```
-
-Then we can construct an "unimplemented" version of this dependency that invokes `XCTFail` when
-any endpoint is invoked
-
-```swift
-extension APIClient {
- static let unimplemented = Self(
- fetchUser: { _ in XCTFail("APIClient.fetchUser unimplemented") }
- fetchUsers: { XCTFail("APIClient.fetchUsers unimplemented") }
- )
-}
-```
-
-Unfortunately, `XCTFail` cannot be used in non-test targets, and so this instance cannot be defined
-in the same file where your dependency is registered. To work around this you can use our
-[XCTestDynamicOverlay][xctest-dynamic-overlay-gh] library that dynamically invokes `XCTFail` and
-it is automatically accessible when using the Composable Architecture. It also comes with some
-helpers to ease the construction of these unimplemented values, which we can use when defining the
-`testValue` of your dependency:
-
-```swift
-import XCTestDynamicOverlay
-
-extension APIClient {
- static let testValue = Self(
- fetchUser: unimplemented("APIClient.fetchUser")
- fetchUsers: unimplemented("APIClient.fetchUsers")
- )
-}
-```
-
-The other requirement of `TestDependencyKey` is `previewValue`, and if this value is implemented
-it will be used whenever your feature is run in an Xcode preview. Previews are similar to tests in
-that you usually do not want to interact with the outside world, such as making network requests.
-In fact, many of Apple's frameworks do not work in previews, such as Core Location, and so it will
-be hard to interact with your feature in previews if it touches those frameworks.
-
-However, previews are dissimilar to tests in that it's fine for dependencies to return some mock
-data. There's no need to deal with "unimplemented" clients for proving which dependencies are
-actually used.
-
-For the `APIClient` example from above, we might define its `previewValue` like so:
-
-```swift
-extension APIClient: TestDependencyKey {
- static let previewValue = Self(
- fetchUsers: {
- [
- User(id: 1, name: "Blob"),
- User(id: 1, name: "Blob Jr."),
- User(id: 1, name: "Blob Sr."),
- ]
- },
- fetchUser: { id in
- User(id: id, name: "Blob, id: \(id)")
- }
- )
-}
-```
-
-Then when running a feature that uses this dependency in an Xcode preview will immediately get
-data provided to it, making it easier for you to iterate on your feature's logic and styling.
-
-## Designing dependencies
-
-Making it possible to control your dependencies is the most important step you can take towards
-making your features isolatable and testable. The second most important step after that is to
-design your dependencies in a way that maximizes their flexibility in tests and other situations.
-
-The most popular way to design dependencies in Swift is to use protocols. For example, if your
-feature needs to interact with an audio player, you might design a protocol with methods for
-playing, stopping, and more:
-
-```swift
-protocol AudioPlayer {
- func loop(_ url: URL) async throws
- func play(_ url: URL) async throws
- func setVolume(_ volume: Float) async
- func stop() async
-}
-```
-
-Then you are free to make as many conformances of this protocol as you want, such as a
-`LiveAudioPlayer` that actually interacts with AVFoundation, or a `MockAudioPlayer` that doesn't
-play any sounds, but does suspend in order to simulate that something is playing. You could even
-have an `UnimplementedAudioPlayer` conformance that invokes `XCTFail` when any method is invoked.
-And all of those conformances can be used to specify the live, preview and test values for the
-dependency:
-
-```swift
-private enum AudioPlayerKey: DependencyKey {
- static let liveValue: any AudioPlayer = LiveAudioPlayer()
- static let previewValue: any AudioPlayer = MockAudioPlayer()
- static let testValue: any AudioPlayer = UnimplementedAudioPlayer()
-}
-```
-
-This style of dependencies works just fine, and if it is what you are most comfortable with then
-there is no need to change.
-
-However, there is a small change one can make to this dependency to unlock even more power. Rather
-than designing the audio player as a protocol, we can use a struct with closure properties to
-represent the interface:
-
-```swift
-struct AudioPlayerClient {
- var loop: (_ url: URL) async throws -> Void
- var play: (_ url: URL) async throws -> Void
- var setVolume: (_ volume: Float) async -> Void
- var stop: () async -> Void
-}
-```
-
-Then, rather than defining types that conform to the protocol you construct values:
-
-```swift
-extension AudioPlayerClient {
- static let live = Self(…)
- static let mock = Self(…)
- static let unimplemented = Self(…)
-}
-```
-
-And to register the dependency you can leverage the struct that defines the interface. There's no
-need to define a new type:
-
-```swift
-extension AudioPlayerClient: DependencyKey {
- static let liveValue = AudioPlayerClient.live
- static let previewValue = AudioPlayerClient.mock
- static let testValue = AudioPlayerClient.unimplemented
-}
-```
-
-If you design your dependencies in this way you can pick which dependency endpoints you need in your
-feature. For example, if you have a feature that needs an audio player to do its job, but it only
-needs the `play` endpoint, and doesn't need to loop, set volume or stop audio, then you can specify
-a dependency on just that one function:
-
-```swift
-struct Feature: ReducerProtocol {
- @Dependency(\.audioPlayer.play) var play
- // …
-}
-```
-
-This can allow your features to better describe the minimal interface they need from dependencies,
-which can help a feature to seem less intimidating.
+> Note: The dependency management system in the Composable Architecture is driven off of our
+> [Dependencies][swift-dependencies-gh] library. That repository has extensive
+> [documentation][swift-deps-docs] and articles, and we highly recommend you familiarize yourself
+> with all of that content to best leverage dependencies.
## Overriding dependencies
It is possible to change the dependencies for just one particular reducer inside a larger composed
-reducer. This can be handy when running a feature in a more controlled environment where it may not be
-appropriate to communicate with the outside world.
+reducer. This can be handy when running a feature in a more controlled environment where it may not
+be appropriate to communicate with the outside world.
For example, suppose you want to teach users how to use your feature through an onboarding
experience. In such an experience it may not be appropriate for the user's actions to cause
@@ -481,3 +54,5 @@ as any reducer `Feature` uses under the hood, _and_ any effects produced by `Fea
[swift-identified-collections]: https://github.com/pointfreeco/swift-identified-collections
[environment-values-docs]: https://developer.apple.com/documentation/swiftui/environmentvalues
[xctest-dynamic-overlay-gh]: http://github.com/pointfreeco/xctest-dynamic-overlay
+[swift-dependencies-gh]: http://github.com/pointfreeco/swift-dependencies
+[swift-deps-docs]: https://pointfreeco.github.io/swift-dependencies/main/documentation/dependencies/
diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/GettingStarted.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/GettingStarted.md
index 68fbeee8ef3f..41387dcb6746 100644
--- a/Sources/ComposableArchitecture/Documentation.docc/Articles/GettingStarted.md
+++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/GettingStarted.md
@@ -331,7 +331,7 @@ Then we can use it in the `reduce` implementation:
```swift
case .numberFactButtonTapped:
return .task { [count = state.count] in
- await .numberFactResponse(TaskResult { try wait self.numberFact(count) })
+ await .numberFactResponse(TaskResult { try await self.numberFact(count) })
}
```
@@ -465,9 +465,9 @@ override any dependency you need to for the purpose of the test:
let store = TestStore(
initialState: Feature.State(),
reducer: Feature()
-)
-
-store.dependencies.numberFact.fetch = { "\($0) is a good number Brent" }
+) {
+ $0.numberFact.fetch = { "\($0) is a good number Brent" }
+}
await store.send(.numberFactButtonTapped)
await store.receive(.numberFactResponse(.success("0 is a good number Brent"))) {
diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/MigratingToTheReducerProtocol.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/MigratingToTheReducerProtocol.md
index 2611edae556e..320f08a7c073 100644
--- a/Sources/ComposableArchitecture/Documentation.docc/Articles/MigratingToTheReducerProtocol.md
+++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/MigratingToTheReducerProtocol.md
@@ -251,10 +251,10 @@ struct AppReducer: ReducerProtocol {
Scope(state: \.tabA, action: /Action.tabA) {
TabA()
}
- Scope(state: \.tabB, action: /Action.tabC) {
+ Scope(state: \.tabB, action: /Action.tabB) {
TabB()
}
- Scope(state: \.tabB, action: /Action.tabC) {
+ Scope(state: \.tabC, action: /Action.tabC) {
TabC()
}
}
@@ -492,10 +492,11 @@ But this means that you must explicitly thread all dependencies from the root of
through to every child feature. This can be arduous and make it difficult to add, remove or change
dependencies.
-The library comes with a tool for managing dependencies in a more ergonomic manner, and even comes
-with some common dependencies pre-integrated allowing you to access them with no additional work.
-For example, the `date` dependency ships with the library so that you can declare your feature's
-dependence on that functionality in the following way:
+The Composable Architecture now uses the [Dependencies][swift-dependencies] library to manage
+dependencies in a more ergonomic manner, and even comes with some common dependencies pre-integrated
+allowing you to access them with no additional work. For example, the `date` dependency ships with
+the library so that you can declare your feature's dependence on that functionality in the following
+way:
```swift
struct Feature: ReducerProtocol {
@@ -508,6 +509,17 @@ struct Feature: ReducerProtocol {
With that one declaration you can stop explicitly passing the date dependency through every layer
of your application. A date function will be automatically provided to your feature's reducer.
+> Important: [Dependencies][swift-dependencies] is powered by Swift task locals and is intended to
+> be used in structured contexts. If your reducer's effects make use of escaping closures, then
+> you must do additional work to propagate the dependencies to that context. For example, using
+> a dependency from within a Combine operator such as `.map`, `.flatMap` and even `.filter` will
+> use the default dependency value.
+>
+> See the [Dependencies documentation][swift-dependencies-docs] on
+> [Dependency lifetimes][swift-dependencies-docs-lifetimes] for more information, and how to
+> integrate the `@Dependency` property wrapper into pre-structured concurrency using the
+> `withEscapedDependencies` function.
+
For domain-specific dependencies you can perform a little bit of upfront work to register your
dependency with the system, and then it will be automatically available to every layer in your
application:
@@ -538,6 +550,10 @@ struct Feature: ReducerProtocol {
For more information on designing your dependencies and providing live and test dependencies, see
our article.
+[swift-dependencies]: https://github.com/pointfreeco/swift-dependencies
+[swift-dependencies-docs]: https://pointfreeco.github.io/swift-dependencies/main/documentation/dependencies/
+[swift-dependencies-docs-lifetimes]: https://pointfreeco.github.io/swift-dependencies/main/documentation/dependencies/lifetimes
+
## Stores
Stores can be initialized from an initial state and an instance of a type conforming to
@@ -577,11 +593,25 @@ By default test stores will employ "test" dependencies wherever a dependency is
reducer via the `@Dependency` property wrapper.
Instead of passing an environment of test dependencies to the store, or mutating the store's
-``TestStore/environment``, you will instead mutate the test store's ``TestStore/dependencies`` to
+``TestStore/environment``, you can either provide a trailing closure when initializing ``TestStore``
+or you can directly mutate the test store's ``TestStore/dependencies`` to
override dependencies driving a feature.
For example, to install a test clock as the continuous clock dependency you can do the following:
+```swift
+let clock = TestClock()
+
+let store = TestStore(
+ initialState: Feature.State(),
+ reducer: Feature()
+) {
+ $0.continuousClock = .clock
+}
+```
+
+…or you can do:
+
```swift
let clock = TestClock()
store.dependencies.continuousClock = clock
diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/Performance.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/Performance.md
index 9343da8ee45a..a9b3e585305a 100644
--- a/Sources/ComposableArchitecture/Documentation.docc/Articles/Performance.md
+++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/Performance.md
@@ -236,15 +236,15 @@ struct Feature: ReducerProtocol {
switch action {
case .buttonTapped:
state.count += 1
- return EffectTask(value: .sharedComputation)
+ return .send(.sharedComputation)
case .toggleChanged:
state.isEnabled.toggle()
- return EffectTask(value: .sharedComputation)
+ return .send(.sharedComputation)
case let .textFieldChanged(text):
state.description = text
- return EffectTask(value: .sharedComputation)
+ return .send(.sharedComputation)
case .sharedComputation:
// Some shared work to compute something.
diff --git a/Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md b/Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md
index dc8b78590083..e1ac22c6660c 100644
--- a/Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md
+++ b/Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md
@@ -10,6 +10,7 @@ but also how effects are executed and feed data back into the system.
* [Testing state changes][Testing-state-changes]
* [Testing effects][Testing-effects]
* [Non-exhaustive testing][Non-exhaustive-testing]
+* [Testing gotchas](#Testing-gotchas)
## Testing state changes
@@ -365,9 +366,9 @@ as an immediate clock that does not suspend at all when you ask it to sleep:
let store = TestStore(
initialState: Feature.State(count: 0),
reducer: Feature()
-)
-
-store.dependencies.continuousClock = ImmediateClock()
+) {
+ $0.continuousClock = ImmediateClock()
+}
```
With that small change we can drop the `timeout` arguments from the
@@ -553,6 +554,51 @@ The test still passes, and none of these notifications are test failures. They j
what things you are not explicitly asserting against, and can be useful to see when tracking down
bugs that happen in production but that aren't currently detected in tests.
+## Testing gotchas
+
+This is not well known, but when an application target runs tests it actually boots up a simulator
+and runs your actual application entry point in the simulator. This means while tests are running,
+your application's code is separately also running. This can be a huge gotcha because it means you
+may be unknowingly making network requests, tracking analytics, writing data to user defaults or
+to the disk, and more.
+
+This usually flies under the radar and you just won't know it's happening, which can be problematic.
+But, once you start using this library and start controlling your dependencies, the problem can
+surface in a very visible manner. Typically, when a dependency is used in a test context without
+being overridden, a test failure occurs. This makes it possible for your test to pass successfully,
+yet for some mysterious reason the test suite fails. This happens because the code in the _app
+host_ is now running in a test context, and accessing dependencies will cause test failures.
+
+This only happens when running tests in a _application target_, that is, a target that is
+specifically used to launch the application for a simulator or device. This does not happen when
+running tests for frameworks or SPM libraries, which is yet another good reason to modularize
+your code base.
+
+However, if you aren't in a position to modularize your code base right now, there is a quick
+fix. Our [XCTest Dynamic Overlay][xctest-dynamic-overlay-gh] library, which is transitively included
+with this library, comes with a property you can check to see if tests are currently running. If
+they are, you can omit the entire entry point of your application:
+
+```swift
+import SwiftUI
+import XCTestDynamicOverlay
+
+@main
+struct MyApp: App {
+ var body: some Scene {
+ WindowGroup {
+ if !_XCTIsTesting {
+ // Your real root view
+ }
+ }
+ }
+}
+```
+
+That will allow tests to run in the application target without your actual application code
+interfering.
+
+[xctest-dynamic-overlay-gh]: http://github.com/pointfreeco/xctest-dynamic-overlay
[Testing-state-changes]: #Testing-state-changes
[Testing-effects]: #Testing-effects
[gh-combine-schedulers]: http://github.com/pointfreeco/combine-schedulers
diff --git a/Sources/ComposableArchitecture/Documentation.docc/ComposableArchitecture.md b/Sources/ComposableArchitecture/Documentation.docc/ComposableArchitecture.md
index f3cdd2a635c5..b899e4932351 100644
--- a/Sources/ComposableArchitecture/Documentation.docc/ComposableArchitecture.md
+++ b/Sources/ComposableArchitecture/Documentation.docc/ComposableArchitecture.md
@@ -73,7 +73,6 @@ day-to-day when building applications, such as:
-
- ``TestStore``
-- ``ActorIsolated``
## See Also
diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Effect.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Effect.md
index a65ca97b82a3..24dbc3e27df5 100644
--- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/Effect.md
+++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/Effect.md
@@ -8,6 +8,7 @@
- ``EffectPublisher/task(priority:operation:catch:file:fileID:line:)``
- ``EffectPublisher/run(priority:operation:catch:file:fileID:line:)``
- ``EffectPublisher/fireAndForget(priority:_:)``
+- ``EffectPublisher/send(_:)``
- ``TaskResult``
### Cancellation
diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectSend.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectSend.md
new file mode 100644
index 000000000000..132f94b7f0ac
--- /dev/null
+++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectSend.md
@@ -0,0 +1,7 @@
+# ``ComposableArchitecture/EffectPublisher/send(_:)``
+
+## Topics
+
+### Animating actions
+
+- ``EffectPublisher/send(_:animation:)``
diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/SwiftUI.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/SwiftUI.md
index a1f14953c60f..f952185143d3 100644
--- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/SwiftUI.md
+++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/SwiftUI.md
@@ -19,7 +19,7 @@ The Composable Architecture can be used to power applications built in many fram
-
- ``ViewStore/binding(get:send:)-65xes``
-- ``BindableState``
+- ``BindingState``
- ``BindableAction``
- ``BindingAction``
- ``BindingReducer``
diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStore.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStore.md
index 8608df50d55e..f255cf3f4c46 100644
--- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStore.md
+++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStore.md
@@ -18,7 +18,7 @@
- ``receive(_:timeout:assert:file:line:)-1rwdd``
- ``receive(_:timeout:assert:file:line:)-4e4m0``
- ``receive(_:timeout:assert:file:line:)-3myco``
-- ``finish(timeout:file:line:)-53gi5``
+- ``finish(timeout:file:line:)``
- ``TestStoreTask``
### Methods for skipping actions and effects
diff --git a/Sources/ComposableArchitecture/Effect.swift b/Sources/ComposableArchitecture/Effect.swift
index 4f6e7ecd4c03..0c98a49ed96d 100644
--- a/Sources/ComposableArchitecture/Effect.swift
+++ b/Sources/ComposableArchitecture/Effect.swift
@@ -11,7 +11,7 @@ import XCTestDynamicOverlay
"""
'EffectPublisher' has been deprecated in favor of 'EffectTask'.
- You are encouraged to use `EffectTask` to model the ouput of your reducers, and to use Swift concurrency to model asynchrony in dependencies.
+ You are encouraged to use `EffectTask` to model the output of your reducers, and to use Swift concurrency to model asynchrony in dependencies.
See the migration roadmap for more information: https://github.com/pointfreeco/swift-composable-architecture/discussions/1477
"""
@@ -23,7 +23,7 @@ import XCTestDynamicOverlay
"""
'EffectPublisher' has been deprecated in favor of 'EffectTask'.
- You are encouraged to use `EffectTask` to model the ouput of your reducers, and to use Swift concurrency to model asynchrony in dependencies.
+ You are encouraged to use `EffectTask` to model the output of your reducers, and to use Swift concurrency to model asynchrony in dependencies.
See the migration roadmap for more information: https://github.com/pointfreeco/swift-composable-architecture/discussions/1477
"""
@@ -35,7 +35,7 @@ import XCTestDynamicOverlay
"""
'EffectPublisher' has been deprecated in favor of 'EffectTask'.
- You are encouraged to use `EffectTask` to model the ouput of your reducers, and to use Swift concurrency to model asynchrony in dependencies.
+ You are encouraged to use `EffectTask` to model the output of your reducers, and to use Swift concurrency to model asynchrony in dependencies.
See the migration roadmap for more information: https://github.com/pointfreeco/swift-composable-architecture/discussions/1477
"""
@@ -47,7 +47,7 @@ import XCTestDynamicOverlay
"""
'EffectPublisher' has been deprecated in favor of 'EffectTask'.
- You are encouraged to use `EffectTask` to model the ouput of your reducers, and to use Swift concurrency to model asynchrony in dependencies.
+ You are encouraged to use `EffectTask` to model the output of your reducers, and to use Swift concurrency to model asynchrony in dependencies.
See the migration roadmap for more information: https://github.com/pointfreeco/swift-composable-architecture/discussions/1477
"""
@@ -102,17 +102,17 @@ extension EffectPublisher {
/// the Combine interface to ``EffectPublisher`` is considered soft deprecated, and you should
/// eventually port to Swift's native concurrency tools.
///
-/// > Important: The publisher interface to ``EffectTask`` is considered deperecated, and you should
-/// try converting any uses of that interface to Swift's native concurrency tools.
+/// > Important: The publisher interface to ``EffectTask`` is considered deprecated, and you should
+/// > try converting any uses of that interface to Swift's native concurrency tools.
/// >
/// > Also, ``Store`` is not thread safe, and so all effects must receive values on the same
-/// thread. This is typically the main thread, **and** if the store is being used to drive UI then
-/// it must receive values on the main thread.
+/// > thread. This is typically the main thread, **and** if the store is being used to drive UI
+/// > then it must receive values on the main thread.
/// >
/// > This is only an issue if using the Combine interface of ``EffectPublisher`` as mentioned
-/// above. If you are using Swift's concurrency tools and the `.task`, `.run` and `.fireAndForget`
-/// functions on ``EffectTask``, then threading is automatically handled for you.
-public typealias EffectTask = Effect
+/// > above. If you are using Swift's concurrency tools and the `.task`, `.run`, and
+/// > `.fireAndForget` functions on ``EffectTask``, then threading is automatically handled for you.
+public typealias EffectTask = EffectPublisher
extension EffectPublisher where Failure == Never {
/// Wraps an asynchronous unit of work in an effect.
@@ -175,39 +175,41 @@ extension EffectPublisher where Failure == Never {
fileID: StaticString = #fileID,
line: UInt = #line
) -> Self {
- let dependencies = DependencyValues._current
- return Self(
- operation: .run(priority) { send in
- await DependencyValues.$_current.withValue(dependencies) {
- do {
- try await send(operation())
- } catch is CancellationError {
- return
- } catch {
- guard let handler = handler else {
- #if DEBUG
- var errorDump = ""
- customDump(error, to: &errorDump, indent: 4)
- runtimeWarn(
- """
- An "EffectTask.task" returned from "\(fileID):\(line)" threw an unhandled error. …
-
- \(errorDump)
-
- All non-cancellation errors must be explicitly handled via the "catch" parameter \
- on "EffectTask.task", or via a "do" block.
- """,
- file: file,
- line: line
- )
- #endif
+ withEscapedDependencies { escaped in
+ Self(
+ operation: .run(priority) { send in
+ await escaped.yield {
+ do {
+ try await send(operation())
+ } catch is CancellationError {
return
+ } catch {
+ guard let handler = handler else {
+ #if DEBUG
+ var errorDump = ""
+ customDump(error, to: &errorDump, indent: 4)
+ runtimeWarn(
+ """
+ An "EffectTask.task" returned from "\(fileID):\(line)" threw an unhandled \
+ error. …
+
+ \(errorDump)
+
+ All non-cancellation errors must be explicitly handled via the "catch" \
+ parameter on "EffectTask.task", or via a "do" block.
+ """,
+ file: file,
+ line: line
+ )
+ #endif
+ return
+ }
+ await send(handler(error))
}
- await send(handler(error))
}
}
- }
- )
+ )
+ }
}
/// Wraps an asynchronous unit of work that can emit any number of times in an effect.
@@ -257,39 +259,40 @@ extension EffectPublisher where Failure == Never {
fileID: StaticString = #fileID,
line: UInt = #line
) -> Self {
- let dependencies = DependencyValues._current
- return Self(
- operation: .run(priority) { send in
- await DependencyValues.$_current.withValue(dependencies) {
- do {
- try await operation(send)
- } catch is CancellationError {
- return
- } catch {
- guard let handler = handler else {
- #if DEBUG
- var errorDump = ""
- customDump(error, to: &errorDump, indent: 4)
- runtimeWarn(
- """
- An "EffectTask.run" returned from "\(fileID):\(line)" threw an unhandled error. …
-
- \(errorDump)
-
- All non-cancellation errors must be explicitly handled via the "catch" parameter \
- on "EffectTask.run", or via a "do" block.
- """,
- file: file,
- line: line
- )
- #endif
+ withEscapedDependencies { escaped in
+ Self(
+ operation: .run(priority) { send in
+ await escaped.yield {
+ do {
+ try await operation(send)
+ } catch is CancellationError {
return
+ } catch {
+ guard let handler = handler else {
+ #if DEBUG
+ var errorDump = ""
+ customDump(error, to: &errorDump, indent: 4)
+ runtimeWarn(
+ """
+ An "EffectTask.run" returned from "\(fileID):\(line)" threw an unhandled error. …
+
+ \(errorDump)
+
+ All non-cancellation errors must be explicitly handled via the "catch" parameter \
+ on "EffectTask.run", or via a "do" block.
+ """,
+ file: file,
+ line: line
+ )
+ #endif
+ return
+ }
+ await handler(error, send)
}
- await handler(error, send)
}
}
- }
- )
+ )
+ }
}
/// Creates an effect that executes some work in the real world that doesn't need to feed data
@@ -320,6 +323,34 @@ extension EffectPublisher where Failure == Never {
) -> Self {
Self.run(priority: priority) { _ in try? await work() }
}
+
+ /// Initializes an effect that immediately emits the action passed in.
+ ///
+ /// > Note: We do not recommend using `Effect.send` to share logic. Instead, limit usage to
+ /// > child-parent communication, where a child may want to emit a "delegate" action for a parent
+ /// > to listen to.
+ /// >
+ /// > For more information, see .
+ ///
+ /// - Parameter action: The action that is immediately emitted by the effect.
+ public static func send(_ action: Action) -> Self {
+ Self(value: action)
+ }
+
+ /// Initializes an effect that immediately emits the action passed in.
+ ///
+ /// > Note: We do not recommend using `Effect.send` to share logic. Instead, limit usage to
+ /// > child-parent communication, where a child may want to emit a "delegate" action for a parent
+ /// > to listen to.
+ /// >
+ /// > For more information, see .
+ ///
+ /// - Parameters:
+ /// - action: The action that is immediately emitted by the effect.
+ /// - animation: An animation.
+ public static func send(_ action: Action, animation: Animation? = nil) -> Self {
+ Self(value: action).animation(animation)
+ }
}
/// A type that can send actions back into the system when used from
@@ -372,8 +403,17 @@ public struct Send {
/// - action: An action.
/// - animation: An animation.
public func callAsFunction(_ action: Action, animation: Animation?) {
+ callAsFunction(action, transaction: Transaction(animation: animation))
+ }
+
+ /// Sends an action back into the system from an effect with transaction.
+ ///
+ /// - Parameters:
+ /// - action: An action.
+ /// - transaction: A transaction.
+ public func callAsFunction(_ action: Action, transaction: Transaction) {
guard !Task.isCancelled else { return }
- withAnimation(animation) {
+ withTransaction(transaction) {
self(action)
}
}
@@ -500,23 +540,35 @@ extension EffectPublisher {
case .none:
return .none
case let .publisher(publisher):
- let dependencies = DependencyValues._current
- let transform = { action in
- DependencyValues.$_current.withValue(dependencies) {
- transform(action)
- }
- }
- return .init(operation: .publisher(publisher.map(transform).eraseToAnyPublisher()))
- case let .run(priority, operation):
return .init(
- operation: .run(priority) { send in
- await operation(
- Send { action in
- send(transform(action))
- }
- )
- }
+ operation: .publisher(
+ publisher
+ .map(
+ withEscapedDependencies { escaped in
+ { action in
+ escaped.yield {
+ transform(action)
+ }
+ }
+ }
+ )
+ .eraseToAnyPublisher()
+ )
)
+ case let .run(priority, operation):
+ return withEscapedDependencies { escaped in
+ .init(
+ operation: .run(priority) { send in
+ await escaped.yield {
+ await operation(
+ Send { action in
+ send(transform(action))
+ }
+ )
+ }
+ }
+ )
+ }
}
}
}
@@ -648,10 +700,11 @@ extension EffectPublisher {
@available(
*,
+ deprecated,
message:
"""
'Effect' has been deprecated in favor of 'EffectTask' when 'Failure == Never', or 'EffectPublisher