diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 649006d6..12e74572 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,12 +8,12 @@ on: workflow_dispatch: env: - DEVELOPER_DIR: /Applications/Xcode_15.2.app + DEVELOPER_DIR: /Applications/Xcode_16.0.app jobs: publish-docs: name: Publish Documentation - runs-on: macos-13 + runs-on: macos-14 steps: - uses: actions/checkout@v4 - name: Build docs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ef75b88..932df46f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,12 +10,12 @@ on: workflow_dispatch: env: - DEVELOPER_DIR: /Applications/Xcode_15.2.app + DEVELOPER_DIR: /Applications/Xcode_16.0.app jobs: test: name: Test - runs-on: macos-13 + runs-on: macos-14 strategy: matrix: platform: @@ -33,7 +33,7 @@ jobs: test_examples: name: Test iOS examples - runs-on: macos-13 + runs-on: macos-14 steps: - uses: actions/checkout@v4 - name: Test example iOS @@ -41,7 +41,7 @@ jobs: benchmark: name: Benchmark - runs-on: macos-13 + runs-on: macos-14 steps: - uses: actions/checkout@v4 - name: Run benchmark test @@ -49,7 +49,7 @@ jobs: validation: name: Validation - runs-on: macos-13 + runs-on: macos-14 steps: - uses: actions/checkout@v4 - name: Show environments diff --git a/Benchmarks/Test.xcodeproj/project.pbxproj b/Benchmarks/Test.xcodeproj/project.pbxproj index 798a995e..93977cd8 100644 --- a/Benchmarks/Test.xcodeproj/project.pbxproj +++ b/Benchmarks/Test.xcodeproj/project.pbxproj @@ -155,6 +155,9 @@ en, ); mainGroup = 5071FF1E3D9776144CA101D1; + packageReferences = ( + BF2795A13208F1083E80780C /* XCLocalSwiftPackageReference ".." */, + ); projectDirPath = ""; projectRoot = ""; targets = ( @@ -248,7 +251,7 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6; }; name = Release; }; @@ -363,7 +366,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6; }; name = Debug; }; @@ -415,6 +418,13 @@ }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + BF2795A13208F1083E80780C /* XCLocalSwiftPackageReference ".." */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ..; + }; +/* End XCLocalSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 8FA28D81FB67542B978C77EC /* Atoms */ = { isa = XCSwiftPackageProductDependency; diff --git a/Benchmarks/Tests/BenchmarkTests.swift b/Benchmarks/Tests/BenchmarkTests.swift index 478f9f72..3ff6a8fa 100644 --- a/Benchmarks/Tests/BenchmarkTests.swift +++ b/Benchmarks/Tests/BenchmarkTests.swift @@ -14,9 +14,12 @@ final class RenderingPerformanceTests: XCTestCase { test.perform { measure { for _ in 0..<100 { - test.sendTouchSequence([ - (location: CGPoint(x: size.width / 2, y: size.height - 30), globalLocation: nil, timestamp: Date()) - ]) + test.sendTouchSequence( + Array( + repeating: (location: CGPoint(x: size.width / 2, y: size.height - 30), globalLocation: nil, timestamp: Date()), + count: 2 + ) + ) test.turnRunloop(times: 1) } } diff --git a/Benchmarks/Tests/ViewTest.swift b/Benchmarks/Tests/ViewTest.swift index c289ef36..0521e8ba 100644 --- a/Benchmarks/Tests/ViewTest.swift +++ b/Benchmarks/Tests/ViewTest.swift @@ -1,14 +1,18 @@ import SwiftUI -struct ViewTest: _ViewTest { - let rootView: () -> Content +struct ViewTest: _ViewTest { + let rootView: @MainActor () -> Content func initRootView() -> some View { - rootView() + MainActor.assumeIsolated { + rootView() + } } func initSize() -> CGSize { - UIScreen.main.bounds.size + MainActor.assumeIsolated { + UIScreen.main.bounds.size + } } func perform(_ body: () -> Void) { diff --git a/Benchmarks/project.yml b/Benchmarks/project.yml index 717333cc..47275dec 100644 --- a/Benchmarks/project.yml +++ b/Benchmarks/project.yml @@ -5,6 +5,7 @@ options: createIntermediateGroups: true settings: + SWIFT_VERSION: 6 CODE_SIGNING_REQUIRED: NO CODE_SIGN_IDENTITY: "-" CODE_SIGN_STYLE: Manual diff --git a/Examples/App.xcodeproj/project.pbxproj b/Examples/App.xcodeproj/project.pbxproj index 5560971a..fe6de17e 100644 --- a/Examples/App.xcodeproj/project.pbxproj +++ b/Examples/App.xcodeproj/project.pbxproj @@ -179,6 +179,10 @@ en, ); mainGroup = 4102E3E068508DD683953C7D; + packageReferences = ( + 263B59300A73460F3EC004F2 /* XCLocalSwiftPackageReference "Packages/CrossPlatform" */, + B5EB40B15A9876E9D1761363 /* XCLocalSwiftPackageReference "Packages/iOS" */, + ); projectDirPath = ""; projectRoot = ""; targets = ( @@ -320,7 +324,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6; TVOS_DEPLOYMENT_TARGET = 16.0; }; name = Release; @@ -437,7 +441,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6; TVOS_DEPLOYMENT_TARGET = 16.0; }; name = Debug; @@ -483,6 +487,17 @@ }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 263B59300A73460F3EC004F2 /* XCLocalSwiftPackageReference "Packages/CrossPlatform" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Packages/CrossPlatform; + }; + B5EB40B15A9876E9D1761363 /* XCLocalSwiftPackageReference "Packages/iOS" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Packages/iOS; + }; +/* End XCLocalSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ A708E16BF51AE12E22A59212 /* CrossPlatformApp */ = { isa = XCSwiftPackageProductDependency; diff --git a/Examples/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Examples/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/Examples/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Examples/App.xcodeproj/xcshareddata/xcschemes/CrossPlatform_macOS.xcscheme b/Examples/App.xcodeproj/xcshareddata/xcschemes/CrossPlatform_macOS.xcscheme new file mode 100644 index 00000000..4457cdef --- /dev/null +++ b/Examples/App.xcodeproj/xcshareddata/xcschemes/CrossPlatform_macOS.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/App.xcodeproj/xcshareddata/xcschemes/CrossPlatform_tvOS.xcscheme b/Examples/App.xcodeproj/xcshareddata/xcschemes/CrossPlatform_tvOS.xcscheme new file mode 100644 index 00000000..da8391a2 --- /dev/null +++ b/Examples/App.xcodeproj/xcshareddata/xcschemes/CrossPlatform_tvOS.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/App.xcodeproj/xcshareddata/xcschemes/iOS.xcscheme b/Examples/App.xcodeproj/xcshareddata/xcschemes/iOS.xcscheme new file mode 100644 index 00000000..c9bb9858 --- /dev/null +++ b/Examples/App.xcodeproj/xcshareddata/xcschemes/iOS.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/Packages/CrossPlatform/Package.swift b/Examples/Packages/CrossPlatform/Package.swift index fe6977e3..ac0d7b35 100644 --- a/Examples/Packages/CrossPlatform/Package.swift +++ b/Examples/Packages/CrossPlatform/Package.swift @@ -1,25 +1,18 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 import PackageDescription -let swiftSettings: [SwiftSetting] = [ - .unsafeFlags(["-Xfrontend", "-strict-concurrency=complete"]), - .unsafeFlags(["-Xfrontend", "-enable-actor-data-race-checks"]), -] - func target(name: String, dependencies: [Target.Dependency] = []) -> Target { .target( name: name, - dependencies: [.product(name: "Atoms", package: "swiftui-atom-properties")] + dependencies, - swiftSettings: swiftSettings + dependencies: [.product(name: "Atoms", package: "swiftui-atom-properties")] + dependencies ) } func testTarget(name: String, dependencies: [Target.Dependency]) -> Target { .testTarget( name: name, - dependencies: dependencies, - swiftSettings: swiftSettings + dependencies: dependencies ) } diff --git a/Examples/Packages/iOS/Package.swift b/Examples/Packages/iOS/Package.swift index 25caf3cb..305b2406 100644 --- a/Examples/Packages/iOS/Package.swift +++ b/Examples/Packages/iOS/Package.swift @@ -1,25 +1,18 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 import PackageDescription -let swiftSettings: [SwiftSetting] = [ - .unsafeFlags(["-Xfrontend", "-strict-concurrency=complete"]), - .unsafeFlags(["-Xfrontend", "-enable-actor-data-race-checks"]), -] - func target(name: String, dependencies: [Target.Dependency] = []) -> Target { .target( name: name, - dependencies: [.product(name: "Atoms", package: "swiftui-atom-properties")] + dependencies, - swiftSettings: swiftSettings + dependencies: [.product(name: "Atoms", package: "swiftui-atom-properties")] + dependencies ) } func testTarget(name: String, dependencies: [Target.Dependency]) -> Target { .testTarget( name: name, - dependencies: dependencies, - swiftSettings: swiftSettings + dependencies: dependencies ) } diff --git a/Examples/Packages/iOS/Sources/ExampleVoiceMemo/Dependency/AudioRecorder.swift b/Examples/Packages/iOS/Sources/ExampleVoiceMemo/Dependency/AudioRecorder.swift index 0670d710..e5252477 100644 --- a/Examples/Packages/iOS/Sources/ExampleVoiceMemo/Dependency/AudioRecorder.swift +++ b/Examples/Packages/iOS/Sources/ExampleVoiceMemo/Dependency/AudioRecorder.swift @@ -1,4 +1,5 @@ import AVFoundation +import os protocol AudioRecorderProtocol { var currentTime: TimeInterval { get } @@ -7,8 +8,8 @@ protocol AudioRecorderProtocol { func stop() } -final class AudioRecorder: NSObject, AVAudioRecorderDelegate, AudioRecorderProtocol { - private var recorder: AVAudioRecorder? +final class AudioRecorder: NSObject, AVAudioRecorderDelegate, AudioRecorderProtocol, @unchecked Sendable { + private var recorder = OSAllocatedUnfairLock(uncheckedState: nil) private let onFail: () -> Void init(onFail: @escaping () -> Void) { @@ -17,26 +18,30 @@ final class AudioRecorder: NSObject, AVAudioRecorderDelegate, AudioRecorderProto } var currentTime: TimeInterval { - recorder?.currentTime ?? .zero + recorder.withLock(\.?.currentTime) ?? .zero } func record(url: URL) throws { - recorder = try AVAudioRecorder( - url: url, - settings: [ - AVFormatIDKey: Int(kAudioFormatMPEG4AAC), - AVSampleRateKey: 44100, - AVNumberOfChannelsKey: 1, - AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue, - ] - ) - recorder?.delegate = self - recorder?.record() + try recorder.withLock { recorder in + recorder = try AVAudioRecorder( + url: url, + settings: [ + AVFormatIDKey: Int(kAudioFormatMPEG4AAC), + AVSampleRateKey: 44100, + AVNumberOfChannelsKey: 1, + AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue, + ] + ) + recorder?.delegate = self + recorder?.record() + } } func stop() { - recorder?.stop() - recorder = nil + recorder.withLock { recorder in + recorder?.stop() + recorder = nil + } } func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { diff --git a/Examples/project.yml b/Examples/project.yml index 425fcfe2..cb01d236 100644 --- a/Examples/project.yml +++ b/Examples/project.yml @@ -9,6 +9,7 @@ options: createIntermediateGroups: true settings: + SWIFT_VERSION: 6 CODE_SIGNING_REQUIRED: NO CODE_SIGN_IDENTITY: "-" CODE_SIGN_STYLE: Manual @@ -34,6 +35,22 @@ packages: CrossPlatformApp: path: Packages/CrossPlatform +schemes: + iOS: + build: + targets: + iOS: all + + CrossPlatform_macOS: + build: + targets: + CrossPlatform_macOS: all + + CrossPlatform_tvOS: + build: + targets: + CrossPlatform_tvOS: all + targets: iOS: templates: diff --git a/Makefile b/Makefile index ba1f75f7..a90e619b 100644 --- a/Makefile +++ b/Makefile @@ -4,10 +4,6 @@ SWIFT_FILE_PATHS = Package.swift Tools/Package.swift Sources Tests Examples Benc XCODEGEN = SWIFT_PACKAGE_RESOURCES=Tools/.build/checkouts/XcodeGen/SettingPresets $(TOOL) xcodegen SWIFTFORMAT = $(TOOL) swift-format -.PHONY: open-dev -open-dev: - DEVELOPMENT=1 open Package.swift - .PHONY: proj proj: $(XCODEGEN) -s Examples/project.yml diff --git a/Package.swift b/Package.swift index e377a1e8..7429136b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 import Foundation import PackageDescription @@ -21,14 +21,5 @@ let package = Package( dependencies: ["Atoms"] ), ], - swiftLanguageVersions: [.v5] + swiftLanguageModes: [.v6] ) - -if ProcessInfo.processInfo.environment["DEVELOPMENT"] != nil { - for target in package.targets { - target.swiftSettings = [ - .unsafeFlags(["-Xfrontend", "-strict-concurrency=complete"]), - .unsafeFlags(["-Xfrontend", "-enable-actor-data-race-checks"]), - ] - } -} diff --git a/README.md b/README.md index 92ab5dc5..a60df7c3 100644 --- a/README.md +++ b/README.md @@ -127,8 +127,8 @@ Open `Examples/App.xcodeproj` and play around with it! | |Minimum Version| |------:|--------------:| -|Swift |5.9 | -|Xcode |15.2 | +|Swift |6.0 | +|Xcode |16.0 | |iOS |14.0 | |macOS |11.0 | |tvOS |14.0 | diff --git a/Sources/Atoms/AsyncPhase.swift b/Sources/Atoms/AsyncPhase.swift index d279930d..3d40286e 100644 --- a/Sources/Atoms/AsyncPhase.swift +++ b/Sources/Atoms/AsyncPhase.swift @@ -28,7 +28,7 @@ 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 { + public init(catching body: @Sendable () async throws(Failure) -> Success) async { do { let value = try await body() self = .success(value) 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 87d78e77..c5d59646 100644 --- a/Sources/Atoms/Atom/PublisherAtom.swift +++ b/Sources/Atoms/Atom/PublisherAtom.swift @@ -1,4 +1,4 @@ -import Combine +@preconcurrency import Combine /// An atom type that provides a sequence of values of the given `Publisher` as an ``AsyncPhase`` value. /// @@ -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/Atom/TaskAtom.swift b/Sources/Atoms/Atom/TaskAtom.swift index 971a990c..8e48c023 100644 --- a/Sources/Atoms/Atom/TaskAtom.swift +++ b/Sources/Atoms/Atom/TaskAtom.swift @@ -54,7 +54,7 @@ public protocol TaskAtom: AsyncAtom where Produced == Task { public extension TaskAtom { var producer: AtomProducer { AtomProducer { context in - Task { [value] in + Task { await context.transaction(value) } } manageValue: { task, context in @@ -64,7 +64,7 @@ public extension TaskAtom { var refreshProducer: AtomRefreshProducer { AtomRefreshProducer { context in - Task { [value] in + Task { await context.transaction(value) } } refreshValue: { task, context in diff --git a/Sources/Atoms/Atom/ThrowingTaskAtom.swift b/Sources/Atoms/Atom/ThrowingTaskAtom.swift index e62665e0..0a77070a 100644 --- a/Sources/Atoms/Atom/ThrowingTaskAtom.swift +++ b/Sources/Atoms/Atom/ThrowingTaskAtom.swift @@ -58,7 +58,7 @@ public protocol ThrowingTaskAtom: AsyncAtom where Produced == Task { AtomProducer { context in - Task { [value] in + Task { try await context.transaction(value) } } manageValue: { task, context in @@ -68,7 +68,7 @@ public extension ThrowingTaskAtom { var refreshProducer: AtomRefreshProducer { AtomRefreshProducer { context in - Task { [value] in + Task { try await context.transaction(value) } } refreshValue: { task, context in diff --git a/Sources/Atoms/Attribute/Scoped.swift b/Sources/Atoms/Attribute/Scoped.swift index 509f585f..93c6f1c6 100644 --- a/Sources/Atoms/Attribute/Scoped.swift +++ b/Sources/Atoms/Attribute/Scoped.swift @@ -45,7 +45,7 @@ public extension Scoped where ScopeID == DefaultScopeID { } /// A default scope ID to find a matching scope inbetween scoped atoms and ``AtomScope``. -public struct DefaultScopeID: Hashable { +public struct DefaultScopeID: Hashable & Sendable { /// Creates a new default scope ID which is always indentical. public init() {} } diff --git a/Sources/Atoms/Context/AtomTestContext.swift b/Sources/Atoms/Context/AtomTestContext.swift index e042f1ee..82e54b26 100644 --- a/Sources/Atoms/Context/AtomTestContext.swift +++ b/Sources/Atoms/Context/AtomTestContext.swift @@ -1,4 +1,4 @@ -import Combine +@preconcurrency import Combine /// A context structure to read, watch, and otherwise interact with atoms in testing. /// @@ -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/Core/Atom/Atom.swift b/Sources/Atoms/Core/Atom/Atom.swift index e29c3506..f30b9047 100644 --- a/Sources/Atoms/Core/Atom/Atom.swift +++ b/Sources/Atoms/Core/Atom/Atom.swift @@ -2,7 +2,7 @@ /// /// The value produced by an atom is created only when the atom is watched from somewhere, /// and is immediately released when no longer watched. -public protocol Atom { +public protocol Atom: Sendable { /// A type representing the stable identity of this atom. associatedtype Key: Hashable diff --git a/Sources/Atoms/Core/LockSendable.swift b/Sources/Atoms/Core/LockSendable.swift new file mode 100644 index 00000000..67b9cd0a --- /dev/null +++ b/Sources/Atoms/Core/LockSendable.swift @@ -0,0 +1,41 @@ +import os + +internal final class LockSendable: @unchecked Sendable { + private var _value: Value + private let lock = os_unfair_lock_t.allocate(capacity: 1) + + init(_ value: Value) { + _value = value + lock.initialize(to: os_unfair_lock()) + } + + deinit { + lock.deinitialize(count: 1) + lock.deallocate() + } + + var value: Value { + _read { + lock.lock() + defer { lock.unlock() } + yield _value + } + _modify { + lock.lock() + defer { lock.unlock() } + yield &_value + } + } +} + +private extension UnsafeMutablePointer where Pointee == os_unfair_lock_s { + @inline(__always) + func lock() { + os_unfair_lock_lock(self) + } + + @inline(__always) + func unlock() { + os_unfair_lock_unlock(self) + } +} diff --git a/Sources/Atoms/Core/Modifier/AtomModifier.swift b/Sources/Atoms/Core/Modifier/AtomModifier.swift index 45c8822e..ff920616 100644 --- a/Sources/Atoms/Core/Modifier/AtomModifier.swift +++ b/Sources/Atoms/Core/Modifier/AtomModifier.swift @@ -9,7 +9,7 @@ public extension Atom { } /// A modifier that you apply to an atom, producing a new value modified from the original value. -public protocol AtomModifier { +public protocol AtomModifier: Sendable { /// A type representing the stable identity of this modifier. associatedtype Key: Hashable diff --git a/Sources/Atoms/Core/StoreState.swift b/Sources/Atoms/Core/StoreState.swift index 22570339..88b784a9 100644 --- a/Sources/Atoms/Core/StoreState.swift +++ b/Sources/Atoms/Core/StoreState.swift @@ -3,4 +3,6 @@ internal final class StoreState { var caches = [AtomKey: any AtomCacheProtocol]() var states = [AtomKey: any AtomStateProtocol]() var subscriptions = [AtomKey: [SubscriberKey: Subscription]]() + + nonisolated init() {} } diff --git a/Sources/Atoms/Core/Subscriber.swift b/Sources/Atoms/Core/Subscriber.swift index 8d2ab4ae..e1e44bee 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 ?? [] } - nonmutating set { state?.subscribing = newValue } + get { state?.subscribing.value ?? [] } + nonmutating set { state?.subscribing.value = newValue } } var unsubscribe: ((Set) -> Void)? { - get { state?.unsubscribe } - nonmutating set { state?.unsubscribe = newValue } + get { state?.unsubscribe.value } + nonmutating set { state?.unsubscribe.value = newValue } } } diff --git a/Sources/Atoms/Core/SubscriberState.swift b/Sources/Atoms/Core/SubscriberState.swift index 0b2a3ba9..40e118dc 100644 --- a/Sources/Atoms/Core/SubscriberState.swift +++ b/Sources/Atoms/Core/SubscriberState.swift @@ -3,18 +3,18 @@ import Foundation @MainActor internal final class SubscriberState { let token = SubscriberKey.Token() - var subscribing = Set() - var unsubscribe: ((Set) -> Void)? + let subscribing = LockSendable(Set()) + let unsubscribe = LockSendable<((Set) -> Void)?>(nil) // TODO: Use isolated synchronous deinit once it's available. // 0371-isolated-synchronous-deinit deinit { if Thread.isMainThread { - unsubscribe?(subscribing) + unsubscribe.value?(subscribing.value) } else { Task(priority: .high) { @MainActor [unsubscribe, subscribing] in - unsubscribe?(subscribing) + unsubscribe.value?(subscribing.value) } } } diff --git a/Sources/Atoms/Deprecated.swift b/Sources/Atoms/Deprecated.swift index 810cea6a..4fbd95a3 100644 --- a/Sources/Atoms/Deprecated.swift +++ b/Sources/Atoms/Deprecated.swift @@ -6,7 +6,7 @@ public extension Atom { @available(*, deprecated, renamed: "changes(of:)") func select( - _ keyPath: KeyPath + _ keyPath: KeyPath & Sendable ) -> ModifiedAtom> { changes(of: keyPath) } 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/Modifier/ChangesOfModifier.swift b/Sources/Atoms/Modifier/ChangesOfModifier.swift index 5f74fe35..00401438 100644 --- a/Sources/Atoms/Modifier/ChangesOfModifier.swift +++ b/Sources/Atoms/Modifier/ChangesOfModifier.swift @@ -23,7 +23,7 @@ public extension Atom { /// /// - Returns: An atom that provides the partial property of the original atom value. func changes( - of keyPath: KeyPath + of keyPath: KeyPath & Sendable ) -> ModifiedAtom> { modifier(ChangesOfModifier(keyPath: keyPath)) } @@ -49,9 +49,9 @@ public struct ChangesOfModifier: AtomModifier { } } - private let keyPath: KeyPath + private let keyPath: KeyPath & Sendable - internal init(keyPath: KeyPath) { + internal init(keyPath: KeyPath & Sendable) { self.keyPath = keyPath } diff --git a/Sources/Atoms/PropertyWrapper/ViewContext.swift b/Sources/Atoms/PropertyWrapper/ViewContext.swift index 2dfebaba..29abc64c 100644 --- a/Sources/Atoms/PropertyWrapper/ViewContext.swift +++ b/Sources/Atoms/PropertyWrapper/ViewContext.swift @@ -52,6 +52,7 @@ 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. + @MainActor public var wrappedValue: AtomViewContext { AtomViewContext( store: store, @@ -72,6 +73,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..beaa51d5 100644 --- a/Sources/Atoms/PropertyWrapper/Watch.swift +++ b/Sources/Atoms/PropertyWrapper/Watch.swift @@ -40,6 +40,7 @@ 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. + @MainActor public var wrappedValue: Node.Produced { context.watch(atom) } diff --git a/Sources/Atoms/PropertyWrapper/WatchState.swift b/Sources/Atoms/PropertyWrapper/WatchState.swift index a7c6f2f6..0a052260 100644 --- a/Sources/Atoms/PropertyWrapper/WatchState.swift +++ b/Sources/Atoms/PropertyWrapper/WatchState.swift @@ -50,6 +50,7 @@ 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. + @MainActor public var wrappedValue: Node.Produced { get { context.watch(atom) } nonmutating set { context.set(newValue, for: atom) } @@ -61,6 +62,7 @@ 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. + @MainActor public var projectedValue: Binding { context.binding(atom) } diff --git a/Sources/Atoms/PropertyWrapper/WatchStateObject.swift b/Sources/Atoms/PropertyWrapper/WatchStateObject.swift index e044b95e..c19dbcec 100644 --- a/Sources/Atoms/PropertyWrapper/WatchStateObject.swift +++ b/Sources/Atoms/PropertyWrapper/WatchStateObject.swift @@ -54,6 +54,7 @@ public struct WatchStateObject: DynamicProperty { /// - Parameter keyPath: A key path to a specific resulting value. /// /// - Returns: A new binding. + @MainActor public subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> Binding { Binding( get: { object[keyPath: keyPath] }, @@ -83,6 +84,7 @@ 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. + @MainActor public var wrappedValue: Node.Produced { context.watch(atom) } @@ -91,6 +93,7 @@ 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 `$`. + @MainActor public var projectedValue: Wrapper { Wrapper(wrappedValue) } diff --git a/Tests/AtomsTests/Attribute/KeepAliveTests.swift b/Tests/AtomsTests/Attribute/KeepAliveTests.swift index e0b70c73..ccddf84a 100644 --- a/Tests/AtomsTests/Attribute/KeepAliveTests.swift +++ b/Tests/AtomsTests/Attribute/KeepAliveTests.swift @@ -5,7 +5,7 @@ import XCTest final class KeepAliveTests: XCTestCase { @MainActor func testKeepAliveAtoms() { - struct KeepAliveAtom: ValueAtom, KeepAlive, Hashable { + struct KeepAliveAtom: ValueAtom, KeepAlive, Hashable { let value: T func value(context: Context) -> T { @@ -13,7 +13,7 @@ final class KeepAliveTests: XCTestCase { } } - struct ScopedKeepAliveAtom: ValueAtom, KeepAlive, Scoped, Hashable { + struct ScopedKeepAliveAtom: ValueAtom, KeepAlive, Scoped, Hashable { let value: T func value(context: Context) -> T { diff --git a/Tests/AtomsTests/Attribute/ScopedTests.swift b/Tests/AtomsTests/Attribute/ScopedTests.swift index ca66a8c6..7d94e8b8 100644 --- a/Tests/AtomsTests/Attribute/ScopedTests.swift +++ b/Tests/AtomsTests/Attribute/ScopedTests.swift @@ -5,7 +5,7 @@ import XCTest final class ScopedTests: XCTestCase { @MainActor func testScopedAtoms() { - struct ScopedAtom: ValueAtom, Scoped, Equatable { + struct ScopedAtom: ValueAtom, Scoped, Equatable { let key = UniqueKey() let scopeID: ID let value: T diff --git a/Tests/AtomsTests/Core/StoreContextTests.swift b/Tests/AtomsTests/Core/StoreContextTests.swift index 3996b704..440f6ad2 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/Tests/AtomsTests/Core/SubscriberStateTests.swift b/Tests/AtomsTests/Core/SubscriberStateTests.swift index 5bd0dd72..9622b102 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 = { _ in + subscriberState!.unsubscribe.value = { _ in unsubscribedCount += 1 } diff --git a/Tests/AtomsTests/Core/SubscriberTestsTests.swift b/Tests/AtomsTests/Core/SubscriberTestsTests.swift index be6c045b..c23136e5 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, expected) + XCTAssertEqual(state?.subscribing.value, expected) state = nil @@ -52,7 +52,7 @@ final class SubscriberTests: XCTestCase { isUnsubscribed = true } - state?.unsubscribe?([]) + state?.unsubscribe.value?([]) XCTAssertTrue(isUnsubscribed) diff --git a/Tests/AtomsTests/Utilities/TestAtom.swift b/Tests/AtomsTests/Utilities/TestAtom.swift index 381ef464..9e7cfc32 100644 --- a/Tests/AtomsTests/Utilities/TestAtom.swift +++ b/Tests/AtomsTests/Utilities/TestAtom.swift @@ -3,7 +3,7 @@ import Combine @testable import Atoms struct TestAtom: ValueAtom, Hashable { - var value: T + nonisolated(unsafe) var value: T func value(context: Context) -> T { value @@ -11,7 +11,7 @@ struct TestAtom: ValueAtom, Hashable { } struct TestValueAtom: ValueAtom { - var value: T + nonisolated(unsafe) var value: T var effect: TestEffect? var key: UniqueKey { @@ -28,7 +28,7 @@ struct TestValueAtom: ValueAtom { } struct TestStateAtom: StateAtom { - var defaultValue: T + nonisolated(unsafe) var defaultValue: T var effect: TestEffect? var key: UniqueKey { @@ -46,7 +46,7 @@ struct TestStateAtom: StateAtom { struct TestTaskAtom: TaskAtom { var effect: TestEffect? - var getValue: () -> T + nonisolated(unsafe) var getValue: () -> T var key: UniqueKey { UniqueKey() @@ -63,7 +63,7 @@ struct TestTaskAtom: TaskAtom { struct TestThrowingTaskAtom: ThrowingTaskAtom { var effect: TestEffect? - var getResult: () -> Result + nonisolated(unsafe) var getResult: () -> Result var key: UniqueKey { UniqueKey() @@ -79,8 +79,8 @@ struct TestThrowingTaskAtom: ThrowingTaskAtom { } struct TestCustomRefreshableAtom: ValueAtom, Refreshable { - var getValue: (Context) -> T - var refresh: (CurrentContext) async -> T + nonisolated(unsafe) var getValue: (Context) -> T + nonisolated(unsafe) var refresh: (CurrentContext) async -> T var key: UniqueKey { UniqueKey() @@ -96,8 +96,8 @@ struct TestCustomRefreshableAtom: ValueAtom, Refreshable { } struct TestCustomResettableAtom: StateAtom, Resettable { - var defaultValue: (Context) -> T - var reset: (CurrentContext) -> Void + nonisolated(unsafe) var defaultValue: (Context) -> T + nonisolated(unsafe) var reset: (CurrentContext) -> Void var key: UniqueKey { UniqueKey() @@ -114,7 +114,7 @@ struct TestCustomResettableAtom: StateAtom, Resettable { struct TestPublisherAtom: PublisherAtom where Publisher.Output: Sendable { var effect: TestEffect? - var makePublisher: () -> Publisher + nonisolated(unsafe) var makePublisher: () -> Publisher var key: UniqueKey { UniqueKey() @@ -131,7 +131,7 @@ struct TestPublisherAtom: PublisherAtom where Publ struct TestAsyncSequenceAtom: AsyncSequenceAtom where Sequence.Element: Sendable { var effect: TestEffect? - var makeSequence: () -> Sequence + nonisolated(unsafe) var makeSequence: () -> Sequence var key: UniqueKey { UniqueKey() diff --git a/Tests/AtomsTests/Utilities/Utilities.swift b/Tests/AtomsTests/Utilities/Utilities.swift index 99c8f674..87430392 100644 --- a/Tests/AtomsTests/Utilities/Utilities.swift +++ b/Tests/AtomsTests/Utilities/Utilities.swift @@ -33,7 +33,7 @@ final class TestObservableObject: ObservableObject, @unchecked Sendable { } } -final class AsyncThrowingStreamPipe { +final class AsyncThrowingStreamPipe: @unchecked Sendable { private(set) var stream: AsyncThrowingStream private(set) var continuation: AsyncThrowingStream.Continuation diff --git a/Tools/Package.resolved b/Tools/Package.resolved index 7b141b4d..b4035338 100644 --- a/Tools/Package.resolved +++ b/Tools/Package.resolved @@ -9,15 +9,6 @@ "version" : "4.6.1" } }, - { - "identity" : "graphviz", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SwiftDocOrg/GraphViz.git", - "state" : { - "revision" : "70bebcf4597b9ce33e19816d6bbd4ba9b7bdf038", - "version" : "0.2.0" - } - }, { "identity" : "jsonutilities", "kind" : "remoteSourceControl", @@ -77,14 +68,14 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-docc-plugin", "state" : { - "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", - "version" : "1.3.0" + "revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", + "version" : "1.4.3" } }, { "identity" : "swift-docc-symbolkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-symbolkit", + "location" : "https://github.com/swiftlang/swift-docc-symbolkit", "state" : { "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", "version" : "1.0.0" @@ -95,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-format.git", "state" : { - "revision" : "83248b4fa37919f78ffbd4650946759bcc54c2b5", - "version" : "509.0.0" + "revision" : "65f9da9aad84adb7e2028eb32ca95164aa590e3b", + "version" : "600.0.0" } }, { @@ -111,10 +102,10 @@ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" + "revision" : "cb53fa1bd3219b0b23ded7dfdd3b2baff266fd25", + "version" : "600.0.0" } }, { @@ -140,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/yonaskolb/XcodeGen.git", "state" : { - "revision" : "9816466703aede482c7436fddc6535684a7a9168", - "version" : "2.40.1" + "revision" : "82c6ab9bbd5b6075fc0887d897733fc0c4ffc9ab", + "version" : "2.42.0" } }, { @@ -149,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tuist/XcodeProj.git", "state" : { - "revision" : "6e60fb55271c80f83a186c9b1b4982fd991cfc0a", - "version" : "8.13.0" + "revision" : "447c159b0c5fb047a024fd8d942d4a76cf47dde0", + "version" : "8.16.0" } }, { diff --git a/Tools/Package.swift b/Tools/Package.swift index dfb0499f..eedeaf64 100644 --- a/Tools/Package.swift +++ b/Tools/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// 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-docc-plugin", exact: "1.3.0"), - .package(url: "https://github.com/apple/swift-format.git", exact: "509.0.0"), - .package(url: "https://github.com/yonaskolb/XcodeGen.git", exact: "2.40.1"), + .package(url: "https://github.com/apple/swift-docc-plugin", exact: "1.4.3"), + .package(url: "https://github.com/apple/swift-format.git", exact: "600.0.0"), + .package(url: "https://github.com/yonaskolb/XcodeGen.git", exact: "2.42.0"), ] ) diff --git a/scripts/test.sh b/scripts/test.sh index 64af5a58..151f91c1 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -11,7 +11,7 @@ pushd "$(cd $(dirname $0)/.. && pwd)" &>/dev/null case $PLATFORM in ios) - platform="iOS Simulator,name=iPhone 15 Pro" + platform="iOS Simulator,name=iPhone 16 Pro" ;; macos) platform="macOS"