diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0ef77b..810c4c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: name: macOS strategy: matrix: - xcode: ["26.2"] + xcode: ["26.6"] config: ["debug", "release"] runs-on: macos-26 steps: @@ -23,16 +23,16 @@ jobs: run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - name: Run ${{ matrix.config }} tests run: swift test -c ${{ matrix.config }} + - name: Run ${{ matrix.config }} tests with all traits + run: swift test -c ${{ matrix.config }} --enable-all-traits examples: name: Examples strategy: matrix: - xcode: ["26.2"] - config: ["debug"] + xcode: ["26.6"] scheme: ["UndoForMacOS"] runs-on: macos-26 - continue-on-error: true steps: - uses: actions/checkout@v5 - name: Select Xcode ${{ matrix.xcode }} diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 01be9b6..f5ee115 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -3,20 +3,11 @@ archiveVersion = 1; classes = { }; - objectVersion = 77; + objectVersion = 100; objects = { /* Begin PBXBuildFile section */ C441694F2F38F3B100051412 /* SQLiteUndo in Frameworks */ = {isa = PBXBuildFile; productRef = C441694E2F38F3B100051412 /* SQLiteUndo */; }; - C44169512F38F3B100051412 /* SQLiteUndoTCA in Frameworks */ = {isa = PBXBuildFile; productRef = C44169502F38F3B100051412 /* SQLiteUndoTCA */; }; - C45245EF2F3D353800F31BB8 /* SQLiteUndo in Frameworks */ = {isa = PBXBuildFile; productRef = C45245EE2F3D353800F31BB8 /* SQLiteUndo */; }; - C45245F12F3D353800F31BB8 /* SQLiteUndoTCA in Frameworks */ = {isa = PBXBuildFile; productRef = C45245F02F3D353800F31BB8 /* SQLiteUndoTCA */; }; - C4B1976A2F33B52B001EAFC2 /* SQLiteUndo in Frameworks */ = {isa = PBXBuildFile; productRef = C4B197692F33B52B001EAFC2 /* SQLiteUndo */; }; - C4B197712F33B5D9001EAFC2 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = C4B197702F33B5D9001EAFC2 /* ComposableArchitecture */; }; - C4B197F02F33C28A001EAFC2 /* SQLiteUndo in Frameworks */ = {isa = PBXBuildFile; productRef = C4B197EF2F33C28A001EAFC2 /* SQLiteUndo */; }; - C4B197F22F33C28A001EAFC2 /* SQLiteUndoTCA in Frameworks */ = {isa = PBXBuildFile; productRef = C4B197F12F33C28A001EAFC2 /* SQLiteUndoTCA */; }; - C4B197F52F33C2B0001EAFC2 /* SQLiteUndo in Frameworks */ = {isa = PBXBuildFile; productRef = C4B197F42F33C2B0001EAFC2 /* SQLiteUndo */; }; - C4B197F72F33C2B0001EAFC2 /* SQLiteUndoTCA in Frameworks */ = {isa = PBXBuildFile; productRef = C4B197F62F33C2B0001EAFC2 /* SQLiteUndoTCA */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -34,20 +25,9 @@ /* Begin PBXFrameworksBuildPhase section */ C4B1975A2F33B502001EAFC2 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; files = ( C441694F2F38F3B100051412 /* SQLiteUndo in Frameworks */, - C45245EF2F3D353800F31BB8 /* SQLiteUndo in Frameworks */, - C4B197712F33B5D9001EAFC2 /* ComposableArchitecture in Frameworks */, - C4B197F22F33C28A001EAFC2 /* SQLiteUndoTCA in Frameworks */, - C4B197F52F33C2B0001EAFC2 /* SQLiteUndo in Frameworks */, - C4B197F02F33C28A001EAFC2 /* SQLiteUndo in Frameworks */, - C44169512F38F3B100051412 /* SQLiteUndoTCA in Frameworks */, - C45245F12F3D353800F31BB8 /* SQLiteUndoTCA in Frameworks */, - C4B1976A2F33B52B001EAFC2 /* SQLiteUndo in Frameworks */, - C4B197F72F33C2B0001EAFC2 /* SQLiteUndoTCA in Frameworks */, ); - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ @@ -81,23 +61,12 @@ ); buildRules = ( ); - dependencies = ( - ); fileSystemSynchronizedGroups = ( C4B1975E2F33B502001EAFC2 /* UndoForMacOS */, ); name = UndoForMacOS; packageProductDependencies = ( - C4B197692F33B52B001EAFC2 /* SQLiteUndo */, - C4B197702F33B5D9001EAFC2 /* ComposableArchitecture */, - C4B197EF2F33C28A001EAFC2 /* SQLiteUndo */, - C4B197F12F33C28A001EAFC2 /* SQLiteUndoTCA */, - C4B197F42F33C2B0001EAFC2 /* SQLiteUndo */, - C4B197F62F33C2B0001EAFC2 /* SQLiteUndoTCA */, C441694E2F38F3B100051412 /* SQLiteUndo */, - C44169502F38F3B100051412 /* SQLiteUndoTCA */, - C45245EE2F3D353800F31BB8 /* SQLiteUndo */, - C45245F02F3D353800F31BB8 /* SQLiteUndoTCA */, ); productName = UndoForMacOS; productReference = C4B1975D2F33B502001EAFC2 /* UndoForMacOS.app */; @@ -128,7 +97,6 @@ mainGroup = C4B1973C2F33B4B4001EAFC2; minimizedProjectReferenceProxies = 1; packageReferences = ( - C4B1976F2F33B5D9001EAFC2 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, C45245ED2F3D353800F31BB8 /* XCLocalSwiftPackageReference "../../sqlite-undo" */, ); preferredProjectObjectVersion = 77; @@ -144,25 +112,21 @@ /* Begin PBXResourcesBuildPhase section */ C4B1975B2F33B502001EAFC2 /* Resources */ = { isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; files = ( ); - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ C4B197592F33B502001EAFC2 /* Sources */ = { isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; files = ( ); - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ - C4B1974E2F33B4B5001EAFC2 /* Debug */ = { + C4B1974E2F33B4B5001EAFC2 /* Debug configuration for PBXProject "Examples" */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -224,7 +188,7 @@ }; name = Debug; }; - C4B1974F2F33B4B5001EAFC2 /* Release */ = { + C4B1974F2F33B4B5001EAFC2 /* Release configuration for PBXProject "Examples" */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -278,7 +242,7 @@ }; name = Release; }; - C4B197662F33B503001EAFC2 /* Debug */ = { + C4B197662F33B503001EAFC2 /* Debug configuration for PBXNativeTarget "UndoForMacOS" */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -312,7 +276,7 @@ }; name = Debug; }; - C4B197672F33B503001EAFC2 /* Release */ = { + C4B197672F33B503001EAFC2 /* Release configuration for PBXNativeTarget "UndoForMacOS" */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -352,19 +316,17 @@ C4B197402F33B4B4001EAFC2 /* Build configuration list for PBXProject "Examples" */ = { isa = XCConfigurationList; buildConfigurations = ( - C4B1974E2F33B4B5001EAFC2 /* Debug */, - C4B1974F2F33B4B5001EAFC2 /* Release */, + C4B1974E2F33B4B5001EAFC2 /* Debug configuration for PBXProject "Examples" */, + C4B1974F2F33B4B5001EAFC2 /* Release configuration for PBXProject "Examples" */, ); - defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; C4B197652F33B503001EAFC2 /* Build configuration list for PBXNativeTarget "UndoForMacOS" */ = { isa = XCConfigurationList; buildConfigurations = ( - C4B197662F33B503001EAFC2 /* Debug */, - C4B197672F33B503001EAFC2 /* Release */, + C4B197662F33B503001EAFC2 /* Debug configuration for PBXNativeTarget "UndoForMacOS" */, + C4B197672F33B503001EAFC2 /* Release configuration for PBXNativeTarget "UndoForMacOS" */, ); - defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ @@ -373,64 +335,17 @@ C45245ED2F3D353800F31BB8 /* XCLocalSwiftPackageReference "../../sqlite-undo" */ = { isa = XCLocalSwiftPackageReference; relativePath = "../../sqlite-undo"; + traits = ( + ComposableArchitecture, + ); }; /* End XCLocalSwiftPackageReference section */ -/* Begin XCRemoteSwiftPackageReference section */ - C4B1976F2F33B5D9001EAFC2 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.23.1; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - /* Begin XCSwiftPackageProductDependency section */ C441694E2F38F3B100051412 /* SQLiteUndo */ = { isa = XCSwiftPackageProductDependency; productName = SQLiteUndo; }; - C44169502F38F3B100051412 /* SQLiteUndoTCA */ = { - isa = XCSwiftPackageProductDependency; - productName = SQLiteUndoTCA; - }; - C45245EE2F3D353800F31BB8 /* SQLiteUndo */ = { - isa = XCSwiftPackageProductDependency; - productName = SQLiteUndo; - }; - C45245F02F3D353800F31BB8 /* SQLiteUndoTCA */ = { - isa = XCSwiftPackageProductDependency; - productName = SQLiteUndoTCA; - }; - C4B197692F33B52B001EAFC2 /* SQLiteUndo */ = { - isa = XCSwiftPackageProductDependency; - productName = SQLiteUndo; - }; - C4B197702F33B5D9001EAFC2 /* ComposableArchitecture */ = { - isa = XCSwiftPackageProductDependency; - package = C4B1976F2F33B5D9001EAFC2 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; - productName = ComposableArchitecture; - }; - C4B197EF2F33C28A001EAFC2 /* SQLiteUndo */ = { - isa = XCSwiftPackageProductDependency; - package = C4B1976F2F33B5D9001EAFC2 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; - productName = SQLiteUndo; - }; - C4B197F12F33C28A001EAFC2 /* SQLiteUndoTCA */ = { - isa = XCSwiftPackageProductDependency; - package = C4B1976F2F33B5D9001EAFC2 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; - productName = SQLiteUndoTCA; - }; - C4B197F42F33C2B0001EAFC2 /* SQLiteUndo */ = { - isa = XCSwiftPackageProductDependency; - productName = SQLiteUndo; - }; - C4B197F62F33C2B0001EAFC2 /* SQLiteUndoTCA */ = { - isa = XCSwiftPackageProductDependency; - productName = SQLiteUndoTCA; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = C4B1973D2F33B4B4001EAFC2 /* Project object */; diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 26e1323..37ceb54 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "09bc66dffe4811b4627872585613011627e6f6341a227183a35983c8f373e434", + "originHash" : "bd37d1fc9d32651eefdd5312e63688e1986e36146747a4ca919d4575aab6cb36", "pins" : [ { "identity" : "combine-schedulers", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "fd16d76fd8b9a976d88bfb6cacc05ca8d19c91b6", - "version" : "1.1.0" + "revision" : "dcccb979a2183b8df3334237e3dc1ae2b4116a86", + "version" : "1.2.0" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/GRDB.swift", "state" : { - "revision" : "aa0079aeb82a4bf00324561a40bffe68c6fe1c26", - "version" : "7.9.0" + "revision" : "b83108d10f42680d78f23fe4d4d80fc88dab3212", + "version" : "7.11.1" } }, { @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/sqlite-data.git", "state" : { - "revision" : "05704b563ecb7f0bd7e49b6f360a6383a3e53e7d", - "version" : "1.5.1" + "revision" : "97987458b49f0311717ecfbf7e8ac4c406afbf55", + "version" : "1.9.0" } }, { @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "6989976265be3f8d2b5802c722f9ba168e227c71", - "version" : "1.7.2" + "revision" : "794f4b0a9cf32042592388d014f6a1ea987d323a", + "version" : "1.9.1" } }, { @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e", - "version" : "1.0.6" + "revision" : "72d749bf341b78851203066ab421869b783ec42a", + "version" : "1.1.0" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", - "version" : "1.3.0" + "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", + "version" : "1.6.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", "state" : { - "revision" : "5b0890fabfd68a2d375d68502bc3f54a8548c494", - "version" : "1.23.1" + "revision" : "ead11e04e5011c437722c1990d22f80d87056978", + "version" : "1.26.1" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "5a3825302b1a0d744183200915a47b508c828e6f", - "version" : "1.3.2" + "revision" : "5fa253428866f2360c3754e88537f700ed2656b5", + "version" : "1.4.1" } }, { @@ -78,17 +78,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "2a2a938798236b8fa0bc57c453ee9de9f9ec3ab0", - "version" : "1.4.1" + "revision" : "a8cd6c976f335ed361dcecddb0dc39ebda51bc3e", + "version" : "1.6.1" } }, { "identity" : "swift-dependencies", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-dependencies", + "location" : "https://github.com/pointfreeco/swift-dependencies.git", "state" : { - "revision" : "c79f72b3e67a1eb64f66f76704c22ed6a5c1ed84", - "version" : "1.11.0" + "revision" : "8dc1fbf2f6255a73dec53b4648164884898db4c5", + "version" : "1.14.1" } }, { @@ -105,17 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-navigation", "state" : { - "revision" : "bf498690e1f6b4af790260f542e8428a4ba10d78", - "version" : "2.6.0" - } - }, - { - "identity" : "swift-parsing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-parsing", - "state" : { - "revision" : "3432cb81164dd3d69a75d0d63205be5fbae2c34b", - "version" : "0.14.1" + "revision" : "32f35241b8be0719c4c7f00eb27713b1cadb6248", + "version" : "2.8.0" } }, { @@ -123,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-perception", "state" : { - "revision" : "4f47ebafed5f0b0172cf5c661454fa8e28fb2ac4", - "version" : "2.0.9" + "revision" : "de219a1cf34e958134e75a9ebb134cf09bf52fc6", + "version" : "2.0.11" } }, { @@ -132,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-sharing", "state" : { - "revision" : "3bfc408cc2d0bee2287c174da6b1c76768377818", - "version" : "2.7.4" + "revision" : "8244fe63bf43e58188ab13851ad693eecf6a9e90", + "version" : "2.9.1" } }, { @@ -141,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "bf8d8c27f0f0c6d5e77bff0db76ab68f2050d15d", - "version" : "1.18.9" + "revision" : "59a99c458de4d2dee580529b61b4f78dca7b7fa6", + "version" : "1.19.4" } }, { @@ -150,8 +141,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "revision" : "d8163b3a98f3c8434c4361e85126db449d84bc66", - "version" : "0.30.0" + "revision" : "fb25c16d0bde460ffc01fe19ab911bb26f40f26b", + "version" : "0.35.0" } }, { @@ -159,8 +150,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "4799286537280063c85a32f09884cfbca301b1a1", - "version" : "602.0.0" + "revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1", + "version" : "603.0.2" } }, { @@ -168,8 +159,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "34e463e98ab8541c604af706c99bed7160f5ec70", - "version" : "1.8.1" + "revision" : "8f6abcf4c8950e2679d5b2fee4ca284fd7c34886", + "version" : "1.11.0" } } ], diff --git a/Examples/UndoForMacOS/UndoForMacOSApp.swift b/Examples/UndoForMacOS/UndoForMacOSApp.swift index 12efaa5..4c5c486 100644 --- a/Examples/UndoForMacOS/UndoForMacOSApp.swift +++ b/Examples/UndoForMacOS/UndoForMacOSApp.swift @@ -1,6 +1,5 @@ import ComposableArchitecture import SQLiteUndo -import SQLiteUndoTCA import SwiftUI @main diff --git a/Package.resolved b/Package.resolved index 3f1b31e..55f7012 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "75bf0478d821e9f021f47f259b924d83b2fefb8965ce5c440228d8abf109c16c", + "originHash" : "d535a2b60bbda978242883e6fb6bdfcff22b5591e927d715970168cc1cb004cf", "pins" : [ { "identity" : "combine-schedulers", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "fd16d76fd8b9a976d88bfb6cacc05ca8d19c91b6", - "version" : "1.1.0" + "revision" : "dcccb979a2183b8df3334237e3dc1ae2b4116a86", + "version" : "1.2.0" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/GRDB.swift", "state" : { - "revision" : "aa0079aeb82a4bf00324561a40bffe68c6fe1c26", - "version" : "7.9.0" + "revision" : "b83108d10f42680d78f23fe4d4d80fc88dab3212", + "version" : "7.11.1" } }, { @@ -24,17 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/sqlite-data.git", "state" : { - "revision" : "05704b563ecb7f0bd7e49b6f360a6383a3e53e7d", - "version" : "1.5.1" - } - }, - { - "identity" : "swift-case-paths", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-case-paths", - "state" : { - "revision" : "6989976265be3f8d2b5802c722f9ba168e227c71", - "version" : "1.7.2" + "revision" : "97987458b49f0311717ecfbf7e8ac4c406afbf55", + "version" : "1.9.0" } }, { @@ -42,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e", - "version" : "1.0.6" + "revision" : "72d749bf341b78851203066ab421869b783ec42a", + "version" : "1.1.0" } }, { @@ -51,17 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-composable-architecture", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", - "state" : { - "revision" : "1eaa6fa2ee57ac42843283b9fd3457af408c858d", - "version" : "1.25.5" + "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", + "version" : "1.6.0" } }, { @@ -69,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "5a3825302b1a0d744183200915a47b508c828e6f", - "version" : "1.3.2" + "revision" : "5fa253428866f2360c3754e88537f700ed2656b5", + "version" : "1.4.1" } }, { @@ -78,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump.git", "state" : { - "revision" : "2a2a938798236b8fa0bc57c453ee9de9f9ec3ab0", - "version" : "1.4.1" + "revision" : "a8cd6c976f335ed361dcecddb0dc39ebda51bc3e", + "version" : "1.6.1" } }, { @@ -87,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies.git", "state" : { - "revision" : "c79f72b3e67a1eb64f66f76704c22ed6a5c1ed84", - "version" : "1.11.0" + "revision" : "8dc1fbf2f6255a73dec53b4648164884898db4c5", + "version" : "1.14.1" } }, { @@ -100,22 +82,13 @@ "version" : "1.1.1" } }, - { - "identity" : "swift-navigation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-navigation", - "state" : { - "revision" : "32f35241b8be0719c4c7f00eb27713b1cadb6248", - "version" : "2.8.0" - } - }, { "identity" : "swift-perception", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-perception", "state" : { - "revision" : "4f47ebafed5f0b0172cf5c661454fa8e28fb2ac4", - "version" : "2.0.9" + "revision" : "de219a1cf34e958134e75a9ebb134cf09bf52fc6", + "version" : "2.0.11" } }, { @@ -123,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-sharing", "state" : { - "revision" : "3bfc408cc2d0bee2287c174da6b1c76768377818", - "version" : "2.7.4" + "revision" : "8244fe63bf43e58188ab13851ad693eecf6a9e90", + "version" : "2.9.1" } }, { @@ -132,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git", "state" : { - "revision" : "bf8d8c27f0f0c6d5e77bff0db76ab68f2050d15d", - "version" : "1.18.9" + "revision" : "59a99c458de4d2dee580529b61b4f78dca7b7fa6", + "version" : "1.19.4" } }, { @@ -141,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "revision" : "d8163b3a98f3c8434c4361e85126db449d84bc66", - "version" : "0.30.0" + "revision" : "fb25c16d0bde460ffc01fe19ab911bb26f40f26b", + "version" : "0.35.0" } }, { @@ -150,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "4799286537280063c85a32f09884cfbca301b1a1", - "version" : "602.0.0" + "revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1", + "version" : "603.0.2" } }, { @@ -159,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "34e463e98ab8541c604af706c99bed7160f5ec70", - "version" : "1.8.1" + "revision" : "8f6abcf4c8950e2679d5b2fee4ca284fd7c34886", + "version" : "1.11.0" } } ], diff --git a/Package.swift b/Package.swift index 44b5d49..f2e3406 100644 --- a/Package.swift +++ b/Package.swift @@ -1,24 +1,32 @@ // swift-tools-version: 6.1 +import Foundation import PackageDescription let package = Package( name: "sqlite-undo", platforms: [ - .iOS(.v13), + .iOS(.v16), .macOS(.v13), ], products: [ - .library(name: "SQLiteUndo", targets: ["SQLiteUndo"]), - .library(name: "SQLiteUndoTCA", targets: ["SQLiteUndoTCA"]), + .library(name: "SQLiteUndo", targets: ["SQLiteUndo"]) + ], + traits: [ + .trait( + name: "ComposableArchitecture", + description: "Enable undo support for the Composable Architecture" + ) ], dependencies: [ .package( - url: "https://github.com/pointfreeco/swift-composable-architecture.git", from: "1.25.0"), + url: "https://github.com/pointfreeco/swift-composable-architecture.git", + from: "1.25.0" + ), .package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "1.3.3"), .package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.9.5"), .package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.18.7"), - .package(url: "https://github.com/pointfreeco/sqlite-data.git", from: "1.3.0"), + .package(url: "https://github.com/pointfreeco/sqlite-data.git", from: "1.9.0"), ], targets: [ .target( @@ -27,13 +35,11 @@ let package = Package( .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "DependenciesMacros", package: "swift-dependencies"), .product(name: "SQLiteData", package: "sqlite-data"), - ] - ), - .target( - name: "SQLiteUndoTCA", - dependencies: [ - "SQLiteUndo", - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product( + name: "ComposableArchitecture", + package: "swift-composable-architecture", + condition: .when(traits: ["ComposableArchitecture"]) + ), ] ), .testTarget( @@ -45,16 +51,21 @@ let package = Package( .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"), .product(name: "SQLiteDataTestSupport", package: "sqlite-data"), .product(name: "SnapshotTestingCustomDump", package: "swift-snapshot-testing"), - ] - ), - .testTarget( - name: "SQLiteUndoTCATests", - dependencies: [ - "SQLiteUndoTCA", - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - .product(name: "DependenciesTestSupport", package: "swift-dependencies"), - .product(name: "SQLiteDataTestSupport", package: "sqlite-data"), + .product( + name: "ComposableArchitecture", + package: "swift-composable-architecture", + condition: .when(traits: ["ComposableArchitecture"]) + ), ] ), ] ) + +// NB: Xcode provides no way to select traits for the package you have open, so +// uncomment the '|| true' below to work on the trait-gated code there. +if ProcessInfo.processInfo.environment["SPI_GENERATE_DOCS"] != nil // || true +{ + package.traits.insert( + .default(enabledTraits: ["ComposableArchitecture"]) + ) +} diff --git a/README.md b/README.md index 2ebe28e..26a6ed6 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,7 @@ SQLite-based undo/redo for Swift apps using [SQLiteData](https://github.com/poin Changes are grouped into barriers that represent single user actions (e.g., "Set Rating", "Delete Item"). Barriers integrate with `NSUndoManager` so undo/redo works with the standard Edit menu, keyboard shortcuts, and shake-to-undo. -Two libraries are provided: - -- **SQLiteUndo** — core undo engine, barriers, and free functions (`undoable`, `withUndoDisabled`) -- **SQLiteUndoTCA** — [ComposableArchitecture](https://github.com/pointfreeco/swift-composable-architecture) integration for `UndoManager` wiring in SwiftUI +A single **SQLiteUndo** library is provided: the core undo engine, barriers, and free functions (`undoable`, `withUndoDisabled`). [ComposableArchitecture](https://github.com/pointfreeco/swift-composable-architecture) integration for `UndoManager` wiring in SwiftUI ships in the same library, behind a package trait. ## Adding SQLiteUndo as a dependency @@ -27,6 +24,22 @@ Then add the product to your target's dependencies: .product(name: "SQLiteUndo", package: "sqlite-undo"), ``` +### ComposableArchitecture support + +The ComposableArchitecture integration is gated behind the `ComposableArchitecture` trait, which is **off by default**. Apps that don't use the Composable Architecture never resolve it as a dependency at all. To opt in, enable the trait on the package dependency: + +```swift +.package( + url: "https://github.com/latentco/sqlite-undo.git", + from: "0.1.0", + traits: ["ComposableArchitecture"] +), +``` + +SQLiteUndo declares no default traits, so this is the complete trait set — there is nothing else to re-enable alongside it. + +In an Xcode project, traits are selected on the package dependency itself rather than in `Package.swift`: choose the package under **Package Dependencies** and enable the trait in its Traits picker. This requires Xcode 26.5 or later, which is when trait selection was added to the project format. + ## Setup ```swift @@ -151,8 +164,11 @@ Events are scoped to the stack that performed the undo, so an undo in one window ## ComposableArchitecture/SwiftUI Integration +Requires the `ComposableArchitecture` trait — see [ComposableArchitecture support](#composablearchitecture-support). + ```swift -import SQLiteUndoTCA +import ComposableArchitecture +import SQLiteUndo @Reducer struct MyFeature { @@ -231,6 +247,19 @@ Because AppKit resolves `UndoManager` up the responder chain, this also gives yo document case for free: several windows onto one `NSDocument` resolve to the *same* `UndoManager`, so give them the same stack and they correctly share one undo history. +## Development + +Because the ComposableArchitecture trait is off by default, `swift test` exercises only the core engine. Run the suite both ways: + +```sh +swift test # core engine, trait off +swift test --enable-all-traits # adds the ComposableArchitecture tests +``` + +CI runs both, in debug and release. Building with the trait off is what catches ComposableArchitecture symbols leaking outside their `#if ComposableArchitecture` guards. + +Xcode offers no way to select traits for a package you have opened directly — traits are chosen on a dependency edge, and a root package has none. To work on the trait-gated code in Xcode, uncomment the `|| true` in `Package.swift` to force the trait on locally. Leaving it uncommitted matters: with the trait forced on, the trait-off path stops being compiled anywhere. + ## License This library is released under the MIT license. See [LICENSE](LICENSE) for details. diff --git a/Sources/SQLiteUndo/ComposableArchitecture.swift b/Sources/SQLiteUndo/ComposableArchitecture.swift new file mode 100644 index 0000000..28fc2ea --- /dev/null +++ b/Sources/SQLiteUndo/ComposableArchitecture.swift @@ -0,0 +1,110 @@ +#if ComposableArchitecture + import CasePaths + import ComposableArchitecture + import Foundation + import SwiftUI + + /// Protocol for actions that can receive the UndoManager from the view layer. + /// + /// Conform your feature's Action to this protocol: + /// + /// ```swift + /// @Reducer struct MyFeature { + /// enum Action: UndoManageableAction { + /// case undoManager(UndoManagingAction) + /// // ... other actions + /// } + /// + /// var body: some Reducer { + /// UndoManagingReducer() + /// Reduce { state, action in + /// // ... + /// } + /// } + /// } + /// ``` + /// + /// Then set the undo manager in your view: + /// + /// ```swift + /// ContentView(store: store) + /// .setUndoManager(store: store) + /// ``` + public protocol UndoManageableAction { + static func undoManager(_ action: UndoManagingAction) -> Self + } + + /// Action for managing the UndoManager connection. + @CasePathable + public enum UndoManagingAction: Sendable { + case set(UndoManager?) + case event(UndoEvent) + } + + /// A reducer that handles `UndoManaging` actions by setting the UndoManager on the UndoEngine. + /// + /// Compose this into your feature like `BindingReducer`: + /// + /// ```swift + /// var body: some Reducer { + /// UndoManagingReducer() + /// Reduce { state, action in + /// // ... + /// } + /// } + /// ``` + public struct UndoManagingReducer: Reducer { + @Dependency(\.defaultUndoStack) var undoStack + + private enum CancelID { case eventSubscription } + + public init() {} + public var body: some Reducer { + Reduce { state, action in + guard let undoAction = AnyCasePath(unsafe: Action.undoManager).extract(from: action) else { + return .none + } + switch undoAction { + case .set(let manager): + undoStack.setUndoManager(manager) + let events = undoStack.events + return .run { send in + for await event in events() { + await send(.undoManager(.event(event))) + } + } + .cancellable(id: CancelID.eventSubscription, cancelInFlight: true) + case .event: + return .none + } + } + } + } + + extension View { + /// Sets the view's UndoManager on the store's undo system. + /// + /// Call this on your root view to connect the window's UndoManager: + /// + /// ```swift + /// ContentView(store: store) + /// .setUndoManager(store: store) + /// ``` + public func setUndoManager( + store: Store + ) -> some View { + modifier(SetUndoManagerModifier(store: store)) + } + } + + struct SetUndoManagerModifier: ViewModifier { + @Environment(\.undoManager) var undoManager + let store: Store + + func body(content: Content) -> some View { + content.task(id: undoManager) { + await store.send(.undoManager(.set(undoManager))).finish() + } + } + } +#endif diff --git a/Sources/SQLiteUndoTCA/Exports.swift b/Sources/SQLiteUndoTCA/Exports.swift deleted file mode 100644 index e139771..0000000 --- a/Sources/SQLiteUndoTCA/Exports.swift +++ /dev/null @@ -1 +0,0 @@ -@_exported import SQLiteUndo diff --git a/Sources/SQLiteUndoTCA/UndoManaging.swift b/Sources/SQLiteUndoTCA/UndoManaging.swift deleted file mode 100644 index 6156f27..0000000 --- a/Sources/SQLiteUndoTCA/UndoManaging.swift +++ /dev/null @@ -1,109 +0,0 @@ -import CasePaths -import ComposableArchitecture -import Foundation -import SQLiteUndo -import SwiftUI - -/// Protocol for actions that can receive the UndoManager from the view layer. -/// -/// Conform your feature's Action to this protocol: -/// -/// ```swift -/// @Reducer struct MyFeature { -/// enum Action: UndoManageableAction { -/// case undoManager(UndoManagingAction) -/// // ... other actions -/// } -/// -/// var body: some Reducer { -/// UndoManagingReducer() -/// Reduce { state, action in -/// // ... -/// } -/// } -/// } -/// ``` -/// -/// Then set the undo manager in your view: -/// -/// ```swift -/// ContentView(store: store) -/// .setUndoManager(store: store) -/// ``` -public protocol UndoManageableAction { - static func undoManager(_ action: UndoManagingAction) -> Self -} - -/// Action for managing the UndoManager connection. -@CasePathable -public enum UndoManagingAction: Sendable { - case set(UndoManager?) - case event(UndoEvent) -} - -/// A reducer that handles `UndoManaging` actions by setting the UndoManager on the UndoEngine. -/// -/// Compose this into your feature like `BindingReducer`: -/// -/// ```swift -/// var body: some Reducer { -/// UndoManagingReducer() -/// Reduce { state, action in -/// // ... -/// } -/// } -/// ``` -public struct UndoManagingReducer: Reducer { - @Dependency(\.defaultUndoStack) var undoStack - - private enum CancelID { case eventSubscription } - - public init() {} - public var body: some Reducer { - Reduce { state, action in - guard let undoAction = AnyCasePath(unsafe: Action.undoManager).extract(from: action) else { - return .none - } - switch undoAction { - case .set(let manager): - undoStack.setUndoManager(manager) - let events = undoStack.events - return .run { send in - for await event in events() { - await send(.undoManager(.event(event))) - } - } - .cancellable(id: CancelID.eventSubscription, cancelInFlight: true) - case .event: - return .none - } - } - } -} - -extension View { - /// Sets the view's UndoManager on the store's undo system. - /// - /// Call this on your root view to connect the window's UndoManager: - /// - /// ```swift - /// ContentView(store: store) - /// .setUndoManager(store: store) - /// ``` - public func setUndoManager( - store: Store - ) -> some View { - modifier(SetUndoManagerModifier(store: store)) - } -} - -struct SetUndoManagerModifier: ViewModifier { - @Environment(\.undoManager) var undoManager - let store: Store - - func body(content: Content) -> some View { - content.task(id: undoManager) { - await store.send(.undoManager(.set(undoManager))).finish() - } - } -} diff --git a/Tests/SQLiteUndoTCATests/UndoableEffectTests.swift b/Tests/SQLiteUndoTCATests/UndoableEffectTests.swift deleted file mode 100644 index f82c845..0000000 --- a/Tests/SQLiteUndoTCATests/UndoableEffectTests.swift +++ /dev/null @@ -1,207 +0,0 @@ -import ComposableArchitecture -import DependenciesTestSupport -import Foundation -import SQLiteData -import SQLiteUndo -import StructuredQueries -import Testing - -@testable import SQLiteUndoTCA - -@Suite( - .serialized -) -@MainActor -struct UndoableEffectTests { - - @Test - func effectUndoableCreatesBarrier() async throws { - let testUndoManager = UndoManager() - - try await withDependencies { - let database = try! makeTestDatabase() - $0.defaultDatabase = database - $0.installDefaultUndoStack(testUndoManager) - $0.defaultUndoEngine = try! UndoEngine(for: database, tables: TestRecord.self) - } operation: { - @Dependency(\.defaultDatabase) var database - - let store = TestStore(initialState: TestFeature.State()) { - TestFeature() - } - - await store.send(.insertItem) - await store.receive(\.itemInserted) - - let count = try await database.read { db in try TestRecord.all.fetchCount(db) } - #expect(count == 1) - #expect(testUndoManager.canUndo == true) - #expect(testUndoManager.undoActionName == "Insert Item") - } - } - - @Test - func effectUndoableUndoWorks() async throws { - let testUndoManager = UndoManager() - - try await withDependencies { - let database = try! makeTestDatabase() - $0.defaultDatabase = database - $0.installDefaultUndoStack(testUndoManager) - $0.defaultUndoEngine = try! UndoEngine(for: database, tables: TestRecord.self) - } operation: { - @Dependency(\.defaultDatabase) var database - - let store = TestStore(initialState: TestFeature.State()) { - TestFeature() - } - - await store.send(.insertItem) - await store.receive(\.itemInserted) - - let countBefore = try await database.read { db in try TestRecord.all.fetchCount(db) } - #expect(countBefore == 1) - - testUndoManager.undo() - - let countAfter = try await database.read { db in try TestRecord.all.fetchCount(db) } - #expect(countAfter == 0) - } - } - /// `.task(id: undoManager)` re-fires whenever the environment's UndoManager changes - /// identity, so `.set` arrives more than once and resubscribes to the event stream. - @Test - func resettingUndoManagerKeepsEventsFlowing() async throws { - let testUndoManager = UndoManager() - - await withDependencies { - let database = try! makeTestDatabase() - $0.defaultDatabase = database - $0.installDefaultUndoStack(testUndoManager) - $0.defaultUndoEngine = try! UndoEngine(for: database, tables: TestRecord.self) - } operation: { - let store = TestStore(initialState: TestFeature.State()) { - TestFeature() - } - - let first = await store.send(.undoManager(.set(testUndoManager))) - let second = await store.send(.undoManager(.set(testUndoManager))) - - await store.send(.insertItem) - await store.receive(\.itemInserted) - - testUndoManager.undo() - - await store.receive(\.undoManager.event) - - await first.cancel() - await second.cancel() - } - } - - /// Two windows, each with its own UndoStack and UndoManager, observe their own - /// events independently — mounting the second leaves the first's subscription intact. - @Test - func eachWindowObservesItsOwnEventsIndependently() async throws { - let managerA = UndoManager() - let managerB = UndoManager() - let stackA = UndoStack.live(managerA) - let stackB = UndoStack.live(managerB) - - try await withDependencies { - let database = try! makeTestDatabase() - $0.defaultDatabase = database - $0.defaultUndoEngine = try! UndoEngine(for: database, tables: TestRecord.self) - } operation: { - let storeA = withDependencies { - $0.defaultUndoStack = stackA - } operation: { - TestStore(initialState: TestFeature.State()) { TestFeature() } - } - let storeB = withDependencies { - $0.defaultUndoStack = stackB - } operation: { - TestStore(initialState: TestFeature.State()) { TestFeature() } - } - - let subscriptionA = await storeA.send(.undoManager(.set(managerA))) - let subscriptionB = await storeB.send(.undoManager(.set(managerB))) - - await storeA.send(.insertItem) - await storeA.receive(\.itemInserted) - - managerA.undo() - - await storeA.receive(\.undoManager.event) - - await subscriptionA.cancel() - await subscriptionB.cancel() - } - } -} - -// MARK: - Test Feature - -@Reducer -private struct TestFeature { - @ObservableState - struct State: Equatable {} - - enum Action: UndoManageableAction { - case insertItem - case itemInserted - case undoManager(UndoManagingAction) - } - - @Dependency(\.defaultDatabase) var database - - var body: some ReducerOf { - UndoManagingReducer() - Reduce { state, action in - switch action { - case .insertItem: - return .run { [database] send in - try await undoable("Insert Item") { - try await database.write { db in - try TestRecord.insert { TestRecord(id: 1, name: "Test") }.execute(db) - } - } - await send(.itemInserted) - } - - case .itemInserted: - return .none - - case .undoManager: - return .none - } - } - } -} - -// MARK: - Test Helpers - -@Table -private struct TestRecord: Identifiable { - @Column(primaryKey: true) var id: Int - var name: String = "" - var value: Int? -} - -private func makeTestDatabase() throws -> any DatabaseWriter { - let database = try DatabaseQueue(configuration: Configuration()) - - try database.write { db in - try db.execute( - sql: """ - CREATE TABLE "testRecords" ( - "id" INTEGER PRIMARY KEY, - "name" TEXT NOT NULL DEFAULT '', - "value" INTEGER - ) - """ - ) - } - - return database -} diff --git a/Tests/SQLiteUndoTests/ComposableArchitectureTests.swift b/Tests/SQLiteUndoTests/ComposableArchitectureTests.swift new file mode 100644 index 0000000..e9a3c9a --- /dev/null +++ b/Tests/SQLiteUndoTests/ComposableArchitectureTests.swift @@ -0,0 +1,208 @@ +#if ComposableArchitecture + import ComposableArchitecture + import DependenciesTestSupport + import Foundation + import SQLiteData + import SQLiteUndo + import StructuredQueries + import Testing + + @Suite( + .serialized + ) + @MainActor + struct ComposableArchitectureTests { + + @Test + func effectUndoableCreatesBarrier() async throws { + let testUndoManager = UndoManager() + + try await withDependencies { + let database = try! makeTestDatabase() + $0.defaultDatabase = database + $0.installDefaultUndoStack(testUndoManager) + $0.defaultUndoEngine = try! UndoEngine(for: database, tables: TestRecord.self) + } operation: { + @Dependency(\.defaultDatabase) var database + + let store = TestStore(initialState: TestFeature.State()) { + TestFeature() + } + + await store.send(.insertItem) + await store.receive(\.itemInserted) + + let count = try await database.read { db in try TestRecord.all.fetchCount(db) } + #expect(count == 1) + #expect(testUndoManager.canUndo == true) + #expect(testUndoManager.undoActionName == "Insert Item") + } + } + + @Test + func effectUndoableUndoWorks() async throws { + let testUndoManager = UndoManager() + + try await withDependencies { + let database = try! makeTestDatabase() + $0.defaultDatabase = database + $0.installDefaultUndoStack(testUndoManager) + $0.defaultUndoEngine = try! UndoEngine(for: database, tables: TestRecord.self) + } operation: { + @Dependency(\.defaultDatabase) var database + + let store = TestStore(initialState: TestFeature.State()) { + TestFeature() + } + + await store.send(.insertItem) + await store.receive(\.itemInserted) + + let countBefore = try await database.read { db in try TestRecord.all.fetchCount(db) } + #expect(countBefore == 1) + + testUndoManager.undo() + + let countAfter = try await database.read { db in try TestRecord.all.fetchCount(db) } + #expect(countAfter == 0) + } + } + + /// `.task(id: undoManager)` re-fires whenever the environment's UndoManager changes + /// identity, so `.set` arrives more than once and resubscribes to the event stream. + @Test + func resettingUndoManagerKeepsEventsFlowing() async throws { + let testUndoManager = UndoManager() + + await withDependencies { + let database = try! makeTestDatabase() + $0.defaultDatabase = database + $0.installDefaultUndoStack(testUndoManager) + $0.defaultUndoEngine = try! UndoEngine(for: database, tables: TestRecord.self) + } operation: { + let store = TestStore(initialState: TestFeature.State()) { + TestFeature() + } + + let first = await store.send(.undoManager(.set(testUndoManager))) + let second = await store.send(.undoManager(.set(testUndoManager))) + + await store.send(.insertItem) + await store.receive(\.itemInserted) + + testUndoManager.undo() + + await store.receive(\.undoManager.event) + + await first.cancel() + await second.cancel() + } + } + + /// Two windows, each with its own UndoStack and UndoManager, observe their own + /// events independently — mounting the second leaves the first's subscription intact. + @Test + func eachWindowObservesItsOwnEventsIndependently() async throws { + let managerA = UndoManager() + let managerB = UndoManager() + let stackA = UndoStack.live(managerA) + let stackB = UndoStack.live(managerB) + + try await withDependencies { + let database = try! makeTestDatabase() + $0.defaultDatabase = database + $0.defaultUndoEngine = try! UndoEngine(for: database, tables: TestRecord.self) + } operation: { + let storeA = withDependencies { + $0.defaultUndoStack = stackA + } operation: { + TestStore(initialState: TestFeature.State()) { TestFeature() } + } + let storeB = withDependencies { + $0.defaultUndoStack = stackB + } operation: { + TestStore(initialState: TestFeature.State()) { TestFeature() } + } + + let subscriptionA = await storeA.send(.undoManager(.set(managerA))) + let subscriptionB = await storeB.send(.undoManager(.set(managerB))) + + await storeA.send(.insertItem) + await storeA.receive(\.itemInserted) + + managerA.undo() + + await storeA.receive(\.undoManager.event) + + await subscriptionA.cancel() + await subscriptionB.cancel() + } + } + } + + // MARK: - Test Feature + + @Reducer + private struct TestFeature { + @ObservableState + struct State: Equatable {} + + enum Action: UndoManageableAction { + case insertItem + case itemInserted + case undoManager(UndoManagingAction) + } + + @Dependency(\.defaultDatabase) var database + + var body: some ReducerOf { + UndoManagingReducer() + Reduce { state, action in + switch action { + case .insertItem: + return .run { [database] send in + try await undoable("Insert Item") { + try await database.write { db in + try TestRecord.insert { TestRecord(id: 1, name: "Test") }.execute(db) + } + } + await send(.itemInserted) + } + + case .itemInserted: + return .none + + case .undoManager: + return .none + } + } + } + } + + // MARK: - Test Helpers + + @Table + private struct TestRecord: Identifiable { + @Column(primaryKey: true) var id: Int + var name: String = "" + var value: Int? + } + + private func makeTestDatabase() throws -> any DatabaseWriter { + let database = try DatabaseQueue(configuration: Configuration()) + + try database.write { db in + try db.execute( + sql: """ + CREATE TABLE "testRecords" ( + "id" INTEGER PRIMARY KEY, + "name" TEXT NOT NULL DEFAULT '', + "value" INTEGER + ) + """ + ) + } + + return database + } +#endif diff --git a/Tests/SQLiteUndoTests/UndoBenchmarkTests.swift b/Tests/SQLiteUndoTests/UndoBenchmarkTests.swift index 765c760..bb47017 100644 --- a/Tests/SQLiteUndoTests/UndoBenchmarkTests.swift +++ b/Tests/SQLiteUndoTests/UndoBenchmarkTests.swift @@ -83,7 +83,7 @@ private func measureUpdate(rows: Int, batched: Bool) throws -> Double { // Update all rows in one barrier let barrier = try engine.withBarrier("Update") { try database.write { db in - try BenchRecord.all.update { $0.value = 42 }.execute(db) + try BenchRecord.all.update { $0.value = #bind(42) }.execute(db) } }! diff --git a/Tests/SQLiteUndoTests/UndoEngineTests.swift b/Tests/SQLiteUndoTests/UndoEngineTests.swift index 04314c5..4f8880f 100644 --- a/Tests/SQLiteUndoTests/UndoEngineTests.swift +++ b/Tests/SQLiteUndoTests/UndoEngineTests.swift @@ -619,7 +619,7 @@ enum UndoEngineTests { try database.write { db in try TestRecord.find(1).update { $0.name = "Updated" - $0.value = 20 + $0.value = #bind(20) }.execute(db) } }! @@ -681,7 +681,7 @@ enum UndoEngineTests { let barrier = try engine.withBarrier("Set Value") { try database.write { db in - try TestRecord.find(1).update { $0.value = 100 }.execute(db) + try TestRecord.find(1).update { $0.value = #bind(100) }.execute(db) } }! @@ -1231,7 +1231,7 @@ enum UndoEngineTests { let barrier = try engine.withBarrier("Bulk Update") { try database.write { db in - try TestRecord.all.update { $0.value = 42 }.execute(db) + try TestRecord.all.update { $0.value = #bind(42) }.execute(db) } }! @@ -1262,7 +1262,7 @@ enum UndoEngineTests { try TestRecord.insert { TestRecord(id: i, name: "Item \(i)") }.execute(db) } for i in 1...250 { - try TestRecord.find(i).update { $0.value = 99 }.execute(db) + try TestRecord.find(i).update { $0.value = #bind(99) }.execute(db) } for i in 251...500 { try TestRecord.find(i).delete().execute(db)