From 3a2deccbfe83b23f1fde59e62177144a790095c7 Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Mon, 3 Aug 2026 16:32:21 -0700 Subject: [PATCH 01/12] drop SQLiteUndoTCA for traits --- Package.resolved | 14 +- Package.swift | 42 ++-- .../SQLiteUndo/ComposableArchitecture.swift | 110 +++++++++ Sources/SQLiteUndoTCA/Exports.swift | 1 - Sources/SQLiteUndoTCA/UndoManaging.swift | 109 --------- .../UndoableEffectTests.swift | 207 ----------------- .../ComposableArchitectureTests.swift | 208 ++++++++++++++++++ 7 files changed, 348 insertions(+), 343 deletions(-) create mode 100644 Sources/SQLiteUndo/ComposableArchitecture.swift delete mode 100644 Sources/SQLiteUndoTCA/Exports.swift delete mode 100644 Sources/SQLiteUndoTCA/UndoManaging.swift delete mode 100644 Tests/SQLiteUndoTCATests/UndoableEffectTests.swift create mode 100644 Tests/SQLiteUndoTests/ComposableArchitectureTests.swift diff --git a/Package.resolved b/Package.resolved index 3f1b31e..46e0e76 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "75bf0478d821e9f021f47f259b924d83b2fefb8965ce5c440228d8abf109c16c", + "originHash" : "616e695cc6b5bc811a6e6f6e44ef4ca51c9d52cfcb6aa57be7b2c070a962dcf5", "pins" : [ { "identity" : "combine-schedulers", @@ -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" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", "state" : { - "revision" : "1eaa6fa2ee57ac42843283b9fd3457af408c858d", - "version" : "1.25.5" + "revision" : "ead11e04e5011c437722c1990d22f80d87056978", + "version" : "1.26.1" } }, { @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-navigation", "state" : { - "revision" : "32f35241b8be0719c4c7f00eb27713b1cadb6248", - "version" : "2.8.0" + "revision" : "418a57db2e757b02817dfd3d8a0374d5794664ab", + "version" : "2.11.0" } }, { diff --git a/Package.swift b/Package.swift index 44b5d49..d39dc6a 100644 --- a/Package.swift +++ b/Package.swift @@ -9,12 +9,22 @@ let package = Package( .macOS(.v13), ], products: [ - .library(name: "SQLiteUndo", targets: ["SQLiteUndo"]), - .library(name: "SQLiteUndoTCA", targets: ["SQLiteUndoTCA"]), + .library(name: "SQLiteUndo", targets: ["SQLiteUndo"]) + ], + traits: [ + .trait( + name: "SQLiteUndoComposableArchitecture", + description: "Enable undo support for the Composable Architecture" + ), + .default(enabledTraits: [ + "SQLiteUndoComposableArchitecture" + ]), ], 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"), @@ -27,13 +37,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: ["SQLiteUndoComposableArchitecture"]) + ), ] ), .testTarget( @@ -45,15 +53,11 @@ 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: ["SQLiteUndoComposableArchitecture"]) + ), ] ), ] diff --git a/Sources/SQLiteUndo/ComposableArchitecture.swift b/Sources/SQLiteUndo/ComposableArchitecture.swift new file mode 100644 index 0000000..b655a68 --- /dev/null +++ b/Sources/SQLiteUndo/ComposableArchitecture.swift @@ -0,0 +1,110 @@ +#if SQLiteUndoComposableArchitecture + 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..b01207d --- /dev/null +++ b/Tests/SQLiteUndoTests/ComposableArchitectureTests.swift @@ -0,0 +1,208 @@ +#if SQLiteUndoComposableArchitecture + 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 From 555e7b48100069bd9b08844f86e0f65f9b437b7a Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Mon, 3 Aug 2026 16:56:55 -0700 Subject: [PATCH 02/12] update examples to run using traits --- Examples/Examples.xcodeproj/project.pbxproj | 62 +-------------- .../xcshareddata/swiftpm/Package.resolved | 79 ++++++++----------- Examples/UndoForMacOS/UndoForMacOSApp.swift | 1 - 3 files changed, 38 insertions(+), 104 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 01be9b6..0de6000 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -8,15 +8,7 @@ /* 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 */ @@ -37,15 +29,7 @@ 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; }; @@ -88,16 +72,8 @@ ); name = UndoForMacOS; packageProductDependencies = ( - C4B197692F33B52B001EAFC2 /* SQLiteUndo */, - C4B197702F33B5D9001EAFC2 /* ComposableArchitecture */, - C4B197EF2F33C28A001EAFC2 /* SQLiteUndo */, - C4B197F12F33C28A001EAFC2 /* SQLiteUndoTCA */, - C4B197F42F33C2B0001EAFC2 /* SQLiteUndo */, - C4B197F62F33C2B0001EAFC2 /* SQLiteUndoTCA */, C441694E2F38F3B100051412 /* SQLiteUndo */, - C44169502F38F3B100051412 /* SQLiteUndoTCA */, - C45245EE2F3D353800F31BB8 /* SQLiteUndo */, - C45245F02F3D353800F31BB8 /* SQLiteUndoTCA */, + C4B197702F33B5D9001EAFC2 /* ComposableArchitecture */, ); productName = UndoForMacOS; productReference = C4B1975D2F33B502001EAFC2 /* UndoForMacOS.app */; @@ -384,6 +360,8 @@ kind = upToNextMajorVersion; minimumVersion = 1.23.1; }; + traits = ( + ); }; /* End XCRemoteSwiftPackageReference section */ @@ -392,45 +370,11 @@ 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..d814f97 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "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" : "418a57db2e757b02817dfd3d8a0374d5794664ab", + "version" : "2.11.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 From 7185f6aacadda0c33e3e058ecf35b41efd7026ca Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Mon, 3 Aug 2026 17:15:38 -0700 Subject: [PATCH 03/12] bump sqlite-data to 0.9.0 with #bind helprs --- Package.resolved | 58 +++++++++---------- Package.swift | 4 +- .../SQLiteUndoTests/UndoBenchmarkTests.swift | 2 +- Tests/SQLiteUndoTests/UndoEngineTests.swift | 8 +-- 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Package.resolved b/Package.resolved index 46e0e76..529326a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "616e695cc6b5bc811a6e6f6e44ef4ca51c9d52cfcb6aa57be7b2c070a962dcf5", + "originHash" : "e07b0bf7824140cc055f20300ebe9e6b2aa2c6308f0f3b929716c07811ec2d2b", "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" } }, { @@ -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" } }, { @@ -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,8 +78,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 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies.git", "state" : { - "revision" : "c79f72b3e67a1eb64f66f76704c22ed6a5c1ed84", - "version" : "1.11.0" + "revision" : "8dc1fbf2f6255a73dec53b4648164884898db4c5", + "version" : "1.14.1" } }, { @@ -114,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" } }, { @@ -123,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" } }, { @@ -132,8 +132,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 +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" } }, { @@ -150,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" } }, { @@ -159,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/Package.swift b/Package.swift index d39dc6a..0c9b4c6 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "sqlite-undo", platforms: [ - .iOS(.v13), + .iOS(.v16), .macOS(.v13), ], products: [ @@ -28,7 +28,7 @@ let package = Package( .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( 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) From dc29d2fb0ccdc0c42e4cb8a75cf8f4a3ca0d8656 Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Mon, 3 Aug 2026 17:16:15 -0700 Subject: [PATCH 04/12] default no traits --- Package.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 0c9b4c6..c03dffd 100644 --- a/Package.swift +++ b/Package.swift @@ -15,10 +15,7 @@ let package = Package( .trait( name: "SQLiteUndoComposableArchitecture", description: "Enable undo support for the Composable Architecture" - ), - .default(enabledTraits: [ - "SQLiteUndoComposableArchitecture" - ]), + ) ], dependencies: [ .package( From 85e8ef6063c032636a312e6ff7b90b213a700cc2 Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Mon, 3 Aug 2026 17:19:42 -0700 Subject: [PATCH 05/12] update example project more --- Examples/Examples.xcodeproj/project.pbxproj | 51 +++++---------------- 1 file changed, 11 insertions(+), 40 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 0de6000..9513442 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -3,12 +3,11 @@ archiveVersion = 1; classes = { }; - objectVersion = 77; + objectVersion = 100; objects = { /* Begin PBXBuildFile section */ C441694F2F38F3B100051412 /* SQLiteUndo in Frameworks */ = {isa = PBXBuildFile; productRef = C441694E2F38F3B100051412 /* SQLiteUndo */; }; - C4B197712F33B5D9001EAFC2 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = C4B197702F33B5D9001EAFC2 /* ComposableArchitecture */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -26,12 +25,9 @@ /* Begin PBXFrameworksBuildPhase section */ C4B1975A2F33B502001EAFC2 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; files = ( C441694F2F38F3B100051412 /* SQLiteUndo in Frameworks */, - C4B197712F33B5D9001EAFC2 /* ComposableArchitecture in Frameworks */, ); - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ @@ -65,15 +61,12 @@ ); buildRules = ( ); - dependencies = ( - ); fileSystemSynchronizedGroups = ( C4B1975E2F33B502001EAFC2 /* UndoForMacOS */, ); name = UndoForMacOS; packageProductDependencies = ( C441694E2F38F3B100051412 /* SQLiteUndo */, - C4B197702F33B5D9001EAFC2 /* ComposableArchitecture */, ); productName = UndoForMacOS; productReference = C4B1975D2F33B502001EAFC2 /* UndoForMacOS.app */; @@ -104,7 +97,6 @@ mainGroup = C4B1973C2F33B4B4001EAFC2; minimizedProjectReferenceProxies = 1; packageReferences = ( - C4B1976F2F33B5D9001EAFC2 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, C45245ED2F3D353800F31BB8 /* XCLocalSwiftPackageReference "../../sqlite-undo" */, ); preferredProjectObjectVersion = 77; @@ -120,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; @@ -200,7 +188,7 @@ }; name = Debug; }; - C4B1974F2F33B4B5001EAFC2 /* Release */ = { + C4B1974F2F33B4B5001EAFC2 /* Release configuration for PBXProject "Examples" */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -254,7 +242,7 @@ }; name = Release; }; - C4B197662F33B503001EAFC2 /* Debug */ = { + C4B197662F33B503001EAFC2 /* Debug configuration for PBXNativeTarget "UndoForMacOS" */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -288,7 +276,7 @@ }; name = Debug; }; - C4B197672F33B503001EAFC2 /* Release */ = { + C4B197672F33B503001EAFC2 /* Release configuration for PBXNativeTarget "UndoForMacOS" */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -328,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 */ @@ -349,32 +335,17 @@ C45245ED2F3D353800F31BB8 /* XCLocalSwiftPackageReference "../../sqlite-undo" */ = { isa = XCLocalSwiftPackageReference; relativePath = "../../sqlite-undo"; - }; -/* 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; - }; traits = ( + SQLiteUndoComposableArchitecture, ); }; -/* End XCRemoteSwiftPackageReference section */ +/* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ C441694E2F38F3B100051412 /* SQLiteUndo */ = { isa = XCSwiftPackageProductDependency; productName = SQLiteUndo; }; - C4B197702F33B5D9001EAFC2 /* ComposableArchitecture */ = { - isa = XCSwiftPackageProductDependency; - package = C4B1976F2F33B5D9001EAFC2 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; - productName = ComposableArchitecture; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = C4B1973D2F33B4B4001EAFC2 /* Project object */; From 40508b950d58ab9f9805bb2a74c7c38b321f263e Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Mon, 3 Aug 2026 17:28:30 -0700 Subject: [PATCH 06/12] updated ci for 26.6 and traits --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0ef77b..3f1154f 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,17 @@ 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"] + xcode: ["26.6"] config: ["debug"] scheme: ["UndoForMacOS"] runs-on: macos-26 - continue-on-error: true steps: - uses: actions/checkout@v5 - name: Select Xcode ${{ matrix.xcode }} From 9f20b1c66760a308f7439de9c44f7c6b04158cea Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Mon, 3 Aug 2026 17:31:17 -0700 Subject: [PATCH 07/12] add xcode workaround --- Package.resolved | 29 +---------------------------- Package.swift | 10 ++++++++++ 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/Package.resolved b/Package.resolved index 529326a..55f7012 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "e07b0bf7824140cc055f20300ebe9e6b2aa2c6308f0f3b929716c07811ec2d2b", + "originHash" : "d535a2b60bbda978242883e6fb6bdfcff22b5591e927d715970168cc1cb004cf", "pins" : [ { "identity" : "combine-schedulers", @@ -28,15 +28,6 @@ "version" : "1.9.0" } }, - { - "identity" : "swift-case-paths", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-case-paths", - "state" : { - "revision" : "794f4b0a9cf32042592388d014f6a1ea987d323a", - "version" : "1.9.1" - } - }, { "identity" : "swift-clocks", "kind" : "remoteSourceControl", @@ -55,15 +46,6 @@ "version" : "1.6.0" } }, - { - "identity" : "swift-composable-architecture", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", - "state" : { - "revision" : "ead11e04e5011c437722c1990d22f80d87056978", - "version" : "1.26.1" - } - }, { "identity" : "swift-concurrency-extras", "kind" : "remoteSourceControl", @@ -100,15 +82,6 @@ "version" : "1.1.1" } }, - { - "identity" : "swift-navigation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-navigation", - "state" : { - "revision" : "418a57db2e757b02817dfd3d8a0374d5794664ab", - "version" : "2.11.0" - } - }, { "identity" : "swift-perception", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index c03dffd..c5bdead 100644 --- a/Package.swift +++ b/Package.swift @@ -1,5 +1,6 @@ // swift-tools-version: 6.1 +import Foundation import PackageDescription let package = Package( @@ -59,3 +60,12 @@ let package = Package( ), ] ) + +// 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: ["SQLiteUndoComposableArchitecture"]) + ) +} From 222a5ffb03c7e1b5be1f5e52a9b42e6918675b1c Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Mon, 3 Aug 2026 17:34:59 -0700 Subject: [PATCH 08/12] fix trait name --- Package.swift | 8 ++++---- Sources/SQLiteUndo/ComposableArchitecture.swift | 2 +- Tests/SQLiteUndoTests/ComposableArchitectureTests.swift | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Package.swift b/Package.swift index c5bdead..f2e3406 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], traits: [ .trait( - name: "SQLiteUndoComposableArchitecture", + name: "ComposableArchitecture", description: "Enable undo support for the Composable Architecture" ) ], @@ -38,7 +38,7 @@ let package = Package( .product( name: "ComposableArchitecture", package: "swift-composable-architecture", - condition: .when(traits: ["SQLiteUndoComposableArchitecture"]) + condition: .when(traits: ["ComposableArchitecture"]) ), ] ), @@ -54,7 +54,7 @@ let package = Package( .product( name: "ComposableArchitecture", package: "swift-composable-architecture", - condition: .when(traits: ["SQLiteUndoComposableArchitecture"]) + condition: .when(traits: ["ComposableArchitecture"]) ), ] ), @@ -66,6 +66,6 @@ let package = Package( if ProcessInfo.processInfo.environment["SPI_GENERATE_DOCS"] != nil // || true { package.traits.insert( - .default(enabledTraits: ["SQLiteUndoComposableArchitecture"]) + .default(enabledTraits: ["ComposableArchitecture"]) ) } diff --git a/Sources/SQLiteUndo/ComposableArchitecture.swift b/Sources/SQLiteUndo/ComposableArchitecture.swift index b655a68..28fc2ea 100644 --- a/Sources/SQLiteUndo/ComposableArchitecture.swift +++ b/Sources/SQLiteUndo/ComposableArchitecture.swift @@ -1,4 +1,4 @@ -#if SQLiteUndoComposableArchitecture +#if ComposableArchitecture import CasePaths import ComposableArchitecture import Foundation diff --git a/Tests/SQLiteUndoTests/ComposableArchitectureTests.swift b/Tests/SQLiteUndoTests/ComposableArchitectureTests.swift index b01207d..e9a3c9a 100644 --- a/Tests/SQLiteUndoTests/ComposableArchitectureTests.swift +++ b/Tests/SQLiteUndoTests/ComposableArchitectureTests.swift @@ -1,4 +1,4 @@ -#if SQLiteUndoComposableArchitecture +#if ComposableArchitecture import ComposableArchitecture import DependenciesTestSupport import Foundation From 3c7035e8fe603c0ae4d3a2b0bfa172daadb60615 Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Mon, 3 Aug 2026 17:37:33 -0700 Subject: [PATCH 09/12] fixup --- Examples/Examples.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 29 +------------------ 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 9513442..f5ee115 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -336,7 +336,7 @@ isa = XCLocalSwiftPackageReference; relativePath = "../../sqlite-undo"; traits = ( - SQLiteUndoComposableArchitecture, + ComposableArchitecture, ); }; /* End XCLocalSwiftPackageReference section */ diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d814f97..a68a3d6 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "09bc66dffe4811b4627872585613011627e6f6341a227183a35983c8f373e434", + "originHash" : "bd37d1fc9d32651eefdd5312e63688e1986e36146747a4ca919d4575aab6cb36", "pins" : [ { "identity" : "combine-schedulers", @@ -28,15 +28,6 @@ "version" : "1.9.0" } }, - { - "identity" : "swift-case-paths", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-case-paths", - "state" : { - "revision" : "794f4b0a9cf32042592388d014f6a1ea987d323a", - "version" : "1.9.1" - } - }, { "identity" : "swift-clocks", "kind" : "remoteSourceControl", @@ -55,15 +46,6 @@ "version" : "1.6.0" } }, - { - "identity" : "swift-composable-architecture", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", - "state" : { - "revision" : "ead11e04e5011c437722c1990d22f80d87056978", - "version" : "1.26.1" - } - }, { "identity" : "swift-concurrency-extras", "kind" : "remoteSourceControl", @@ -100,15 +82,6 @@ "version" : "1.1.1" } }, - { - "identity" : "swift-navigation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-navigation", - "state" : { - "revision" : "418a57db2e757b02817dfd3d8a0374d5794664ab", - "version" : "2.11.0" - } - }, { "identity" : "swift-perception", "kind" : "remoteSourceControl", From 0454ddb27b41b25774d0f08c8e5bcc7cf3030209 Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Mon, 3 Aug 2026 17:42:01 -0700 Subject: [PATCH 10/12] dep --- .../xcshareddata/swiftpm/Package.resolved | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a68a3d6..37ceb54 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -28,6 +28,15 @@ "version" : "1.9.0" } }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "794f4b0a9cf32042592388d014f6a1ea987d323a", + "version" : "1.9.1" + } + }, { "identity" : "swift-clocks", "kind" : "remoteSourceControl", @@ -46,6 +55,15 @@ "version" : "1.6.0" } }, + { + "identity" : "swift-composable-architecture", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", + "state" : { + "revision" : "ead11e04e5011c437722c1990d22f80d87056978", + "version" : "1.26.1" + } + }, { "identity" : "swift-concurrency-extras", "kind" : "remoteSourceControl", @@ -82,6 +100,15 @@ "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", From 66f42a528abeb808d12eee7e01a4c7e7683cbb20 Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Mon, 3 Aug 2026 20:31:08 -0700 Subject: [PATCH 11/12] updated readme --- .github/workflows/ci.yml | 1 - README.md | 39 ++++++++++++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f1154f..810c4c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,6 @@ jobs: strategy: matrix: xcode: ["26.6"] - config: ["debug"] scheme: ["UndoForMacOS"] runs-on: macos-26 steps: diff --git a/README.md b/README.md index 2ebe28e..79a326f 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 it on, `swift test` reports 59 tests instead of 57, and the trait-off path stops being compiled anywhere. + ## License This library is released under the MIT license. See [LICENSE](LICENSE) for details. From 82120e15fa876fefc52cb70bff58d61003d54ff1 Mon Sep 17 00:00:00 2001 From: Ryan Carver Date: Tue, 4 Aug 2026 12:33:14 -0700 Subject: [PATCH 12/12] fixup --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 79a326f..26a6ed6 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,7 @@ 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 it on, `swift test` reports 59 tests instead of 57, and the trait-off path stops being compiled anywhere. +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