From 336596dadbfb00f1435f136929669b685168c228 Mon Sep 17 00:00:00 2001 From: ra1028 Date: Thu, 3 Oct 2024 15:36:44 +0900 Subject: [PATCH 01/10] Setup swift6 --- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 7 +++- Benchmarks/project.yml | 2 +- Examples/Packages/CrossPlatform/Package.swift | 33 ++++----------- Examples/Packages/iOS/Package.swift | 41 ++++++------------- Examples/project.yml | 2 +- Package.swift | 17 ++------ Package@swift-5.swift | 32 +++++++++++++++ README.md | 4 +- Tools/Package.swift | 4 +- 10 files changed, 69 insertions(+), 75 deletions(-) create mode 100644 Package@swift-5.swift diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f06cf49b..12e74572 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: env: - DEVELOPER_DIR: /Applications/Xcode_15.4.app + DEVELOPER_DIR: /Applications/Xcode_16.0.app jobs: publish-docs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b8a4d8c4..33002b00 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ on: workflow_dispatch: env: - DEVELOPER_DIR: /Applications/Xcode_15.4.app + DEVELOPER_DIR: /Applications/Xcode_16.app jobs: test: @@ -18,11 +18,16 @@ jobs: runs-on: macos-14 strategy: matrix: + xcode_version: + - 15.4 + - 16 platform: - ios - macos - tvos - watchos + env: + DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode_version }}.app steps: - uses: actions/checkout@v4 - name: Test library diff --git a/Benchmarks/project.yml b/Benchmarks/project.yml index e3b94d05..a1195b4b 100644 --- a/Benchmarks/project.yml +++ b/Benchmarks/project.yml @@ -8,7 +8,7 @@ settings: CODE_SIGNING_REQUIRED: NO CODE_SIGN_IDENTITY: "-" CODE_SIGN_STYLE: Manual - SWIFT_STRICT_CONCURRENCY: complete + SWIFT_VERSION: 6 packages: swiftui-atom-properties: diff --git a/Examples/Packages/CrossPlatform/Package.swift b/Examples/Packages/CrossPlatform/Package.swift index ea892f63..f7acdf17 100644 --- a/Examples/Packages/CrossPlatform/Package.swift +++ b/Examples/Packages/CrossPlatform/Package.swift @@ -1,26 +1,8 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.0 import PackageDescription -let swiftSettings: [SwiftSetting] = [ - .enableExperimentalFeature("StrictConcurrency") -] - -func target(name: String, dependencies: [Target.Dependency] = []) -> Target { - .target( - name: name, - dependencies: [.product(name: "Atoms", package: "swiftui-atom-properties")] + dependencies, - swiftSettings: swiftSettings - ) -} - -func testTarget(name: String, dependencies: [Target.Dependency]) -> Target { - .testTarget( - name: name, - dependencies: dependencies, - swiftSettings: swiftSettings - ) -} +let atoms = Target.Dependency.product(name: "Atoms", package: "swiftui-atom-properties") let package = Package( name: "CrossPlatformExamples", @@ -37,16 +19,17 @@ let package = Package( .package(path: "../../..") ], targets: [ - target( + .target( name: "CrossPlatformApp", dependencies: [ + atoms, "ExampleCounter", "ExampleTodo", ] ), - target(name: "ExampleCounter"), - testTarget(name: "ExampleCounterTests", dependencies: ["ExampleCounter"]), - target(name: "ExampleTodo"), - testTarget(name: "ExampleTodoTests", dependencies: ["ExampleTodo"]), + .target(name: "ExampleCounter", dependencies: [atoms]), + .testTarget(name: "ExampleCounterTests", dependencies: ["ExampleCounter"]), + .target(name: "ExampleTodo", dependencies: [atoms]), + .testTarget(name: "ExampleTodoTests", dependencies: ["ExampleTodo"]), ] ) diff --git a/Examples/Packages/iOS/Package.swift b/Examples/Packages/iOS/Package.swift index 4248a840..653f36c0 100644 --- a/Examples/Packages/iOS/Package.swift +++ b/Examples/Packages/iOS/Package.swift @@ -1,26 +1,8 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.0 import PackageDescription -let swiftSettings: [SwiftSetting] = [ - .enableExperimentalFeature("StrictConcurrency") -] - -func target(name: String, dependencies: [Target.Dependency] = []) -> Target { - .target( - name: name, - dependencies: [.product(name: "Atoms", package: "swiftui-atom-properties")] + dependencies, - swiftSettings: swiftSettings - ) -} - -func testTarget(name: String, dependencies: [Target.Dependency]) -> Target { - .testTarget( - name: name, - dependencies: dependencies, - swiftSettings: swiftSettings - ) -} +let atoms = Target.Dependency.product(name: "Atoms", package: "swiftui-atom-properties") let package = Package( name: "iOSExamples", @@ -35,9 +17,10 @@ let package = Package( .package(path: "../CrossPlatform"), ], targets: [ - target( + .target( name: "iOSApp", dependencies: [ + atoms, .product(name: "CrossPlatformApp", package: "CrossPlatform"), "ExampleMovieDB", "ExampleMap", @@ -45,13 +28,13 @@ let package = Package( "ExampleTimeTravel", ] ), - target(name: "ExampleMovieDB"), - testTarget(name: "ExampleMovieDBTests", dependencies: ["ExampleMovieDB"]), - target(name: "ExampleMap"), - testTarget(name: "ExampleMapTests", dependencies: ["ExampleMap"]), - target(name: "ExampleVoiceMemo"), - testTarget(name: "ExampleVoiceMemoTests", dependencies: ["ExampleVoiceMemo"]), - target(name: "ExampleTimeTravel"), - testTarget(name: "ExampleTimeTravelTests", dependencies: ["ExampleTimeTravel"]), + .target(name: "ExampleMovieDB", dependencies: [atoms]), + .testTarget(name: "ExampleMovieDBTests", dependencies: ["ExampleMovieDB"]), + .target(name: "ExampleMap", dependencies: [atoms]), + .testTarget(name: "ExampleMapTests", dependencies: ["ExampleMap"]), + .target(name: "ExampleVoiceMemo", dependencies: [atoms]), + .testTarget(name: "ExampleVoiceMemoTests", dependencies: ["ExampleVoiceMemo"]), + .target(name: "ExampleTimeTravel", dependencies: [atoms]), + .testTarget(name: "ExampleTimeTravelTests", dependencies: ["ExampleTimeTravel"]), ] ) diff --git a/Examples/project.yml b/Examples/project.yml index f3e297e6..32ac882e 100644 --- a/Examples/project.yml +++ b/Examples/project.yml @@ -12,7 +12,7 @@ settings: CODE_SIGNING_REQUIRED: NO CODE_SIGN_IDENTITY: "-" CODE_SIGN_STYLE: Manual - SWIFT_STRICT_CONCURRENCY: complete + SWIFT_VERSION: 6 targetTemplates: App: diff --git a/Package.swift b/Package.swift index 3c11985f..8b2f4b62 100644 --- a/Package.swift +++ b/Package.swift @@ -1,12 +1,7 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.0 -import Foundation import PackageDescription -let swiftSettings: [SwiftSetting] = [ - .enableExperimentalFeature("StrictConcurrency") -] - let package = Package( name: "swiftui-atom-properties", platforms: [ @@ -19,15 +14,11 @@ let package = Package( .library(name: "Atoms", targets: ["Atoms"]) ], targets: [ - .target( - name: "Atoms", - swiftSettings: swiftSettings - ), + .target(name: "Atoms"), .testTarget( name: "AtomsTests", - dependencies: ["Atoms"], - swiftSettings: swiftSettings + dependencies: ["Atoms"] ), ], - swiftLanguageVersions: [.v5] + swiftLanguageModes: [.v6] ) diff --git a/Package@swift-5.swift b/Package@swift-5.swift new file mode 100644 index 00000000..d761ac9f --- /dev/null +++ b/Package@swift-5.swift @@ -0,0 +1,32 @@ +// swift-tools-version:5.10 + +import PackageDescription + +let swiftSettings: [SwiftSetting] = [ + .enableExperimentalFeature("StrictConcurrency") +] + +let package = Package( + name: "swiftui-atom-properties", + platforms: [ + .iOS(.v14), + .macOS(.v11), + .tvOS(.v14), + .watchOS(.v7), + ], + products: [ + .library(name: "Atoms", targets: ["Atoms"]) + ], + targets: [ + .target( + name: "Atoms", + swiftSettings: swiftSettings + ), + .testTarget( + name: "AtomsTests", + dependencies: ["Atoms"], + swiftSettings: swiftSettings + ), + ], + swiftLanguageVersions: [.v5] +) diff --git a/README.md b/README.md index 7eb91875..ac673874 100644 --- a/README.md +++ b/README.md @@ -127,8 +127,8 @@ Open `Examples/Project.xcodeproj` and play around with it! | |Minimum Version| |------:|--------------:| -|Swift |5.10 | -|Xcode |15.4 | +|Swift |5.10, 6.0 | +|Xcode |15.4, 16.0 | |iOS |14.0 | |macOS |11.0 | |tvOS |14.0 | diff --git a/Tools/Package.swift b/Tools/Package.swift index 2d377acc..9a4a04b1 100644 --- a/Tools/Package.swift +++ b/Tools/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.0 import PackageDescription @@ -6,8 +6,8 @@ let package = Package( name: "dev-tools", dependencies: [ .package(name: "swiftui-atom-properties", path: ".."), + .package(url: "https://github.com/apple/swift-format.git", exact: "600.0.0"), .package(url: "https://github.com/apple/swift-docc-plugin", exact: "1.4.3"), - .package(url: "https://github.com/apple/swift-format.git", exact: "510.1.0"), .package(url: "https://github.com/yonaskolb/XcodeGen.git", exact: "2.42.0"), ] ) From dc2120df3517ba1c87eac501837f81b7ee6b4c91 Mon Sep 17 00:00:00 2001 From: ra1028 Date: Thu, 3 Oct 2024 16:04:14 +0900 Subject: [PATCH 02/10] Fix for newly appeared compilation errors --- Benchmarks/Project.xcodeproj/project.pbxproj | 6 ++---- Benchmarks/Tests/ViewTest.swift | 2 +- .../Dependency/AudioRecorder.swift | 2 +- Examples/Project.xcodeproj/project.pbxproj | 6 ++---- Sources/Atoms/Atom/ObservableObjectAtom.swift | 4 +++- Sources/Atoms/Atom/PublisherAtom.swift | 2 +- Sources/Atoms/Context/AtomTestContext.swift | 4 ++-- Sources/Atoms/Effect/MergedEffect.swift | 6 +++--- Sources/Atoms/PropertyWrapper/ViewContext.swift | 4 ++++ Sources/Atoms/PropertyWrapper/Watch.swift | 3 +++ Sources/Atoms/PropertyWrapper/WatchState.swift | 6 ++++++ .../Atoms/PropertyWrapper/WatchStateObject.swift | 7 +++++++ Tests/AtomsTests/Core/StoreContextTests.swift | 14 +++++--------- Tools/Package.resolved | 12 ++++++------ 14 files changed, 46 insertions(+), 32 deletions(-) diff --git a/Benchmarks/Project.xcodeproj/project.pbxproj b/Benchmarks/Project.xcodeproj/project.pbxproj index 126be4a8..056baedb 100644 --- a/Benchmarks/Project.xcodeproj/project.pbxproj +++ b/Benchmarks/Project.xcodeproj/project.pbxproj @@ -251,8 +251,7 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6; }; name = Release; }; @@ -383,8 +382,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6; }; name = Debug; }; diff --git a/Benchmarks/Tests/ViewTest.swift b/Benchmarks/Tests/ViewTest.swift index 5b65f9e1..0521e8ba 100644 --- a/Benchmarks/Tests/ViewTest.swift +++ b/Benchmarks/Tests/ViewTest.swift @@ -1,6 +1,6 @@ import SwiftUI -struct ViewTest: _ViewTest { +struct ViewTest: _ViewTest { let rootView: @MainActor () -> Content func initRootView() -> some View { diff --git a/Examples/Packages/iOS/Sources/ExampleVoiceMemo/Dependency/AudioRecorder.swift b/Examples/Packages/iOS/Sources/ExampleVoiceMemo/Dependency/AudioRecorder.swift index 0670d710..5b1a7ab6 100644 --- a/Examples/Packages/iOS/Sources/ExampleVoiceMemo/Dependency/AudioRecorder.swift +++ b/Examples/Packages/iOS/Sources/ExampleVoiceMemo/Dependency/AudioRecorder.swift @@ -7,7 +7,7 @@ protocol AudioRecorderProtocol { func stop() } -final class AudioRecorder: NSObject, AVAudioRecorderDelegate, AudioRecorderProtocol { +final class AudioRecorder: NSObject, AVAudioRecorderDelegate, AudioRecorderProtocol, @unchecked Sendable { private var recorder: AVAudioRecorder? private let onFail: () -> Void diff --git a/Examples/Project.xcodeproj/project.pbxproj b/Examples/Project.xcodeproj/project.pbxproj index 8fab08ce..5535ffb5 100644 --- a/Examples/Project.xcodeproj/project.pbxproj +++ b/Examples/Project.xcodeproj/project.pbxproj @@ -276,8 +276,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6; TVOS_DEPLOYMENT_TARGET = 16.0; }; name = Release; @@ -408,8 +407,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6; TVOS_DEPLOYMENT_TARGET = 16.0; }; name = Debug; diff --git a/Sources/Atoms/Atom/ObservableObjectAtom.swift b/Sources/Atoms/Atom/ObservableObjectAtom.swift index 25ec2889..b5e00d0b 100644 --- a/Sources/Atoms/Atom/ObservableObjectAtom.swift +++ b/Sources/Atoms/Atom/ObservableObjectAtom.swift @@ -82,7 +82,9 @@ public extension ObservableObjectAtom { } } - context.onTermination = cancellable.cancel + context.onTermination = { + cancellable.cancel() + } } } } diff --git a/Sources/Atoms/Atom/PublisherAtom.swift b/Sources/Atoms/Atom/PublisherAtom.swift index 24c3676c..c5d59646 100644 --- a/Sources/Atoms/Atom/PublisherAtom.swift +++ b/Sources/Atoms/Atom/PublisherAtom.swift @@ -98,7 +98,7 @@ public extension PublisherAtom { } } -private extension Publisher { +private extension Publisher where Output: Sendable { var results: AsyncStream> { AsyncStream { continuation in let cancellable = map(Result.success) diff --git a/Sources/Atoms/Context/AtomTestContext.swift b/Sources/Atoms/Context/AtomTestContext.swift index dc97a8c0..82e54b26 100644 --- a/Sources/Atoms/Context/AtomTestContext.swift +++ b/Sources/Atoms/Context/AtomTestContext.swift @@ -52,7 +52,7 @@ public struct AtomTestContext: AtomWatchableContext { await withTaskGroup(of: Bool.self) { group in let updates = _state.makeUpdateStream() - group.addTask { @MainActor in + group.addTask { @MainActor @Sendable in for await _ in updates { return true } @@ -121,7 +121,7 @@ public struct AtomTestContext: AtomWatchableContext { let updates = _state.makeUpdateStream() - group.addTask { @MainActor in + group.addTask { @MainActor @Sendable in guard !check() else { return false } diff --git a/Sources/Atoms/Effect/MergedEffect.swift b/Sources/Atoms/Effect/MergedEffect.swift index 2ef97ce6..6ee397a4 100644 --- a/Sources/Atoms/Effect/MergedEffect.swift +++ b/Sources/Atoms/Effect/MergedEffect.swift @@ -8,13 +8,13 @@ public struct MergedEffect: AtomEffect { /// Creates an atom effect that merges multiple atom effects into one. public init(_ effect: repeat each Effect) { - initialized = { context in + initialized = { @Sendable context in repeat (each effect).initialized(context: context) } - updated = { context in + updated = { @Sendable context in repeat (each effect).updated(context: context) } - released = { context in + released = { @Sendable context in repeat (each effect).released(context: context) } } diff --git a/Sources/Atoms/PropertyWrapper/ViewContext.swift b/Sources/Atoms/PropertyWrapper/ViewContext.swift index 2dfebaba..2008dbc8 100644 --- a/Sources/Atoms/PropertyWrapper/ViewContext.swift +++ b/Sources/Atoms/PropertyWrapper/ViewContext.swift @@ -52,6 +52,9 @@ public struct ViewContext: DynamicProperty { /// This property provides primary access to the view context. However you don't /// access ``wrappedValue`` directly. /// Instead, you use the property variable created with the `@ViewContext` attribute. + #if hasFeature(DisableOutwardActorInference) + @MainActor + #endif public var wrappedValue: AtomViewContext { AtomViewContext( store: store, @@ -72,6 +75,7 @@ private extension ViewContext { let subscriberState = SubscriberState() } + @MainActor var store: StoreContext { guard let _store else { assertionFailure( diff --git a/Sources/Atoms/PropertyWrapper/Watch.swift b/Sources/Atoms/PropertyWrapper/Watch.swift index f5df9bf3..b9bd2a06 100644 --- a/Sources/Atoms/PropertyWrapper/Watch.swift +++ b/Sources/Atoms/PropertyWrapper/Watch.swift @@ -40,6 +40,9 @@ public struct Watch: DynamicProperty { /// access ``wrappedValue`` directly. Instead, you use the property variable created /// with the `@Watch` attribute. /// Accessing this property starts watching the atom. + #if hasFeature(DisableOutwardActorInference) + @MainActor + #endif public var wrappedValue: Node.Produced { context.watch(atom) } diff --git a/Sources/Atoms/PropertyWrapper/WatchState.swift b/Sources/Atoms/PropertyWrapper/WatchState.swift index a7c6f2f6..b75a3670 100644 --- a/Sources/Atoms/PropertyWrapper/WatchState.swift +++ b/Sources/Atoms/PropertyWrapper/WatchState.swift @@ -50,6 +50,9 @@ public struct WatchState: DynamicProperty { /// with the `@WatchState` attribute. /// Accessing to the getter of this property starts watching the atom, but doesn't /// by setting a new value. + #if hasFeature(DisableOutwardActorInference) + @MainActor + #endif public var wrappedValue: Node.Produced { get { context.watch(atom) } nonmutating set { context.set(newValue, for: atom) } @@ -61,6 +64,9 @@ public struct WatchState: DynamicProperty { /// To get the ``projectedValue``, prefix the property variable with `$`. /// Accessing this property itself does not start watching the atom, but does when /// the view accesses to the getter of the binding. + #if hasFeature(DisableOutwardActorInference) + @MainActor + #endif public var projectedValue: Binding { context.binding(atom) } diff --git a/Sources/Atoms/PropertyWrapper/WatchStateObject.swift b/Sources/Atoms/PropertyWrapper/WatchStateObject.swift index e044b95e..e9c498b2 100644 --- a/Sources/Atoms/PropertyWrapper/WatchStateObject.swift +++ b/Sources/Atoms/PropertyWrapper/WatchStateObject.swift @@ -46,6 +46,7 @@ public struct WatchStateObject: DynamicProperty { /// A wrapper of the underlying observable object that can create bindings to /// its properties using dynamic member lookup. @dynamicMemberLookup + @MainActor public struct Wrapper { private let object: Node.Produced @@ -83,6 +84,9 @@ public struct WatchStateObject: DynamicProperty { /// access ``wrappedValue`` directly. Instead, you use the property variable created /// with the `@WatchStateObject` attribute. /// Accessing this property starts watching the atom. + #if hasFeature(DisableOutwardActorInference) + @MainActor + #endif public var wrappedValue: Node.Produced { context.watch(atom) } @@ -91,6 +95,9 @@ public struct WatchStateObject: DynamicProperty { /// /// Use the projected value to pass a binding value down a view hierarchy. /// To get the projected value, prefix the property variable with `$`. + #if hasFeature(DisableOutwardActorInference) + @MainActor + #endif public var projectedValue: Wrapper { Wrapper(wrappedValue) } diff --git a/Tests/AtomsTests/Core/StoreContextTests.swift b/Tests/AtomsTests/Core/StoreContextTests.swift index 6a9ee160..f4033043 100644 --- a/Tests/AtomsTests/Core/StoreContextTests.swift +++ b/Tests/AtomsTests/Core/StoreContextTests.swift @@ -1529,10 +1529,6 @@ final class StoreContextTests: XCTestCase { let d = DAtom() let phase = PhaseAtom() - func watch() async -> Int { - await atomStore.watch(atom, subscriber: subscriber, subscription: Subscription()).value - } - do { // first @@ -1541,7 +1537,7 @@ final class StoreContextTests: XCTestCase { pipe.continuation.yield() } - let value = await watch() + let value = await atomStore.watch(atom, subscriber: subscriber, subscription: Subscription()).value XCTAssertEqual(value, 0) XCTAssertNil(store.graph.children[AtomKey(d)]) @@ -1568,8 +1564,8 @@ final class StoreContextTests: XCTestCase { pipe.reset() atomStore.set(.second, for: phase) - let before = await watch() - let after = await watch() + let before = await atomStore.watch(atom, subscriber: subscriber, subscription: Subscription()).value + let after = await atomStore.watch(atom, subscriber: subscriber, subscription: Subscription()).value XCTAssertEqual(before, 0) XCTAssertEqual(after, 1) @@ -1596,8 +1592,8 @@ final class StoreContextTests: XCTestCase { pipe.reset() atomStore.set(.third, for: phase) - let before = await watch() - let after = await watch() + let before = await atomStore.watch(atom, subscriber: subscriber, subscription: Subscription()).value + let after = await atomStore.watch(atom, subscriber: subscriber, subscription: Subscription()).value XCTAssertEqual(before, 1) XCTAssertEqual(after, 3) diff --git a/Tools/Package.resolved b/Tools/Package.resolved index 85cb4986..10b044a2 100644 --- a/Tools/Package.resolved +++ b/Tools/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "eaa10925341d06ca847c182626c709d014e4b62c8401b5b6545f08c0b096e23f", + "originHash" : "a807c1694a525436ee3bf845fdaabded7e6aef74d71d09780253d4058fecf5a7", "pins" : [ { "identity" : "aexml", @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-format.git", "state" : { - "revision" : "7996ac678197d293f6c088a1e74bb778b4e10139", - "version" : "510.1.0" + "revision" : "65f9da9aad84adb7e2028eb32ca95164aa590e3b", + "version" : "600.0.0" } }, { @@ -103,10 +103,10 @@ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { - "revision" : "2bc86522d115234d1f588efe2bcb4ce4be8f8b82", - "version" : "510.0.3" + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" } }, { From 9fe02c989a757317e8e3d8718e24abfe844fba9b Mon Sep 17 00:00:00 2001 From: ra1028 Date: Thu, 3 Oct 2024 16:31:27 +0900 Subject: [PATCH 03/10] Add support for typed throws in AsyncPhase --- Sources/Atoms/AsyncPhase.swift | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Sources/Atoms/AsyncPhase.swift b/Sources/Atoms/AsyncPhase.swift index d279930d..b845c017 100644 --- a/Sources/Atoms/AsyncPhase.swift +++ b/Sources/Atoms/AsyncPhase.swift @@ -28,15 +28,27 @@ public enum AsyncPhase { /// returned value as a success, or any thrown error as a failure. /// /// - Parameter body: A async throwing closure to evaluate. - public init(catching body: @Sendable () async throws -> Success) async where Failure == Error { - do { - let value = try await body() - self = .success(value) + #if compiler(>=6) + public init(catching body: @Sendable () async throws(Failure) -> Success) async { + do { + let value = try await body() + self = .success(value) + } + catch { + self = .failure(error) + } } - catch { - self = .failure(error) + #else + public init(catching body: @Sendable () async throws -> Success) async where Failure == Error { + do { + let value = try await body() + self = .success(value) + } + catch { + self = .failure(error) + } } - } + #endif /// A boolean value indicating whether `self` is ``AsyncPhase/suspending``. public var isSuspending: Bool { From c4e9e8f8835b9725f3b542b9818b84ec380c3527 Mon Sep 17 00:00:00 2001 From: ra1028 Date: Thu, 3 Oct 2024 17:00:56 +0900 Subject: [PATCH 04/10] Use nonisolated(unsafe) when possible --- Sources/Atoms/Core/Subscriber.swift | 8 ++--- Sources/Atoms/Core/SubscriberState.swift | 35 ++++++++++++++++--- .../Core/SubscriberStateTests.swift | 2 +- .../Core/SubscriberTestsTests.swift | 4 +-- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/Sources/Atoms/Core/Subscriber.swift b/Sources/Atoms/Core/Subscriber.swift index e1e44bee..8d2ab4ae 100644 --- a/Sources/Atoms/Core/Subscriber.swift +++ b/Sources/Atoms/Core/Subscriber.swift @@ -11,12 +11,12 @@ internal struct Subscriber { } var subscribing: Set { - get { state?.subscribing.value ?? [] } - nonmutating set { state?.subscribing.value = newValue } + get { state?.subscribing ?? [] } + nonmutating set { state?.subscribing = newValue } } var unsubscribe: ((Set) -> Void)? { - get { state?.unsubscribe.value } - nonmutating set { state?.unsubscribe.value = newValue } + get { state?.unsubscribe } + nonmutating set { state?.unsubscribe = newValue } } } diff --git a/Sources/Atoms/Core/SubscriberState.swift b/Sources/Atoms/Core/SubscriberState.swift index 7ca27db9..66976de5 100644 --- a/Sources/Atoms/Core/SubscriberState.swift +++ b/Sources/Atoms/Core/SubscriberState.swift @@ -3,19 +3,46 @@ import Foundation @MainActor internal final class SubscriberState { let token = SubscriberKey.Token() - var subscribing = UnsafeUncheckedSendable(Set()) - var unsubscribe = UnsafeUncheckedSendable<((Set) -> Void)?>(nil) + +#if compiler(>=6) + nonisolated(unsafe) var subscribing = Set() + nonisolated(unsafe) var unsubscribe: ((Set) -> Void)? // TODO: Use isolated synchronous deinit once it's available. // 0371-isolated-synchronous-deinit deinit { if Thread.isMainThread { - unsubscribe.value?(subscribing.value) + unsubscribe?(subscribing) } else { Task { @MainActor [unsubscribe, subscribing] in - unsubscribe.value?(subscribing.value) + unsubscribe?(subscribing) + } + } + } +#else + private var _subscribing = UnsafeUncheckedSendable(Set()) + private var _unsubscribe = UnsafeUncheckedSendable<((Set) -> Void)?>(nil) + + var subscribing: Set { + _read { yield _subscribing.value } + _modify { yield &_subscribing.value } + } + + var unsubscribe: ((Set) -> Void)? { + _read { yield _unsubscribe.value } + _modify { yield &_unsubscribe.value } + } + + deinit { + if Thread.isMainThread { + _unsubscribe.value?(_subscribing.value) + } + else { + Task { @MainActor [_unsubscribe, _subscribing] in + _unsubscribe.value?(_subscribing.value) } } } +#endif } diff --git a/Tests/AtomsTests/Core/SubscriberStateTests.swift b/Tests/AtomsTests/Core/SubscriberStateTests.swift index 9622b102..5bd0dd72 100644 --- a/Tests/AtomsTests/Core/SubscriberStateTests.swift +++ b/Tests/AtomsTests/Core/SubscriberStateTests.swift @@ -8,7 +8,7 @@ final class SubscriberStateTests: XCTestCase { var subscriberState: SubscriberState? = SubscriberState() var unsubscribedCount = 0 - subscriberState!.unsubscribe.value = { _ in + subscriberState!.unsubscribe = { _ in unsubscribedCount += 1 } diff --git a/Tests/AtomsTests/Core/SubscriberTestsTests.swift b/Tests/AtomsTests/Core/SubscriberTestsTests.swift index c23136e5..be6c045b 100644 --- a/Tests/AtomsTests/Core/SubscriberTestsTests.swift +++ b/Tests/AtomsTests/Core/SubscriberTestsTests.swift @@ -35,7 +35,7 @@ final class SubscriberTests: XCTestCase { subscriber.subscribing = expected - XCTAssertEqual(state?.subscribing.value, expected) + XCTAssertEqual(state?.subscribing, expected) state = nil @@ -52,7 +52,7 @@ final class SubscriberTests: XCTestCase { isUnsubscribed = true } - state?.unsubscribe.value?([]) + state?.unsubscribe?([]) XCTAssertTrue(isUnsubscribed) From cc9689283b119529b0a187354d2e29ef281058b8 Mon Sep 17 00:00:00 2001 From: ra1028 Date: Thu, 3 Oct 2024 17:13:04 +0900 Subject: [PATCH 05/10] Add support for InferSendableFromCaptures --- .../Atoms/Modifier/ChangesOfModifier.swift | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Sources/Atoms/Modifier/ChangesOfModifier.swift b/Sources/Atoms/Modifier/ChangesOfModifier.swift index a217d6ae..f34f9eb2 100644 --- a/Sources/Atoms/Modifier/ChangesOfModifier.swift +++ b/Sources/Atoms/Modifier/ChangesOfModifier.swift @@ -22,11 +22,19 @@ public extension Atom { /// - Parameter keyPath: A key path for the property of the original atom value. /// /// - Returns: An atom that provides the partial property of the original atom value. +#if hasFeature(InferSendableFromCaptures) + func changes( + of keyPath: KeyPath & Sendable + ) -> ModifiedAtom> { + modifier(ChangesOfModifier(keyPath: keyPath)) + } +#else func changes( of keyPath: KeyPath ) -> ModifiedAtom> { modifier(ChangesOfModifier(keyPath: keyPath)) } +#endif } /// A modifier that derives a partial property with the specified key path from the original atom @@ -49,22 +57,33 @@ public struct ChangesOfModifier: AtomModifier { } } - private let keyPath: UnsafeUncheckedSendable> +#if hasFeature(InferSendableFromCaptures) + private let keyPath: KeyPath & Sendable + + internal init(keyPath: KeyPath & Sendable) { + self.keyPath = keyPath + } +#else + private let _keyPath: UnsafeUncheckedSendable> + private var keyPath: KeyPath { + _keyPath.value + } internal init(keyPath: KeyPath) { - self.keyPath = UnsafeUncheckedSendable(keyPath) + _keyPath = UnsafeUncheckedSendable(keyPath) } +#endif /// A unique value used to identify the modifier internally. public var key: Key { - Key(keyPath: keyPath.value) + Key(keyPath: keyPath) } /// A producer that produces the value of this atom. public func producer(atom: some Atom) -> AtomProducer { AtomProducer { context in let value = context.transaction { $0.watch(atom) } - return value[keyPath: keyPath.value] + return value[keyPath: keyPath] } shouldUpdate: { oldValue, newValue in oldValue != newValue } From 6c30dccb9b53673dbaa22f6718e853392b8ba816 Mon Sep 17 00:00:00 2001 From: ra1028 Date: Thu, 3 Oct 2024 17:17:29 +0900 Subject: [PATCH 06/10] Add Swift v5 language mode --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 8b2f4b62..c7df51cc 100644 --- a/Package.swift +++ b/Package.swift @@ -20,5 +20,5 @@ let package = Package( dependencies: ["Atoms"] ), ], - swiftLanguageModes: [.v6] + swiftLanguageModes: [.v5, .v6] ) From 9712d1c3f6672aee8a2f4e63ce2147644f1d88fd Mon Sep 17 00:00:00 2001 From: ra1028 Date: Thu, 3 Oct 2024 17:30:59 +0900 Subject: [PATCH 07/10] Delete deprecated APIs --- Sources/Atoms/Core/Atom/Atom.swift | 7 --- Sources/Atoms/Core/StoreContext.swift | 2 - Sources/Atoms/Deprecated.swift | 82 --------------------------- 3 files changed, 91 deletions(-) delete mode 100644 Sources/Atoms/Deprecated.swift diff --git a/Sources/Atoms/Core/Atom/Atom.swift b/Sources/Atoms/Core/Atom/Atom.swift index f30b9047..563ac361 100644 --- a/Sources/Atoms/Core/Atom/Atom.swift +++ b/Sources/Atoms/Core/Atom/Atom.swift @@ -36,10 +36,6 @@ public protocol Atom: Sendable { @MainActor func effect(context: CurrentContext) -> Effect - /// Deprecated. use `Atom.effect(context:)` instead. - @MainActor - func updated(newValue: Produced, oldValue: Produced, context: CurrentContext) - // --- Internal --- /// A producer that produces the value of this atom. @@ -51,9 +47,6 @@ public extension Atom { func effect(context: CurrentContext) -> Effect where Effect == EmptyEffect { EmptyEffect() } - - @MainActor - func updated(newValue: Produced, oldValue: Produced, context: CurrentContext) {} } public extension Atom where Self == Key { diff --git a/Sources/Atoms/Core/StoreContext.swift b/Sources/Atoms/Core/StoreContext.swift index f5c8f801..a327b914 100644 --- a/Sources/Atoms/Core/StoreContext.swift +++ b/Sources/Atoms/Core/StoreContext.swift @@ -323,7 +323,6 @@ private extension StoreContext { // Perform side effects first. let state = getState(of: atom, for: key) let context = AtomCurrentContext(store: self) - atom.updated(newValue: newValue, oldValue: oldValue, context: context) state.effect.updated(context: context) // Calculate topological order for updating downstream efficiently. @@ -346,7 +345,6 @@ private extension StoreContext { // Perform side effects before updating downstream. let state = getState(of: cache.atom, for: key) - cache.atom.updated(newValue: newValue, oldValue: cache.value, context: context) state.effect.updated(context: context) } diff --git a/Sources/Atoms/Deprecated.swift b/Sources/Atoms/Deprecated.swift deleted file mode 100644 index 4fbd95a3..00000000 --- a/Sources/Atoms/Deprecated.swift +++ /dev/null @@ -1,82 +0,0 @@ -import SwiftUI - -public extension Atom { - @available(*, deprecated, message: "`Atom.updated(newValue:oldValue:context:)` is also deprecated. Use `Atom.effect(context:)` instead.") - typealias UpdatedContext = AtomCurrentContext - - @available(*, deprecated, renamed: "changes(of:)") - func select( - _ keyPath: KeyPath & Sendable - ) -> ModifiedAtom> { - changes(of: keyPath) - } -} - -public extension AtomWatchableContext { - @available(*, deprecated, renamed: "AtomViewContext.binding(_:)") - func state(_ atom: Node) -> Binding { - Binding( - get: { watch(atom) }, - set: { set($0, for: atom) } - ) - } -} - -public extension AtomTransactionContext { - @available(*, unavailable, message: "Use `AtomEffect` or create another atom to manage coordinator.") - var coordinator: Never { - fatalError("Obsoleted.") - } -} - -public extension AtomCurrentContext { - @available(*, unavailable, message: "Use `AtomEffect` or create another atom to manage coordinator.") - var coordinator: Never { - fatalError("Obsoleted.") - } -} - -public extension Resettable { - @available(*, deprecated, renamed: "CurrentContext") - typealias ResetContext = AtomCurrentContext -} - -public extension Refreshable { - @available(*, deprecated, renamed: "CurrentContext") - typealias RefreshContext = AtomCurrentContext -} - -public extension AtomScope { - @available(*, deprecated, renamed: "init(inheriting:content:)") - init( - _ context: AtomViewContext, - @ViewBuilder content: () -> Content - ) { - self.init(inheriting: context, content: content) - } - - @available(*, deprecated, renamed: "AtomRoot.init(storesIn:content:)") - init( - _ store: AtomStore, - @ViewBuilder content: () -> Root - ) where Content == AtomRoot { - self.init { - AtomRoot(storesIn: store, content: content) - } - } - - @available(*, deprecated, renamed: "scopedObserve(_:)") - func observe(_ onUpdate: @escaping @MainActor @Sendable (Snapshot) -> Void) -> Self { - scopedObserve(onUpdate) - } - - @available(*, deprecated, renamed: "scopedOverride(_:with:)") - func override(_ atom: Node, with value: @escaping @MainActor @Sendable (Node) -> Node.Produced) -> Self { - scopedOverride(atom, with: value) - } - - @available(*, deprecated, renamed: "scopedOverride(_:with:)") - func override(_ atomType: Node.Type, with value: @escaping @MainActor @Sendable (Node) -> Node.Produced) -> Self { - scopedOverride(atomType, with: value) - } -} From fee95ed59775e5cd1ba85cfa1ea7d6dbe818566c Mon Sep 17 00:00:00 2001 From: ra1028 Date: Thu, 3 Oct 2024 17:45:48 +0900 Subject: [PATCH 08/10] Format --- Sources/Atoms/Core/SubscriberState.swift | 62 +++++++++---------- .../Atoms/Modifier/ChangesOfModifier.swift | 54 ++++++++-------- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/Sources/Atoms/Core/SubscriberState.swift b/Sources/Atoms/Core/SubscriberState.swift index 66976de5..cc9a2f91 100644 --- a/Sources/Atoms/Core/SubscriberState.swift +++ b/Sources/Atoms/Core/SubscriberState.swift @@ -4,45 +4,45 @@ import Foundation internal final class SubscriberState { let token = SubscriberKey.Token() -#if compiler(>=6) - nonisolated(unsafe) var subscribing = Set() - nonisolated(unsafe) var unsubscribe: ((Set) -> Void)? + #if compiler(>=6) + nonisolated(unsafe) var subscribing = Set() + nonisolated(unsafe) var unsubscribe: ((Set) -> Void)? - // TODO: Use isolated synchronous deinit once it's available. - // 0371-isolated-synchronous-deinit - deinit { - if Thread.isMainThread { - unsubscribe?(subscribing) - } - else { - Task { @MainActor [unsubscribe, subscribing] in + // TODO: Use isolated synchronous deinit once it's available. + // 0371-isolated-synchronous-deinit + deinit { + if Thread.isMainThread { unsubscribe?(subscribing) } + else { + Task { @MainActor [unsubscribe, subscribing] in + unsubscribe?(subscribing) + } + } } - } -#else - private var _subscribing = UnsafeUncheckedSendable(Set()) - private var _unsubscribe = UnsafeUncheckedSendable<((Set) -> Void)?>(nil) - - var subscribing: Set { - _read { yield _subscribing.value } - _modify { yield &_subscribing.value } - } + #else + private var _subscribing = UnsafeUncheckedSendable(Set()) + private var _unsubscribe = UnsafeUncheckedSendable<((Set) -> Void)?>(nil) - var unsubscribe: ((Set) -> Void)? { - _read { yield _unsubscribe.value } - _modify { yield &_unsubscribe.value } - } + var subscribing: Set { + _read { yield _subscribing.value } + _modify { yield &_subscribing.value } + } - deinit { - if Thread.isMainThread { - _unsubscribe.value?(_subscribing.value) + var unsubscribe: ((Set) -> Void)? { + _read { yield _unsubscribe.value } + _modify { yield &_unsubscribe.value } } - else { - Task { @MainActor [_unsubscribe, _subscribing] in + + deinit { + if Thread.isMainThread { _unsubscribe.value?(_subscribing.value) } + else { + Task { @MainActor [_unsubscribe, _subscribing] in + _unsubscribe.value?(_subscribing.value) + } + } } - } -#endif + #endif } diff --git a/Sources/Atoms/Modifier/ChangesOfModifier.swift b/Sources/Atoms/Modifier/ChangesOfModifier.swift index f34f9eb2..0bb7c526 100644 --- a/Sources/Atoms/Modifier/ChangesOfModifier.swift +++ b/Sources/Atoms/Modifier/ChangesOfModifier.swift @@ -22,19 +22,19 @@ public extension Atom { /// - Parameter keyPath: A key path for the property of the original atom value. /// /// - Returns: An atom that provides the partial property of the original atom value. -#if hasFeature(InferSendableFromCaptures) - func changes( - of keyPath: KeyPath & Sendable - ) -> ModifiedAtom> { - modifier(ChangesOfModifier(keyPath: keyPath)) - } -#else - func changes( - of keyPath: KeyPath - ) -> ModifiedAtom> { - modifier(ChangesOfModifier(keyPath: keyPath)) - } -#endif + #if hasFeature(InferSendableFromCaptures) + func changes( + of keyPath: KeyPath & Sendable + ) -> ModifiedAtom> { + modifier(ChangesOfModifier(keyPath: keyPath)) + } + #else + func changes( + of keyPath: KeyPath + ) -> ModifiedAtom> { + modifier(ChangesOfModifier(keyPath: keyPath)) + } + #endif } /// A modifier that derives a partial property with the specified key path from the original atom @@ -57,22 +57,22 @@ public struct ChangesOfModifier: AtomModifier { } } -#if hasFeature(InferSendableFromCaptures) - private let keyPath: KeyPath & Sendable + #if hasFeature(InferSendableFromCaptures) + private let keyPath: KeyPath & Sendable - internal init(keyPath: KeyPath & Sendable) { - self.keyPath = keyPath - } -#else - private let _keyPath: UnsafeUncheckedSendable> - private var keyPath: KeyPath { - _keyPath.value - } + internal init(keyPath: KeyPath & Sendable) { + self.keyPath = keyPath + } + #else + private let _keyPath: UnsafeUncheckedSendable> + private var keyPath: KeyPath { + _keyPath.value + } - internal init(keyPath: KeyPath) { - _keyPath = UnsafeUncheckedSendable(keyPath) - } -#endif + internal init(keyPath: KeyPath) { + _keyPath = UnsafeUncheckedSendable(keyPath) + } + #endif /// A unique value used to identify the modifier internally. public var key: Key { From 8027e5443f1c972be452e80f9561e3eea5a6cc0e Mon Sep 17 00:00:00 2001 From: ra1028 Date: Thu, 3 Oct 2024 17:48:28 +0900 Subject: [PATCH 09/10] Lint warnings --- Sources/Atoms/AsyncPhase.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Sources/Atoms/AsyncPhase.swift b/Sources/Atoms/AsyncPhase.swift index b845c017..0c981c6a 100644 --- a/Sources/Atoms/AsyncPhase.swift +++ b/Sources/Atoms/AsyncPhase.swift @@ -24,11 +24,11 @@ public enum AsyncPhase { } } - /// Creates a new phase by evaluating a async throwing closure, capturing the - /// returned value as a success, or any thrown error as a failure. - /// - /// - Parameter body: A async throwing closure to evaluate. #if compiler(>=6) + /// Creates a new phase by evaluating a async throwing closure, capturing the + /// returned value as a success, or thrown error as a failure. + /// + /// - Parameter body: A async throwing closure to evaluate. public init(catching body: @Sendable () async throws(Failure) -> Success) async { do { let value = try await body() @@ -39,6 +39,10 @@ public enum AsyncPhase { } } #else + /// Creates a new phase by evaluating a async throwing closure, capturing the + /// returned value as a success, or thrown error as a failure. + /// + /// - Parameter body: A async throwing closure to evaluate. public init(catching body: @Sendable () async throws -> Success) async where Failure == Error { do { let value = try await body() From 8fd0b48046bcba4d01f0f9b2c68cf4f8d7a0f7e2 Mon Sep 17 00:00:00 2001 From: ra1028 Date: Thu, 3 Oct 2024 17:53:27 +0900 Subject: [PATCH 10/10] Fix CI testing flow --- .github/workflows/test.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33002b00..bf81df12 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,17 +32,18 @@ jobs: - uses: actions/checkout@v4 - name: Test library run: scripts/test.sh library ${{ matrix.platform }} - - name: Test example cross platform - if: matrix.platform == 'macos' || matrix.platform == 'tvos' - run: scripts/test.sh example-cross-platform ${{ matrix.platform }} test_examples: - name: Test iOS examples + name: Test examples runs-on: macos-14 steps: - uses: actions/checkout@v4 - name: Test example iOS run: scripts/test.sh example-ios ios + - name: Test example macOS + run: scripts/test.sh example-cross-platform macos + - name: Test example tvOS + run: scripts/test.sh example-cross-platform tvos benchmark: name: Benchmark