From a03571e6ba9fdbc826b0c336a8f23a21e7142464 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 5 Oct 2026 21:12:19 +0400 Subject: [PATCH 1/6] feat!: adopt Swift 6.2 strict concurrency and update TCA to 1.26.2 --- CHANGELOG.md | 8 ++ Package.resolved | 81 ++++++++++++------- Package.swift | 5 +- ...wift-5.8.swift => Package@swift-5.10.swift | 23 ++++-- ...swift-5.7.swift => Package@swift-6.0.swift | 13 ++- README.md | 4 +- .../Classes/Core/Paginator/IPaginator.swift | 4 +- .../PageLoader/ICursorPageLoader.swift | 4 +- .../PageLoader/IOffsetPageLoader.swift | 4 +- .../Classes/Core/Paginator/Paginator.swift | 4 +- .../Strategies/CursorSeekStrategy.swift | 2 +- .../Strategies/LimitOffsetStrategy.swift | 2 +- .../Protocols/IPaginationStrategy.swift | 2 +- .../Configuration/PaginationCursorSeek.swift | 4 + .../Configuration/PaginationLimitOffset.swift | 4 + Sources/Blade/Classes/Models/Page.swift | 4 + .../Requests/CursorPaginationRequest.swift | 4 + .../Requests/OffsetPaginationRequest.swift | 4 + .../CursorPositionBuilderStrategy.swift | 2 +- .../OffsetPositionBuilderStrategy.swift | 2 +- .../Protocols/IPositionBuilderStrategy.swift | 4 +- .../CursorRequestBuilderStrategy.swift | 2 +- .../OffsetRequestBuilderStrategy.swift | 2 +- .../Protocols/IRequestBuilderStrategy.swift | 4 +- .../PaginatorIntegrationReducer.swift | 13 +-- .../Reducers/Internal/PaginatorReducer.swift | 8 +- .../Classes/COre/Reducers/Reducer+.swift | 8 +- .../Classes/Models/PaginatorAction.swift | 10 ++- .../Models/State/CursorPaginatorState.swift | 2 +- .../Classes/Models/State/PaginatorState.swift | 6 +- .../Views/PaginatorForEachView.swift | 8 +- .../Views/PaginatorListView.swift | 9 +-- .../Presentation/Views/PaginatorView.swift | 9 +-- Tests/BladeTCATests/Models/TestItem.swift | 2 +- .../Mocks/CursorPageLoaderMock.swift | 3 +- .../Mocks/OffsetPageLoaderMock.swift | 3 +- 36 files changed, 177 insertions(+), 96 deletions(-) rename Package@swift-5.8.swift => Package@swift-5.10.swift (53%) rename Package@swift-5.7.swift => Package@swift-6.0.swift (73%) diff --git a/CHANGELOG.md b/CHANGELOG.md index d369d25..4a80ca9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. #### 1.x Releases - `1.0.x` Releases - [1.0.0](#100) | [1.1.0](#110) +## Unreleased + +#### Changed +- Adopt Swift 6 language mode with strict concurrency (Swift 6.2 toolchain). Swift 6.0 uses Swift 6 mode, Swift 5.10 uses Swift 5 mode with `StrictConcurrency`. +- **Breaking:** public generics and loader/strategy protocols now require `Sendable` (`Element`, `State`, `Action`, `Request`, `ICursorPageLoader`, `IOffsetPageLoader`, `IPaginator`). +- Update `swift-composable-architecture` to 1.26.2 (Swift 6.1+). Older toolchains resolve an older compatible TCA. +- Minimum Swift toolchain is now 5.10 (`Package@swift-5.7.swift` and `Package@swift-5.8.swift` removed). + ## [1.1.0](https://github.com/space-code/blade/releases/tag/1.1.0) Released on 2024-08-26. diff --git a/Package.resolved b/Package.resolved index 4284f1c..01f99ca 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,12 +1,13 @@ { + "originHash" : "8864fcce19fd29e265bca9677fa7284a69299f0681e7a92417ccc3ce78a81f64", "pins" : [ { "identity" : "combine-schedulers", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", - "version" : "1.0.0" + "revision" : "114354e8c1667a2edc4993700fb9fa4f90157b56", + "version" : "1.2.2" } }, { @@ -14,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "76d7791b5bda47df7e3d4690c4c3aaf089730707", - "version" : "1.2.1" + "revision" : "cb4cba6a8a46a86db0d37dbe09935ead6b24c91a", + "version" : "1.10.0" } }, { @@ -23,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", - "version" : "1.0.2" + "revision" : "82440fa0a8b1c381a6d1e8e0fc7bbba53ec63204", + "version" : "1.1.1" } }, { @@ -32,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", - "version" : "1.0.6" + "revision" : "98ef3c98609a1e31b7e157b5b619579001a789d6", + "version" : "1.7.1" } }, { @@ -41,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", "state" : { - "revision" : "ae491c9e3f66631e72d58db8bb4c27dfc3d3afd4", - "version" : "1.6.0" + "revision" : "377da4061db10d26337a71bb279c506bb951f50f", + "version" : "1.26.2" } }, { @@ -50,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", - "version" : "1.1.0" + "revision" : "5fa253428866f2360c3754e88537f700ed2656b5", + "version" : "1.4.1" } }, { @@ -59,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605", - "version" : "1.1.2" + "revision" : "4fb9e7cbb4b8f5a7354005db39447f570d6b3e19", + "version" : "1.7.3" } }, { @@ -68,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "adb04a8e35f07edc001877af9f9f97fcc21d409e", - "version" : "1.2.0" + "revision" : "b476cc5761057d8d857de063fe7ac8daecf5c06b", + "version" : "1.17.1" } }, { @@ -77,37 +78,55 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-identified-collections", "state" : { - "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", - "version" : "1.0.0" + "revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597", + "version" : "1.1.1" } }, { - "identity" : "swift-syntax", + "identity" : "swift-issue-reporting", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-issue-reporting", + "state" : { + "revision" : "75b000cea2ca6d7527a57e1b9cc0bdf21a9abe9f", + "version" : "2.1.1" + } + }, + { + "identity" : "swift-navigation", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax", + "location" : "https://github.com/pointfreeco/swift-navigation", "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" + "revision" : "7e95e5e9ff0a64f6ad8bca59083f8d6ba381b6ff", + "version" : "2.11.2" } }, { - "identity" : "swiftui-navigation", + "identity" : "swift-perception", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swiftui-navigation", + "location" : "https://github.com/pointfreeco/swift-perception", "state" : { - "revision" : "78f9d72cf667adb47e2040aa373185c88c63f0dc", - "version" : "1.2.0" + "revision" : "597afd46249f4a71885b95d26b26cc5b893daf32", + "version" : "2.0.12" } }, { - "identity" : "xctest-dynamic-overlay", + "identity" : "swift-sharing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-sharing", + "state" : { + "revision" : "3552faf8a6c18ce896ec5a72bd8c34755f0f03e0", + "version" : "2.10.1" + } + }, + { + "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "b58e6627149808b40634c4552fcf2f44d0b3ca87", - "version" : "1.1.0" + "revision" : "050f1a346fbbac0ca2cfb15a95274f7bd1cf0ccf", + "version" : "604.0.0" } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index ffa6db4..923de30 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -36,5 +36,6 @@ let package = Package( .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), ] ), - ] + ], + swiftLanguageModes: [.v6] ) diff --git a/Package@swift-5.8.swift b/Package@swift-5.10.swift similarity index 53% rename from Package@swift-5.8.swift rename to Package@swift-5.10.swift index ff005b6..ffd1937 100644 --- a/Package@swift-5.8.swift +++ b/Package@swift-5.10.swift @@ -1,8 +1,10 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 5.10 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription +let strictConcurrency: [SwiftSetting] = [.enableExperimentalFeature("StrictConcurrency")] + let package = Package( name: "Blade", platforms: [ @@ -10,6 +12,7 @@ let package = Package( .macOS(.v13), .tvOS(.v16), .watchOS(.v9), + .visionOS(.v1), ], products: [ .library(name: "Blade", targets: ["Blade"]), @@ -19,14 +22,24 @@ let package = Package( .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", .upToNextMajor(from: "1.5.5")), ], targets: [ - .target(name: "Blade"), + .target(name: "Blade", swiftSettings: strictConcurrency), .target( name: "BladeTCA", dependencies: [ "Blade", .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - ] + ], + swiftSettings: strictConcurrency ), - .testTarget(name: "BladeTests", dependencies: ["Blade"]), - ] + .testTarget(name: "BladeTests", dependencies: ["Blade"], swiftSettings: strictConcurrency), + .testTarget( + name: "BladeTCATests", + dependencies: [ + "BladeTCA", + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + ], + swiftSettings: strictConcurrency + ), + ], + swiftLanguageVersions: [.v5] ) diff --git a/Package@swift-5.7.swift b/Package@swift-6.0.swift similarity index 73% rename from Package@swift-5.7.swift rename to Package@swift-6.0.swift index 2eef0a0..519cd47 100644 --- a/Package@swift-5.7.swift +++ b/Package@swift-6.0.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -10,6 +10,7 @@ let package = Package( .macOS(.v13), .tvOS(.v16), .watchOS(.v9), + .visionOS(.v1), ], products: [ .library(name: "Blade", targets: ["Blade"]), @@ -28,5 +29,13 @@ let package = Package( ] ), .testTarget(name: "BladeTests", dependencies: ["Blade"]), - ] + .testTarget( + name: "BladeTCATests", + dependencies: [ + "BladeTCA", + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + ] + ), + ], + swiftLanguageModes: [.v6] ) diff --git a/README.md b/README.md index b49231c..d39e799 100644 --- a/README.md +++ b/README.md @@ -157,8 +157,8 @@ struct SomeView: View { ## Requirements - iOS 16.0+ / macOS 13.0+ / tvOS 16.0+ / watchOS 9.0+ / visionOS 1.0+ -- Xcode 15.0 -- Swift 5.7 +- Xcode 15.3+ (Swift 5.10+); Swift 6 language mode on Swift 6.0+ +- Swift 5.10+ (TCA 1.26.2 is used on Swift 6.1+, older TCA is resolved automatically on 5.10 and 6.0) ## Installation ### Swift Package Manager diff --git a/Sources/Blade/Classes/Core/Paginator/IPaginator.swift b/Sources/Blade/Classes/Core/Paginator/IPaginator.swift index b921e6d..23bed43 100644 --- a/Sources/Blade/Classes/Core/Paginator/IPaginator.swift +++ b/Sources/Blade/Classes/Core/Paginator/IPaginator.swift @@ -6,9 +6,9 @@ import Foundation /// Protocol defining the interface for a paginator that manages asynchronous loading of paginated data. -public protocol IPaginator { +public protocol IPaginator: Sendable { /// The type of elements the paginator is handling, which must conform to `Decodable` & `Equatable`. - associatedtype Element: Decodable & Equatable + associatedtype Element: Decodable & Equatable & Sendable /// Asynchronously refreshes the paginator, resetting to the first page and fetching new data. /// diff --git a/Sources/Blade/Classes/Core/Paginator/PageLoader/ICursorPageLoader.swift b/Sources/Blade/Classes/Core/Paginator/PageLoader/ICursorPageLoader.swift index 1424f7e..b1af6b7 100644 --- a/Sources/Blade/Classes/Core/Paginator/PageLoader/ICursorPageLoader.swift +++ b/Sources/Blade/Classes/Core/Paginator/PageLoader/ICursorPageLoader.swift @@ -5,9 +5,9 @@ import Foundation -public protocol ICursorPageLoader { +public protocol ICursorPageLoader: Sendable { /// The type of elements the paginator is handling, which must conform to `Decodable` & `Equatable`. - associatedtype Element: Decodable & Equatable & Identifiable + associatedtype Element: Decodable & Equatable & Identifiable & Sendable where Element.ID: Sendable /// Loads a page of elements based on the provided pagination request asynchronously. /// diff --git a/Sources/Blade/Classes/Core/Paginator/PageLoader/IOffsetPageLoader.swift b/Sources/Blade/Classes/Core/Paginator/PageLoader/IOffsetPageLoader.swift index 140a149..cda9865 100644 --- a/Sources/Blade/Classes/Core/Paginator/PageLoader/IOffsetPageLoader.swift +++ b/Sources/Blade/Classes/Core/Paginator/PageLoader/IOffsetPageLoader.swift @@ -5,9 +5,9 @@ import Foundation -public protocol IOffsetPageLoader { +public protocol IOffsetPageLoader: Sendable { /// The type of elements the paginator is handling, which must conform to `Decodable` & `Equatable`. - associatedtype Element: Decodable & Equatable + associatedtype Element: Decodable & Equatable & Sendable /// Loads a page of elements based on the provided pagination request asynchronously. /// diff --git a/Sources/Blade/Classes/Core/Paginator/Paginator.swift b/Sources/Blade/Classes/Core/Paginator/Paginator.swift index 32789c5..9cacacb 100644 --- a/Sources/Blade/Classes/Core/Paginator/Paginator.swift +++ b/Sources/Blade/Classes/Core/Paginator/Paginator.swift @@ -8,7 +8,7 @@ import Foundation // MARK: - Paginator /// Paginator is an actor responsible for paginating and loading data using a provided paginator service. -public actor Paginator { +public actor Paginator { // MARK: Types /// Enum representing errors that may occur during pagination. @@ -48,7 +48,7 @@ public actor Paginator { public init( configuration: PaginationCursorSeek, cursorPageLoader: any ICursorPageLoader - ) where T: Identifiable { + ) where T: Identifiable, T.ID: Sendable { paginationStrategy = CursorSeekStrategy( configuration: configuration, pageLoader: cursorPageLoader diff --git a/Sources/Blade/Classes/Core/Paginator/Strategies/CursorSeekStrategy.swift b/Sources/Blade/Classes/Core/Paginator/Strategies/CursorSeekStrategy.swift index 804dd4e..68d0148 100644 --- a/Sources/Blade/Classes/Core/Paginator/Strategies/CursorSeekStrategy.swift +++ b/Sources/Blade/Classes/Core/Paginator/Strategies/CursorSeekStrategy.swift @@ -7,7 +7,7 @@ import Foundation // MARK: - CursorSeekStrategy -actor CursorSeekStrategy { +actor CursorSeekStrategy where Element.ID: Sendable { // MARK: Properties private let configuration: PaginationCursorSeek diff --git a/Sources/Blade/Classes/Core/Paginator/Strategies/LimitOffsetStrategy.swift b/Sources/Blade/Classes/Core/Paginator/Strategies/LimitOffsetStrategy.swift index e3bfc41..e347bc4 100644 --- a/Sources/Blade/Classes/Core/Paginator/Strategies/LimitOffsetStrategy.swift +++ b/Sources/Blade/Classes/Core/Paginator/Strategies/LimitOffsetStrategy.swift @@ -7,7 +7,7 @@ import Foundation // MARK: - LimitOffsetStrategy -actor LimitOffsetStrategy { +actor LimitOffsetStrategy { // MARK: Properties private let configuration: PaginationLimitOffset diff --git a/Sources/Blade/Classes/Core/Paginator/Strategies/Protocols/IPaginationStrategy.swift b/Sources/Blade/Classes/Core/Paginator/Strategies/Protocols/IPaginationStrategy.swift index a0f6e86..654ef27 100644 --- a/Sources/Blade/Classes/Core/Paginator/Strategies/Protocols/IPaginationStrategy.swift +++ b/Sources/Blade/Classes/Core/Paginator/Strategies/Protocols/IPaginationStrategy.swift @@ -5,4 +5,4 @@ import Foundation -protocol IPaginationStrategy: IPaginator where Element: Decodable & Equatable {} +protocol IPaginationStrategy: IPaginator where Element: Decodable & Equatable & Sendable {} diff --git a/Sources/Blade/Classes/Models/Configuration/PaginationCursorSeek.swift b/Sources/Blade/Classes/Models/Configuration/PaginationCursorSeek.swift index dd57163..ddd12b4 100644 --- a/Sources/Blade/Classes/Models/Configuration/PaginationCursorSeek.swift +++ b/Sources/Blade/Classes/Models/Configuration/PaginationCursorSeek.swift @@ -16,3 +16,7 @@ public struct PaginationCursorSeek { self.id = id } } + +// MARK: - Sendable + +extension PaginationCursorSeek: Sendable where T.ID: Sendable {} diff --git a/Sources/Blade/Classes/Models/Configuration/PaginationLimitOffset.swift b/Sources/Blade/Classes/Models/Configuration/PaginationLimitOffset.swift index 0ce0315..063c35b 100644 --- a/Sources/Blade/Classes/Models/Configuration/PaginationLimitOffset.swift +++ b/Sources/Blade/Classes/Models/Configuration/PaginationLimitOffset.swift @@ -18,3 +18,7 @@ public struct PaginationLimitOffset { self.limit = limit } } + +// MARK: - Sendable + +extension PaginationLimitOffset: Sendable {} diff --git a/Sources/Blade/Classes/Models/Page.swift b/Sources/Blade/Classes/Models/Page.swift index 5b99678..fe8b96e 100644 --- a/Sources/Blade/Classes/Models/Page.swift +++ b/Sources/Blade/Classes/Models/Page.swift @@ -28,3 +28,7 @@ public struct Page: Equatable { self.hasMoreData = hasMoreData } } + +// MARK: - Sendable + +extension Page: Sendable where T: Sendable {} diff --git a/Sources/Blade/Classes/Models/Requests/CursorPaginationRequest.swift b/Sources/Blade/Classes/Models/Requests/CursorPaginationRequest.swift index eea7eb3..47075b4 100644 --- a/Sources/Blade/Classes/Models/Requests/CursorPaginationRequest.swift +++ b/Sources/Blade/Classes/Models/Requests/CursorPaginationRequest.swift @@ -16,3 +16,7 @@ public struct CursorPaginationRequest: Equatable { self.id = id } } + +// MARK: - Sendable + +extension CursorPaginationRequest: Sendable where T.ID: Sendable {} diff --git a/Sources/Blade/Classes/Models/Requests/OffsetPaginationRequest.swift b/Sources/Blade/Classes/Models/Requests/OffsetPaginationRequest.swift index ff30950..df0f9a7 100644 --- a/Sources/Blade/Classes/Models/Requests/OffsetPaginationRequest.swift +++ b/Sources/Blade/Classes/Models/Requests/OffsetPaginationRequest.swift @@ -29,3 +29,7 @@ public struct OffsetPaginationRequest: Equatable { self.offset = offset } } + +// MARK: - Sendable + +extension OffsetPaginationRequest: Sendable {} diff --git a/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/PositionBuilders/CursorPositionBuilderStrategy.swift b/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/PositionBuilders/CursorPositionBuilderStrategy.swift index f2dd580..c3043c5 100644 --- a/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/PositionBuilders/CursorPositionBuilderStrategy.swift +++ b/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/PositionBuilders/CursorPositionBuilderStrategy.swift @@ -6,7 +6,7 @@ import Foundation /// A cursor-based paginator position builder. -struct CursorPositionBuilderStrategy: IPositionBuilderStrategy { +struct CursorPositionBuilderStrategy: IPositionBuilderStrategy where State.ID: Sendable { /// Creates a next position. /// /// - Parameter state: The current state of the paginator. diff --git a/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/PositionBuilders/OffsetPositionBuilderStrategy.swift b/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/PositionBuilders/OffsetPositionBuilderStrategy.swift index 418fabe..ebbfce0 100644 --- a/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/PositionBuilders/OffsetPositionBuilderStrategy.swift +++ b/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/PositionBuilders/OffsetPositionBuilderStrategy.swift @@ -6,7 +6,7 @@ import Blade /// A offset-based paginator position builder. -struct OffsetPositionBuilderStrategy: IPositionBuilderStrategy { +struct OffsetPositionBuilderStrategy: IPositionBuilderStrategy { /// Creates a next position. /// /// - Parameter state: The current state of the paginator. diff --git a/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/PositionBuilders/Protocols/IPositionBuilderStrategy.swift b/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/PositionBuilders/Protocols/IPositionBuilderStrategy.swift index 3f85ff6..a0dbac6 100644 --- a/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/PositionBuilders/Protocols/IPositionBuilderStrategy.swift +++ b/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/PositionBuilders/Protocols/IPositionBuilderStrategy.swift @@ -6,9 +6,9 @@ import Foundation /// This protocol defines the interface for a strategy used in building positions based on a given state. -protocol IPositionBuilderStrategy { +protocol IPositionBuilderStrategy: Sendable { associatedtype State: Equatable - associatedtype PositionType: Equatable + associatedtype PositionType: Equatable & Sendable & Sendable /// Takes a state as input and returns the corresponding position. /// diff --git a/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/RequestBuilders/CursorRequestBuilderStrategy.swift b/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/RequestBuilders/CursorRequestBuilderStrategy.swift index d1ca2fc..fd6bc47 100644 --- a/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/RequestBuilders/CursorRequestBuilderStrategy.swift +++ b/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/RequestBuilders/CursorRequestBuilderStrategy.swift @@ -6,7 +6,7 @@ import Blade /// A request builder strategy for cursor-based pagination. -struct CursorRequestBuilderStrategy: IRequestBuilderStrategy { +struct CursorRequestBuilderStrategy: IRequestBuilderStrategy where State.ID: Sendable { // MARK: IRequestBuilderStrategy /// Constructs a pagination request based on the provided state. diff --git a/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/RequestBuilders/OffsetRequestBuilderStrategy.swift b/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/RequestBuilders/OffsetRequestBuilderStrategy.swift index 1fc7aea..b4b2772 100644 --- a/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/RequestBuilders/OffsetRequestBuilderStrategy.swift +++ b/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/RequestBuilders/OffsetRequestBuilderStrategy.swift @@ -7,7 +7,7 @@ import Blade import Foundation /// A request builder strategy for offset-based pagination. -struct OffsetRequestBuilderStrategy: IRequestBuilderStrategy { +struct OffsetRequestBuilderStrategy: IRequestBuilderStrategy { // MARK: Properties /// The maximum number of items to be included in the pagination request. diff --git a/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/RequestBuilders/Protocols/IRequestBuilderStrategy.swift b/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/RequestBuilders/Protocols/IRequestBuilderStrategy.swift index c11a87b..7904cf9 100644 --- a/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/RequestBuilders/Protocols/IRequestBuilderStrategy.swift +++ b/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/RequestBuilders/Protocols/IRequestBuilderStrategy.swift @@ -6,12 +6,12 @@ import Foundation /// A protocol for defining request builder strategies in a paginator. -protocol IRequestBuilderStrategy { +protocol IRequestBuilderStrategy: Sendable { /// The state type associated with the strategy, conforming to Equatable. associatedtype State: Equatable /// The request type associated with the strategy, conforming to Equatable. - associatedtype Request: Equatable + associatedtype Request: Equatable & Sendable /// Makes a request. /// diff --git a/Sources/BladeTCA/Classes/COre/Reducers/Internal/PaginatorIntegrationReducer.swift b/Sources/BladeTCA/Classes/COre/Reducers/Internal/PaginatorIntegrationReducer.swift index a05aa9a..ca6495a 100644 --- a/Sources/BladeTCA/Classes/COre/Reducers/Internal/PaginatorIntegrationReducer.swift +++ b/Sources/BladeTCA/Classes/COre/Reducers/Internal/PaginatorIntegrationReducer.swift @@ -8,11 +8,11 @@ import ComposableArchitecture struct PaginatorIntegrationReducer< Parent: Reducer, - State: Equatable & Identifiable, - Action: Equatable, - PositionType: Equatable, - Request: Equatable ->: Reducer { + State: Equatable & Identifiable & Sendable, + Action: Equatable & Sendable, + PositionType: Equatable & Sendable, + Request: Equatable & Sendable +>: Reducer where Parent.State: Sendable { // MARK: Properties let parent: Parent @@ -41,6 +41,9 @@ struct PaginatorIntegrationReducer< return parent.reduce(into: &state, action: action) } + let childAction = childAction + let loadPage = loadPage + return .run { [state] send in await send(childAction.embed(.response(TaskResult { try await loadPage(pageRequest, state) }))) } diff --git a/Sources/BladeTCA/Classes/COre/Reducers/Internal/PaginatorReducer.swift b/Sources/BladeTCA/Classes/COre/Reducers/Internal/PaginatorReducer.swift index 0797a40..1ac708c 100644 --- a/Sources/BladeTCA/Classes/COre/Reducers/Internal/PaginatorReducer.swift +++ b/Sources/BladeTCA/Classes/COre/Reducers/Internal/PaginatorReducer.swift @@ -7,10 +7,10 @@ import Blade import ComposableArchitecture struct PaginatorReducer< - State: Equatable & Identifiable, - Action: Equatable, - PositionType: Equatable, - Request: Equatable + State: Equatable & Identifiable & Sendable, + Action: Equatable & Sendable, + PositionType: Equatable & Sendable, + Request: Equatable & Sendable >: Reducer { // MARK: Types diff --git a/Sources/BladeTCA/Classes/COre/Reducers/Reducer+.swift b/Sources/BladeTCA/Classes/COre/Reducers/Reducer+.swift index 093485e..fd2ae14 100644 --- a/Sources/BladeTCA/Classes/COre/Reducers/Reducer+.swift +++ b/Sources/BladeTCA/Classes/COre/Reducers/Reducer+.swift @@ -9,7 +9,7 @@ import ComposableArchitecture // MARK: - Reducer Extension for Paginator Integration /// An extension on the `Reducer` type providing a method for integrating a paginator into a Composable Architecture. -public extension Reducer { +public extension Reducer where State: Sendable { /// Integrates a paginator into a Composable Architecture, facilitating paginated data loading. /// /// - Parameters: @@ -19,7 +19,7 @@ public extension Reducer { /// - loadPage: A closure to load a page of items based on the provided `LimitPageRequest` and current state. /// /// - Returns: A reducer for integrating the paginator functionality. - func paginator( + func paginator( limit: Int = 20, state: WritableKeyPath>, action: AnyCasePath>, @@ -43,11 +43,11 @@ public extension Reducer { /// - loadPage: A closure to load a page of items based on the provided `CursorPaginationRequest` and current state. /// /// - Returns: A reducer for integrating the paginator functionality. - func paginator( + func paginator( state: WritableKeyPath>, action: AnyCasePath>>, loadPage: @Sendable @escaping (CursorPaginationRequest, State) async throws -> Page - ) -> some Reducer { + ) -> some Reducer where ItemState.ID: Sendable { PaginatorIntegrationReducer( parent: self, childState: state, diff --git a/Sources/BladeTCA/Classes/Models/PaginatorAction.swift b/Sources/BladeTCA/Classes/Models/PaginatorAction.swift index 5a9ad48..37632c3 100644 --- a/Sources/BladeTCA/Classes/Models/PaginatorAction.swift +++ b/Sources/BladeTCA/Classes/Models/PaginatorAction.swift @@ -15,9 +15,9 @@ import Foundation /// - State: The type of state managed by the paginator. /// - Action: The type of actions that can be associated with the paginator. public enum PaginatorAction< - State: Equatable & Identifiable, - Action: Equatable, - Request: Equatable + State: Equatable & Identifiable & Sendable, + Action: Equatable & Sendable, + Request: Equatable & Sendable >: Equatable { // MARK: Action Cases @@ -30,3 +30,7 @@ public enum PaginatorAction< /// Represents the response to a page request, containing the result of the operation. case response(TaskResult>) } + +// MARK: - Sendable + +extension PaginatorAction: Sendable where State.ID: Sendable {} diff --git a/Sources/BladeTCA/Classes/Models/State/CursorPaginatorState.swift b/Sources/BladeTCA/Classes/Models/State/CursorPaginatorState.swift index ac4a397..e018ab4 100644 --- a/Sources/BladeTCA/Classes/Models/State/CursorPaginatorState.swift +++ b/Sources/BladeTCA/Classes/Models/State/CursorPaginatorState.swift @@ -6,7 +6,7 @@ import ComposableArchitecture /// Represents the state of a paginator for cursor-based pagination. -public struct CursorPaginatorState: Equatable, IPaginatorState { +public struct CursorPaginatorState: Equatable, IPaginatorState { // MARK: Properties /// The array of identifiable items managed by the paginator. diff --git a/Sources/BladeTCA/Classes/Models/State/PaginatorState.swift b/Sources/BladeTCA/Classes/Models/State/PaginatorState.swift index e9d4285..4fa88e2 100644 --- a/Sources/BladeTCA/Classes/Models/State/PaginatorState.swift +++ b/Sources/BladeTCA/Classes/Models/State/PaginatorState.swift @@ -9,7 +9,7 @@ import Foundation // MARK: - OffsetPaginatorState /// Represents the state of a paginator for cursor-based pagination. -public struct PaginatorState: Equatable, IPaginatorState { +public struct PaginatorState: Equatable, IPaginatorState { // MARK: Properties /// The array of identifiable items managed by the paginator. @@ -37,3 +37,7 @@ public struct PaginatorState: Equ self.position = position } } + +// MARK: - Sendable + +extension PaginatorState: Sendable where State.ID: Sendable {} diff --git a/Sources/BladeTCA/Classes/Presentation/Views/PaginatorForEachView.swift b/Sources/BladeTCA/Classes/Presentation/Views/PaginatorForEachView.swift index 865ed87..e9444da 100644 --- a/Sources/BladeTCA/Classes/Presentation/Views/PaginatorForEachView.swift +++ b/Sources/BladeTCA/Classes/Presentation/Views/PaginatorForEachView.swift @@ -7,11 +7,11 @@ import ComposableArchitecture import SwiftUI public struct PaginatorForEachView< - State: Equatable & Identifiable, - Action: Equatable, + State: Equatable & Identifiable & Sendable, + Action: Equatable & Sendable, Body: View, - PositionType: Equatable, - Request: Equatable + PositionType: Equatable & Sendable, + Request: Equatable & Sendable >: View { // MARK: Types diff --git a/Sources/BladeTCA/Classes/Presentation/Views/PaginatorListView.swift b/Sources/BladeTCA/Classes/Presentation/Views/PaginatorListView.swift index 63ac507..39aea32 100644 --- a/Sources/BladeTCA/Classes/Presentation/Views/PaginatorListView.swift +++ b/Sources/BladeTCA/Classes/Presentation/Views/PaginatorListView.swift @@ -3,20 +3,19 @@ // Copyright © 2024 Space Code. All rights reserved. // -import BladeTCA import ComposableArchitecture import SwiftUI // MARK: - PaginatorListView public struct PaginatorListView< - State: Equatable & Identifiable, - Action: Equatable, + State: Equatable & Identifiable & Sendable, + Action: Equatable & Sendable, Header: View, Body: View, Footer: View, - PositionType: Equatable, - Request: Equatable + PositionType: Equatable & Sendable, + Request: Equatable & Sendable >: View { // MARK: Types diff --git a/Sources/BladeTCA/Classes/Presentation/Views/PaginatorView.swift b/Sources/BladeTCA/Classes/Presentation/Views/PaginatorView.swift index 904f12d..827e336 100644 --- a/Sources/BladeTCA/Classes/Presentation/Views/PaginatorView.swift +++ b/Sources/BladeTCA/Classes/Presentation/Views/PaginatorView.swift @@ -3,17 +3,16 @@ // Copyright © 2024 Space Code. All rights reserved. // -import BladeTCA import ComposableArchitecture import SwiftUI // MARK: - PaginatorView public struct PaginatorView< - State: Equatable & Identifiable, - Action: Equatable, - PositionType: Equatable, - Request: Equatable, + State: Equatable & Identifiable & Sendable, + Action: Equatable & Sendable, + PositionType: Equatable & Sendable, + Request: Equatable & Sendable, Header: View, Body: View, Footer: View, diff --git a/Tests/BladeTCATests/Models/TestItem.swift b/Tests/BladeTCATests/Models/TestItem.swift index 35dbef3..3e2f509 100644 --- a/Tests/BladeTCATests/Models/TestItem.swift +++ b/Tests/BladeTCATests/Models/TestItem.swift @@ -21,7 +21,7 @@ struct TestItem: Equatable, Identifiable { // MARK: - UUID + Identifiable -extension UUID: Identifiable { +extension UUID: @retroactive Identifiable { public var id: Self { self } diff --git a/Tests/BladeTests/Mocks/CursorPageLoaderMock.swift b/Tests/BladeTests/Mocks/CursorPageLoaderMock.swift index a345a9e..b499b63 100644 --- a/Tests/BladeTests/Mocks/CursorPageLoaderMock.swift +++ b/Tests/BladeTests/Mocks/CursorPageLoaderMock.swift @@ -5,7 +5,8 @@ import Blade -final class CursorPageLoaderMock: ICursorPageLoader { +// `@unchecked Sendable`: the mock is only mutated sequentially from tests. +final class CursorPageLoaderMock: ICursorPageLoader, @unchecked Sendable where T.ID: Sendable { var invokedLoadPage = false var invokedLoadPageCount = 0 var invokedLoadPageParameters: (request: CursorPaginationRequest, Void)? diff --git a/Tests/BladeTests/Mocks/OffsetPageLoaderMock.swift b/Tests/BladeTests/Mocks/OffsetPageLoaderMock.swift index 1d80496..270613b 100644 --- a/Tests/BladeTests/Mocks/OffsetPageLoaderMock.swift +++ b/Tests/BladeTests/Mocks/OffsetPageLoaderMock.swift @@ -5,7 +5,8 @@ import Blade -final class OffsetPageLoaderMock: IOffsetPageLoader { +// `@unchecked Sendable`: the mock is only mutated sequentially from tests. +final class OffsetPageLoaderMock: IOffsetPageLoader, @unchecked Sendable { var invokedLoadPage = false var invokedLoadPageCount = 0 var invokedLoadPageParameters: (request: OffsetPaginationRequest, Void)? From ac8bf6e5aacf1037101eb4da8e31482d2bf75253 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 5 Oct 2026 21:15:33 +0400 Subject: [PATCH 2/6] ci: add Xcode 26 jobs --- .github/workflows/ci.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48445fc..73bcc96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,10 @@ jobs: runsOn: macos-15 name: "macOS 15, Xcode 16.4, Swift 6.1" params: "-skipMacroValidation" + - xcode: "Xcode_26.0" + runsOn: macos-26 + name: "macOS 26, Xcode 26.0, Swift 6.2" + params: "-skipMacroValidation" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -67,6 +71,11 @@ jobs: xcode: "Xcode_16.4" runsOn: macos-15 params: "-skipMacroValidation" + - destination: "platform=iOS Simulator,name=iPhone 17 Pro" + name: "iOS 26" + xcode: "Xcode_26.0" + runsOn: macos-26 + params: "-skipMacroValidation" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -90,6 +99,11 @@ jobs: xcode: "Xcode_16.4" runsOn: macos-15 params: "-skipMacroValidation" + - destination: "platform=tvOS Simulator,name=Apple TV" + name: "tvOS 26" + xcode: "Xcode_26.0" + runsOn: macos-26 + params: "-skipMacroValidation" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -119,6 +133,11 @@ jobs: xcode: "Xcode_16.4" runsOn: macos-15 params: "-skipMacroValidation" + - destination: "platform=watchOS Simulator,name=Apple Watch Series 11 (46mm)" + name: "watchOS 26" + xcode: "Xcode_26.0" + runsOn: macos-26 + params: "-skipMacroValidation" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -146,6 +165,9 @@ jobs: - name: "Xcode 16" xcode: "Xcode_16.4" runsOn: macos-15 + - name: "Xcode 26" + xcode: "Xcode_26.0" + runsOn: macos-26 steps: - uses: actions/checkout@v4 - name: Toolchain versions From 3ef5fd6ddb47c4386c9e2956d87e3c0afe9fb6db Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 5 Oct 2026 21:21:50 +0400 Subject: [PATCH 3/6] ci: run Xcode 26 only for macOS and spm jobs Simulator runtimes for iOS/tvOS/watchOS 26 are not installed on the runner image for Xcode 26.0. --- .github/workflows/ci.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73bcc96..27bcb0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,11 +71,6 @@ jobs: xcode: "Xcode_16.4" runsOn: macos-15 params: "-skipMacroValidation" - - destination: "platform=iOS Simulator,name=iPhone 17 Pro" - name: "iOS 26" - xcode: "Xcode_26.0" - runsOn: macos-26 - params: "-skipMacroValidation" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -99,11 +94,6 @@ jobs: xcode: "Xcode_16.4" runsOn: macos-15 params: "-skipMacroValidation" - - destination: "platform=tvOS Simulator,name=Apple TV" - name: "tvOS 26" - xcode: "Xcode_26.0" - runsOn: macos-26 - params: "-skipMacroValidation" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -133,11 +123,6 @@ jobs: xcode: "Xcode_16.4" runsOn: macos-15 params: "-skipMacroValidation" - - destination: "platform=watchOS Simulator,name=Apple Watch Series 11 (46mm)" - name: "watchOS 26" - xcode: "Xcode_26.0" - runsOn: macos-26 - params: "-skipMacroValidation" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} From 28bf5623beca1a7c339e80aae6fef34140708c13 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 5 Oct 2026 21:23:09 +0400 Subject: [PATCH 4/6] ci: use Xcode 26.5 on macos-26 with installed simulator runtimes --- .github/workflows/ci.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27bcb0d..8603139 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,9 +38,9 @@ jobs: runsOn: macos-15 name: "macOS 15, Xcode 16.4, Swift 6.1" params: "-skipMacroValidation" - - xcode: "Xcode_26.0" + - xcode: "Xcode_26.5" runsOn: macos-26 - name: "macOS 26, Xcode 26.0, Swift 6.2" + name: "macOS 26, Xcode 26.5" params: "-skipMacroValidation" steps: - uses: actions/checkout@v4 @@ -71,6 +71,11 @@ jobs: xcode: "Xcode_16.4" runsOn: macos-15 params: "-skipMacroValidation" + - destination: "platform=iOS Simulator,name=iPhone 17 Pro" + name: "iOS 26" + xcode: "Xcode_26.5" + runsOn: macos-26 + params: "-skipMacroValidation" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -94,6 +99,11 @@ jobs: xcode: "Xcode_16.4" runsOn: macos-15 params: "-skipMacroValidation" + - destination: "platform=tvOS Simulator,name=Apple TV" + name: "tvOS 26" + xcode: "Xcode_26.5" + runsOn: macos-26 + params: "-skipMacroValidation" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -123,6 +133,11 @@ jobs: xcode: "Xcode_16.4" runsOn: macos-15 params: "-skipMacroValidation" + - destination: "platform=watchOS Simulator,name=Apple Watch Series 11 (46mm)" + name: "watchOS 26" + xcode: "Xcode_26.5" + runsOn: macos-26 + params: "-skipMacroValidation" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -151,7 +166,7 @@ jobs: xcode: "Xcode_16.4" runsOn: macos-15 - name: "Xcode 26" - xcode: "Xcode_26.0" + xcode: "Xcode_26.5" runsOn: macos-26 steps: - uses: actions/checkout@v4 From 3e1de1240b205529f0147194005485abceca6a6c Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 5 Oct 2026 21:26:13 +0400 Subject: [PATCH 5/6] ci: restructure workflow into a platform matrix --- .github/workflows/ci.yml | 215 +++++++++++++++------------------------ 1 file changed, 81 insertions(+), 134 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8603139..f314d74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,12 @@ on: branches: - main - dev + paths: + - '.swiftlint.yml' + - ".github/workflows/**" + - "Package*.swift" + - "Sources/**" + - "Tests/**" pull_request: paths: - '.swiftlint.yml' @@ -13,112 +19,31 @@ on: - "Sources/**" - "Tests/**" +permissions: + contents: read + +concurrency: + group: blade-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +env: + SCHEME_NAME: "Blade-Package" + jobs: - SwiftLint: + swiftlint: + name: SwiftLint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v7 - name: GitHub Action for SwiftLint uses: norio-nomura/action-swiftlint@3.2.1 with: args: --strict env: DIFF_BASE: ${{ github.base_ref }} - macOS: - name: ${{ matrix.name }} - runs-on: ${{ matrix.runsOn }} - env: - DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer" - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - include: - - xcode: "Xcode_16.4" - runsOn: macos-15 - name: "macOS 15, Xcode 16.4, Swift 6.1" - params: "-skipMacroValidation" - - xcode: "Xcode_26.5" - runsOn: macos-26 - name: "macOS 26, Xcode 26.5" - params: "-skipMacroValidation" - steps: - - uses: actions/checkout@v4 - - name: ${{ matrix.name }} - run: xcodebuild test -scheme "Blade-Package" -destination "platform=macOS" clean -enableCodeCoverage YES ${{ matrix.params }} -resultBundlePath "test_output/${{ matrix.name }}.xcresult" || exit 1 - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4.5.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - xcode: true - xcode_archive_path: test_output/${{ matrix.name }}.xcresult - - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.name }} - path: test_output - iOS: - name: ${{ matrix.name }} - runs-on: ${{ matrix.runsOn }} - env: - DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer" - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - include: - - destination: "platform=iOS Simulator,name=iPhone 16 Pro" - name: "iOS 18" - xcode: "Xcode_16.4" - runsOn: macos-15 - params: "-skipMacroValidation" - - destination: "platform=iOS Simulator,name=iPhone 17 Pro" - name: "iOS 26" - xcode: "Xcode_26.5" - runsOn: macos-26 - params: "-skipMacroValidation" - steps: - - uses: actions/checkout@v4 - - name: ${{ matrix.name }} - run: xcodebuild test -scheme "Blade-Package" -destination "${{ matrix.destination }}" clean -enableCodeCoverage YES ${{ matrix.params }} -resultBundlePath "test_output/${{ matrix.name }}.xcresult" || exit 1 - - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.name }} - path: test_output - tvOS: - name: ${{ matrix.name }} - runs-on: ${{ matrix.runsOn }} - env: - DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer" - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - include: - - destination: "platform=tvOS Simulator,name=Apple TV" - name: "tvOS 18" - xcode: "Xcode_16.4" - runsOn: macos-15 - params: "-skipMacroValidation" - - destination: "platform=tvOS Simulator,name=Apple TV" - name: "tvOS 26" - xcode: "Xcode_26.5" - runsOn: macos-26 - params: "-skipMacroValidation" - steps: - - uses: actions/checkout@v4 - - name: ${{ matrix.name }} - run: xcodebuild test -scheme "Blade-Package" -destination "${{ matrix.destination }}" clean -enableCodeCoverage YES ${{ matrix.params }} -resultBundlePath "test_output/${{ matrix.name }}.xcresult" || exit 1 - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4.5.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - xcode: true - xcode_archive_path: test_output/${{ matrix.name }}.xcresult - - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.name }} - path: test_output - watchOS: + + test-apple-platforms: name: ${{ matrix.name }} runs-on: ${{ matrix.runsOn }} env: @@ -128,31 +53,49 @@ jobs: fail-fast: false matrix: include: - - destination: "platform=watchOS Simulator,name=Apple Watch Series 10 (46mm)" - name: "watchOS 11" - xcode: "Xcode_16.4" - runsOn: macos-15 - params: "-skipMacroValidation" - - destination: "platform=watchOS Simulator,name=Apple Watch Series 11 (46mm)" - name: "watchOS 26" - xcode: "Xcode_26.5" - runsOn: macos-26 - params: "-skipMacroValidation" + # macOS + - { platform: macOS, name: "macOS 26, Xcode 26.5, Swift 6.2", xcode: "Xcode_26.5", runsOn: macOS-26, destination: "platform=macOS" } + - { platform: macOS, name: "macOS 14, Xcode 16.1, Swift 6.0.2", xcode: "Xcode_16.1", runsOn: macOS-14, destination: "platform=macOS" } + - { platform: macOS, name: "macOS 14, Xcode 15.4, Swift 5.10", xcode: "Xcode_15.4", runsOn: macOS-14, destination: "platform=macOS" } + + # iOS + - { platform: iOS, name: "iOS 26.5", xcode: "Xcode_26.5", runsOn: macOS-26, destination: "OS=26.5,name=iPhone 17 Pro" } + - { platform: iOS, name: "iOS 18.1", xcode: "Xcode_16.1", runsOn: macOS-14, destination: "OS=18.1,name=iPhone 16 Pro" } + - { platform: iOS, name: "iOS 17.4", xcode: "Xcode_15.3", runsOn: macOS-14, destination: "OS=17.4,name=iPhone 15 Pro" } + + # tvOS + - { platform: tvOS, name: "tvOS 26.5", xcode: "Xcode_26.5", runsOn: macOS-26, destination: "OS=26.5,name=Apple TV" } + - { platform: tvOS, name: "tvOS 18.1", xcode: "Xcode_16.1", runsOn: macOS-14, destination: "OS=18.1,name=Apple TV" } + - { platform: tvOS, name: "tvOS 17.4", xcode: "Xcode_15.3", runsOn: macOS-14, destination: "OS=17.4,name=Apple TV" } + + # watchOS + - { platform: watchOS, name: "watchOS 26.5", xcode: "Xcode_26.5", runsOn: macOS-26, destination: "OS=26.5,name=Apple Watch Ultra 3 (49mm)" } + - { platform: watchOS, name: "watchOS 11.1", xcode: "Xcode_16.1", runsOn: macOS-14, destination: "OS=11.1,name=Apple Watch Series 10 (46mm)" } + - { platform: watchOS, name: "watchOS 10.5", xcode: "Xcode_15.3", runsOn: macOS-14, destination: "OS=10.5,name=Apple Watch Series 9 (45mm)" } + - { platform: watchOS, name: "watchOS 10.4", xcode: "Xcode_15.3", runsOn: macOS-14, destination: "OS=10.4,name=Apple Watch Series 9 (45mm)" } steps: - - uses: actions/checkout@v4 - - name: ${{ matrix.name }} - run: xcodebuild test -scheme "Blade-Package" -destination "${{ matrix.destination }}" clean -enableCodeCoverage YES ${{ matrix.params }} -resultBundlePath "test_output/${{ matrix.name }}.xcresult" || exit 1 - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4.5.0 + - name: Checkout code + uses: actions/checkout@v7 + # The committed pins (TCA 1.26.2) need Swift 6.1+; older toolchains must resolve a compatible TCA. + - name: Resolve dependencies for the toolchain + run: rm -f Package.resolved + - name: Run tests - ${{ matrix.name }} + run: | + xcodebuild test \ + -scheme "${{ env.SCHEME_NAME }}" \ + -destination "${{ matrix.destination }}" \ + -enableCodeCoverage YES \ + -skipMacroValidation \ + -resultBundlePath "test_output/${{ matrix.name }}.xcresult" \ + clean || exit 1 + - name: Upload test coverage to Codecov + uses: space-code/oss-common-actions/.github/actions/upload_test_coverage_report@main with: + scheme_name: ${{ env.SCHEME_NAME }} + filename: ${{ matrix.name }} token: ${{ secrets.CODECOV_TOKEN }} - xcode: true - xcode_archive_path: test_output/${{ matrix.name }}.xcresult - - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.name }} - path: test_output - spm: + + spm-build: name: ${{ matrix.name }} runs-on: ${{ matrix.runsOn }} env: @@ -162,43 +105,47 @@ jobs: fail-fast: false matrix: include: - - name: "Xcode 16" - xcode: "Xcode_16.4" - runsOn: macos-15 - - name: "Xcode 26" - xcode: "Xcode_26.5" - runsOn: macos-26 + - { name: "macOS 26, SPM 6.2", xcode: "Xcode_26.5", runsOn: macOS-26 } + - { name: "macOS 15, SPM 6.0", xcode: "Xcode_16.0", runsOn: macOS-15 } + - { name: "macOS 14, SPM 6.0.2", xcode: "Xcode_16.1", runsOn: macOS-14 } + - { name: "macOS 14, SPM 5.10", xcode: "Xcode_15.3", runsOn: macOS-14 } steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v7 + - name: Resolve dependencies for the toolchain + run: rm -f Package.resolved - name: Toolchain versions run: | swift --version xcodebuild -version - - name: ${{ matrix.name }} + # Debug build: release builds crash the Swift 6.x optimizer on some dependency versions. + - name: Build with Swift Package Manager - ${{ matrix.name }} run: | swift build --target "Blade" swift build --target "BladeTCA" merge-test-reports: - needs: [iOS, macOS, watchOS, tvOS] - runs-on: macos-15 + needs: test-apple-platforms + runs-on: macos-26 steps: - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: test_output - run: xcrun xcresulttool merge test_output/**/*.xcresult --output-path test_output/final/final.xcresult - - name: Upload Merged Artifact - uses: actions/upload-artifact@v4 + - name: Upload merged test results + uses: actions/upload-artifact@v7 with: - name: MergedResult + name: MergedTestResults path: test_output/final + retention-days: 30 discover-typos: name: Discover Typos runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v7 - name: Discover typos run: | python3 -m pip install --upgrade pip From 11d54f4151c6de89f8ce940a260a36ed1a2064bf Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 5 Oct 2026 21:38:42 +0400 Subject: [PATCH 6/6] fix: address review findings and require TCA 1.26.2 / Swift 6.1+ - Raise swift-composable-architecture lower bound to 1.26.2 - Replace Package@swift-6.0/5.10 with Package@swift-6.1 - Make CursorPaginatorState Sendable, fix duplicated Sendable constraint - Update SwiftLint exclusions, CI matrix, README and CHANGELOG --- .github/workflows/ci.yml | 22 +++------ .swiftlint.yml | 3 +- CHANGELOG.md | 8 ++-- Package.swift | 2 +- Package@swift-5.10.swift | 45 ------------------- ...swift-6.0.swift => Package@swift-6.1.swift | 4 +- README.md | 4 +- .../Protocols/IPositionBuilderStrategy.swift | 2 +- .../Models/State/CursorPaginatorState.swift | 4 ++ 9 files changed, 21 insertions(+), 73 deletions(-) delete mode 100644 Package@swift-5.10.swift rename Package@swift-6.0.swift => Package@swift-6.1.swift (92%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f314d74..dfc114e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,30 +55,22 @@ jobs: include: # macOS - { platform: macOS, name: "macOS 26, Xcode 26.5, Swift 6.2", xcode: "Xcode_26.5", runsOn: macOS-26, destination: "platform=macOS" } - - { platform: macOS, name: "macOS 14, Xcode 16.1, Swift 6.0.2", xcode: "Xcode_16.1", runsOn: macOS-14, destination: "platform=macOS" } - - { platform: macOS, name: "macOS 14, Xcode 15.4, Swift 5.10", xcode: "Xcode_15.4", runsOn: macOS-14, destination: "platform=macOS" } + - { platform: macOS, name: "macOS 15, Xcode 16.4, Swift 6.1", xcode: "Xcode_16.4", runsOn: macOS-15, destination: "platform=macOS" } # iOS - { platform: iOS, name: "iOS 26.5", xcode: "Xcode_26.5", runsOn: macOS-26, destination: "OS=26.5,name=iPhone 17 Pro" } - - { platform: iOS, name: "iOS 18.1", xcode: "Xcode_16.1", runsOn: macOS-14, destination: "OS=18.1,name=iPhone 16 Pro" } - - { platform: iOS, name: "iOS 17.4", xcode: "Xcode_15.3", runsOn: macOS-14, destination: "OS=17.4,name=iPhone 15 Pro" } + - { platform: iOS, name: "iOS 18.5", xcode: "Xcode_16.4", runsOn: macOS-15, destination: "OS=18.5,name=iPhone 16 Pro" } # tvOS - { platform: tvOS, name: "tvOS 26.5", xcode: "Xcode_26.5", runsOn: macOS-26, destination: "OS=26.5,name=Apple TV" } - - { platform: tvOS, name: "tvOS 18.1", xcode: "Xcode_16.1", runsOn: macOS-14, destination: "OS=18.1,name=Apple TV" } - - { platform: tvOS, name: "tvOS 17.4", xcode: "Xcode_15.3", runsOn: macOS-14, destination: "OS=17.4,name=Apple TV" } + - { platform: tvOS, name: "tvOS 18.5", xcode: "Xcode_16.4", runsOn: macOS-15, destination: "OS=18.5,name=Apple TV" } # watchOS - { platform: watchOS, name: "watchOS 26.5", xcode: "Xcode_26.5", runsOn: macOS-26, destination: "OS=26.5,name=Apple Watch Ultra 3 (49mm)" } - - { platform: watchOS, name: "watchOS 11.1", xcode: "Xcode_16.1", runsOn: macOS-14, destination: "OS=11.1,name=Apple Watch Series 10 (46mm)" } - - { platform: watchOS, name: "watchOS 10.5", xcode: "Xcode_15.3", runsOn: macOS-14, destination: "OS=10.5,name=Apple Watch Series 9 (45mm)" } - - { platform: watchOS, name: "watchOS 10.4", xcode: "Xcode_15.3", runsOn: macOS-14, destination: "OS=10.4,name=Apple Watch Series 9 (45mm)" } + - { platform: watchOS, name: "watchOS 11.5", xcode: "Xcode_16.4", runsOn: macOS-15, destination: "OS=11.5,name=Apple Watch Series 10 (46mm)" } steps: - name: Checkout code uses: actions/checkout@v7 - # The committed pins (TCA 1.26.2) need Swift 6.1+; older toolchains must resolve a compatible TCA. - - name: Resolve dependencies for the toolchain - run: rm -f Package.resolved - name: Run tests - ${{ matrix.name }} run: | xcodebuild test \ @@ -106,14 +98,10 @@ jobs: matrix: include: - { name: "macOS 26, SPM 6.2", xcode: "Xcode_26.5", runsOn: macOS-26 } - - { name: "macOS 15, SPM 6.0", xcode: "Xcode_16.0", runsOn: macOS-15 } - - { name: "macOS 14, SPM 6.0.2", xcode: "Xcode_16.1", runsOn: macOS-14 } - - { name: "macOS 14, SPM 5.10", xcode: "Xcode_15.3", runsOn: macOS-14 } + - { name: "macOS 15, SPM 6.1", xcode: "Xcode_16.4", runsOn: macOS-15 } steps: - name: Checkout code uses: actions/checkout@v7 - - name: Resolve dependencies for the toolchain - run: rm -f Package.resolved - name: Toolchain versions run: | swift --version diff --git a/.swiftlint.yml b/.swiftlint.yml index 44740c2..38873d3 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,8 +1,7 @@ excluded: - Tests - Package.swift - - Package@swift-5.7.swift - - Package@swift-5.8.swift + - Package@swift-6.1.swift - .build # Rules diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a80ca9..6ca9c0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,12 @@ All notable changes to this project will be documented in this file. ## Unreleased #### Changed -- Adopt Swift 6 language mode with strict concurrency (Swift 6.2 toolchain). Swift 6.0 uses Swift 6 mode, Swift 5.10 uses Swift 5 mode with `StrictConcurrency`. +- Adopt Swift 6 language mode with strict concurrency (Swift 6.2 toolchain; Swift 6.1 uses `Package@swift-6.1.swift`). +- **Breaking:** minimum Swift toolchain is now 6.1 (Xcode 16.3+). `Package@swift-5.7.swift` and `Package@swift-5.8.swift` removed. +- **Breaking:** update `swift-composable-architecture` to 1.26.2 (minimum version is now 1.26.2). - **Breaking:** public generics and loader/strategy protocols now require `Sendable` (`Element`, `State`, `Action`, `Request`, `ICursorPageLoader`, `IOffsetPageLoader`, `IPaginator`). -- Update `swift-composable-architecture` to 1.26.2 (Swift 6.1+). Older toolchains resolve an older compatible TCA. -- Minimum Swift toolchain is now 5.10 (`Package@swift-5.7.swift` and `Package@swift-5.8.swift` removed). +- **Breaking:** `Reducer.paginator(...)` is only available when the parent reducer's `State` is `Sendable`. +- `CursorPaginatorState`, `PaginatorState` and `PaginatorAction` are `Sendable` when their identifiers are `Sendable`. ## [1.1.0](https://github.com/space-code/blade/releases/tag/1.1.0) Released on 2024-08-26. diff --git a/Package.swift b/Package.swift index 923de30..ecee3f3 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( .library(name: "BladeTCA", targets: ["BladeTCA"]), ], dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", .upToNextMajor(from: "1.5.5")), + .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", .upToNextMajor(from: "1.26.2")), ], targets: [ .target(name: "Blade"), diff --git a/Package@swift-5.10.swift b/Package@swift-5.10.swift deleted file mode 100644 index ffd1937..0000000 --- a/Package@swift-5.10.swift +++ /dev/null @@ -1,45 +0,0 @@ -// swift-tools-version: 5.10 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let strictConcurrency: [SwiftSetting] = [.enableExperimentalFeature("StrictConcurrency")] - -let package = Package( - name: "Blade", - platforms: [ - .iOS(.v16), - .macOS(.v13), - .tvOS(.v16), - .watchOS(.v9), - .visionOS(.v1), - ], - products: [ - .library(name: "Blade", targets: ["Blade"]), - .library(name: "BladeTCA", targets: ["BladeTCA"]), - ], - dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", .upToNextMajor(from: "1.5.5")), - ], - targets: [ - .target(name: "Blade", swiftSettings: strictConcurrency), - .target( - name: "BladeTCA", - dependencies: [ - "Blade", - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - ], - swiftSettings: strictConcurrency - ), - .testTarget(name: "BladeTests", dependencies: ["Blade"], swiftSettings: strictConcurrency), - .testTarget( - name: "BladeTCATests", - dependencies: [ - "BladeTCA", - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - ], - swiftSettings: strictConcurrency - ), - ], - swiftLanguageVersions: [.v5] -) diff --git a/Package@swift-6.0.swift b/Package@swift-6.1.swift similarity index 92% rename from Package@swift-6.0.swift rename to Package@swift-6.1.swift index 519cd47..a489f26 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.1.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.0 +// swift-tools-version: 6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -17,7 +17,7 @@ let package = Package( .library(name: "BladeTCA", targets: ["BladeTCA"]), ], dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", .upToNextMajor(from: "1.5.5")), + .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", .upToNextMajor(from: "1.26.2")), ], targets: [ .target(name: "Blade"), diff --git a/README.md b/README.md index d39e799..240cb15 100644 --- a/README.md +++ b/README.md @@ -157,8 +157,8 @@ struct SomeView: View { ## Requirements - iOS 16.0+ / macOS 13.0+ / tvOS 16.0+ / watchOS 9.0+ / visionOS 1.0+ -- Xcode 15.3+ (Swift 5.10+); Swift 6 language mode on Swift 6.0+ -- Swift 5.10+ (TCA 1.26.2 is used on Swift 6.1+, older TCA is resolved automatically on 5.10 and 6.0) +- Xcode 16.3+ +- Swift 6.1+ (Swift 6 language mode, `swift-composable-architecture` 1.26.2+) ## Installation ### Swift Package Manager diff --git a/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/PositionBuilders/Protocols/IPositionBuilderStrategy.swift b/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/PositionBuilders/Protocols/IPositionBuilderStrategy.swift index a0dbac6..f62dfd8 100644 --- a/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/PositionBuilders/Protocols/IPositionBuilderStrategy.swift +++ b/Sources/BladeTCA/Classes/COre/Reducers/Internal/Builders/PositionBuilders/Protocols/IPositionBuilderStrategy.swift @@ -8,7 +8,7 @@ import Foundation /// This protocol defines the interface for a strategy used in building positions based on a given state. protocol IPositionBuilderStrategy: Sendable { associatedtype State: Equatable - associatedtype PositionType: Equatable & Sendable & Sendable + associatedtype PositionType: Equatable & Sendable /// Takes a state as input and returns the corresponding position. /// diff --git a/Sources/BladeTCA/Classes/Models/State/CursorPaginatorState.swift b/Sources/BladeTCA/Classes/Models/State/CursorPaginatorState.swift index e018ab4..01d7c40 100644 --- a/Sources/BladeTCA/Classes/Models/State/CursorPaginatorState.swift +++ b/Sources/BladeTCA/Classes/Models/State/CursorPaginatorState.swift @@ -34,3 +34,7 @@ public struct CursorPaginatorState: self.id = id } } + +// MARK: - Sendable + +extension CursorPaginatorState: Sendable where State.ID: Sendable {}