diff --git a/Examples/CaseStudies/UIKit/WiFiFeature/Network.swift b/Examples/CaseStudies/UIKit/WiFiFeature/Network.swift index ea4e0332f..d54998a03 100644 --- a/Examples/CaseStudies/UIKit/WiFiFeature/Network.swift +++ b/Examples/CaseStudies/UIKit/WiFiFeature/Network.swift @@ -1,6 +1,6 @@ import Foundation -struct Network: Identifiable, Hashable { +nonisolated struct Network: Identifiable, Hashable { let id = UUID() var name = "" var isSecured = true diff --git a/Examples/CaseStudies/UIKit/WiFiFeature/WiFiSettingsFeature.swift b/Examples/CaseStudies/UIKit/WiFiFeature/WiFiSettingsFeature.swift index d489bed34..e96a787ef 100644 --- a/Examples/CaseStudies/UIKit/WiFiFeature/WiFiSettingsFeature.swift +++ b/Examples/CaseStudies/UIKit/WiFiFeature/WiFiSettingsFeature.swift @@ -231,14 +231,14 @@ class WiFiSettingsViewController: UICollectionViewController, UIKitCaseStudy { model.networkTapped(network) } - enum Section: Hashable, Sendable { + nonisolated enum Section: Hashable, Sendable { case top case foundNetworks } @CasePathable @dynamicMemberLookup - enum Item: Hashable, Sendable { + nonisolated enum Item: Hashable, Sendable { case isOn case selectedNetwork(Network.ID) case foundNetwork(Network) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index ac05f0d99..e9e6b8f53 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -629,6 +629,7 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 6.0; @@ -685,6 +686,7 @@ MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 6.0; diff --git a/Package.resolved b/Package.resolved index 7ae7ea869..55dc7d870 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "fdc10853c1416eddaab5f368180b2890aa34ef46ff72e4ae1e58d0d63371abe5", + "originHash" : "9ebcf3ff97d788d17988f076334c9a15d1422adafc92297e23ddffb782a91c5f", "pins" : [ { "identity" : "swift-case-paths", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "1197e80bc7e4b177051b6869ef93d8ac3ad677da", - "version" : "1.8.0" + "branch" : "26", + "revision" : "b636146bc9a96cc6897c6cb7e2527a3a66594005" } }, { diff --git a/Package.swift b/Package.swift index ef166b0a4..3f02fcbf9 100644 --- a/Package.swift +++ b/Package.swift @@ -58,7 +58,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.8.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", branch: "26"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"), .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.6.0"), .package(url: "https://github.com/pointfreeco/swift-perception", "1.3.4"..<"3.0.0"), diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 32deaabcc..3d38fbc92 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -31,7 +31,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.8.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", branch: "26"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"), .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.6.0"), .package(url: "https://github.com/pointfreeco/swift-perception", "1.3.4"..<"3.0.0"), diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index ba47d8588..4d6558196 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -58,7 +58,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.8.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", branch: "26"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"), .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.6.0"), .package(url: "https://github.com/pointfreeco/swift-perception", "1.3.4"..<"3.0.0"), diff --git a/Sources/SwiftNavigation/ButtonState.swift b/Sources/SwiftNavigation/ButtonState.swift index e8220bb57..440aa502a 100644 --- a/Sources/SwiftNavigation/ButtonState.swift +++ b/Sources/SwiftNavigation/ButtonState.swift @@ -86,25 +86,23 @@ public struct ButtonState: Identifiable { await perform(action) #if canImport(SwiftUI) case .animatedSend(let action, _): - if let action { - var output = "" - #if CustomDump - customDump(action, to: &output, indent: 4) - #else - output.append(" \(String(describing: action))") - #endif - reportIssue( - """ - An animated action was performed asynchronously: … + var output = "" + #if CustomDump + customDump(self.action, to: &output, indent: 4) + #else + output.append(" \(String(describing: self.action))") + #endif + reportIssue( + """ + An animated action was performed asynchronously: … - Action: - \((output)) + Action: + \((output)) - Asynchronous actions cannot be animated. Evaluate this action in a synchronous \ - closure, or use 'SwiftUI.withAnimation' explicitly. - """ - ) - } + Asynchronous actions cannot be animated. Evaluate this action in a synchronous \ + closure, or use 'SwiftUI.withAnimation' explicitly. + """ + ) await perform(action) #endif } diff --git a/Sources/SwiftNavigation/CaseBindable.swift b/Sources/SwiftNavigation/CaseBindable.swift index c6208304f..089c0ad4e 100644 --- a/Sources/SwiftNavigation/CaseBindable.swift +++ b/Sources/SwiftNavigation/CaseBindable.swift @@ -30,6 +30,7 @@ member, names: named(AllCasePaths), named(allCasePaths), + named(caseName), named(_$Element), named(UIBindingEnumeration), named(BindingEnumeration), diff --git a/Sources/SwiftNavigation/Internal/KeyPath+Sendable.swift b/Sources/SwiftNavigation/Internal/KeyPath+Sendable.swift deleted file mode 100644 index 78e278eae..000000000 --- a/Sources/SwiftNavigation/Internal/KeyPath+Sendable.swift +++ /dev/null @@ -1,35 +0,0 @@ -#if compiler(>=6) - public typealias _SendableKeyPath = any KeyPath & Sendable - public typealias _SendableWritableKeyPath = any WritableKeyPath - & Sendable -#else - public typealias _SendableKeyPath = KeyPath - public typealias _SendableWritableKeyPath = WritableKeyPath -#endif - -// NB: Dynamic member lookup does not currently support sendable key paths and even breaks -// autocomplete. -// -// * https://github.com/swiftlang/swift/issues/77035 -// * https://github.com/swiftlang/swift/issues/77105 -extension _AppendKeyPath { - @_transparent - func unsafeSendable() -> _SendableKeyPath - where Self == KeyPath { - #if compiler(>=6) - unsafeBitCast(self, to: _SendableKeyPath.self) - #else - self - #endif - } - - @_transparent - func unsafeSendable() -> _SendableWritableKeyPath - where Self == WritableKeyPath { - #if compiler(>=6) - unsafeBitCast(self, to: _SendableWritableKeyPath.self) - #else - self - #endif - } -} diff --git a/Sources/SwiftNavigation/Traits/CasePaths.swift b/Sources/SwiftNavigation/Traits/CasePaths.swift index 5970c7394..53256d3cb 100644 --- a/Sources/SwiftNavigation/Traits/CasePaths.swift +++ b/Sources/SwiftNavigation/Traits/CasePaths.swift @@ -13,7 +13,7 @@ where Value: CasePathable { func open(_ location: some _UIBinding) -> UIBinding { UIBinding( - location: _UIBindingEnumToOptionalCase(base: location, keyPath: keyPath.unsafeSendable()), + location: _UIBindingEnumToOptionalCase(base: location, keyPath: keyPath), transaction: transaction ) } @@ -30,7 +30,7 @@ where Value == V? { func open(_ location: some _UIBinding) -> UIBinding { UIBinding( - location: _UIBindingOptionalEnumToCase(base: location, keyPath: keyPath.unsafeSendable()), + location: _UIBindingOptionalEnumToCase(base: location, keyPath: keyPath), transaction: transaction ) } @@ -52,10 +52,10 @@ private final class _UIBindingEnumToOptionalCase: _UIBinding where Base.Value: CasePathable { let base: Base - let keyPath: _SendableKeyPath> + let keyPath: KeyPath> let casePath: AnyCasePath init( - base: Base, keyPath: _SendableKeyPath> + base: Base, keyPath: KeyPath> ) { self.base = base self.keyPath = keyPath @@ -84,9 +84,9 @@ Base: _UIBinding, Enum: CasePathable, Case >: _UIBinding { let base: Base - let keyPath: _SendableKeyPath> + let keyPath: KeyPath> let casePath: AnyCasePath - init(base: Base, keyPath: _SendableKeyPath>) { + init(base: Base, keyPath: KeyPath>) { self.base = base self.keyPath = keyPath self.casePath = Enum.allCasePaths[keyPath: keyPath] diff --git a/Sources/SwiftNavigation/UIBinding.swift b/Sources/SwiftNavigation/UIBinding.swift index 1389110fb..07ba345c1 100644 --- a/Sources/SwiftNavigation/UIBinding.swift +++ b/Sources/SwiftNavigation/UIBinding.swift @@ -77,7 +77,7 @@ /// When `PlayerViewController` initializes `PlayButton`, it passes a binding of its `isPlaying` /// state along. Applying the `$` prefix to a property wrapped value returns its ``projectedValue``, /// which returns a binding to the value. Whenever the user taps the `PlayButton`, the -/// `PlayerViewController` updates its `isPlaying` state. +/// `PlayerViewController` updates its `isPlaying` state. /// /// > Note: To create bindings to properties of a type that conforms to the `Observable` or /// > `Perceptible` protocols, use the [`@UIBindable`]() property wrapper. @@ -131,7 +131,7 @@ /// ``` @dynamicMemberLookup @propertyWrapper -public struct UIBinding: Sendable { +public struct UIBinding { let location: any _UIBinding /// The binding's transaction. @@ -403,7 +403,7 @@ public struct UIBinding: Sendable { ) -> UIBinding { func open(_ location: some _UIBinding) -> UIBinding { UIBinding( - location: _UIBindingAppendKeyPath(base: location, keyPath: keyPath.unsafeSendable()), + location: _UIBindingAppendKeyPath(base: location, keyPath: keyPath), transaction: transaction ) } @@ -420,7 +420,7 @@ public struct UIBinding: Sendable { where Value == Wrapped? { func open(_ location: some _UIBinding) -> UIBinding { UIBinding( - location: _UIBindingOptionalToMember(base: location, keyPath: keyPath.unsafeSendable()), + location: _UIBindingOptionalToMember(base: location, keyPath: keyPath), transaction: transaction ) } @@ -463,24 +463,26 @@ extension UIBinding: Identifiable where Value: Identifiable { } } +extension UIBinding: @unchecked Sendable where Value: Sendable {} + /// A unique identifier for a binding. -public struct UIBindingIdentifier: Hashable, Sendable { - private let location: AnyHashableSendable +public struct UIBindingIdentifier: Hashable { + private let location: AnyHashable /// Creates an instance that uniquely identifies the given binding. /// /// - Parameter binding: An instance of a binding. public init(_ binding: UIBinding) { - self.location = AnyHashableSendable(binding.location) + self.location = AnyHashable(binding.location) } } -protocol _UIBinding: AnyObject, Hashable, Sendable { +protocol _UIBinding: AnyObject, Hashable { associatedtype Value var wrappedValue: Value { get set } } -private final class _UIBindingStrongRoot: _UIBinding, @unchecked Sendable { +private final class _UIBindingStrongRoot: _UIBinding { init(root: Root) { self.wrappedValue = root } @@ -594,10 +596,10 @@ private final class _UIBindingConstant: _UIBinding, @unchecked Sendable { } } -private final class _UIBindingAppendKeyPath: _UIBinding, Sendable { +private final class _UIBindingAppendKeyPath: _UIBinding { let base: Base - let keyPath: _SendableWritableKeyPath - init(base: Base, keyPath: _SendableWritableKeyPath) { + let keyPath: WritableKeyPath + init(base: Base, keyPath: WritableKeyPath) { self.base = base self.keyPath = keyPath } @@ -614,9 +616,7 @@ private final class _UIBindingAppendKeyPath: _UIBinding } } -private final class _UIBindingFromOptional, Value>: _UIBinding, - @unchecked Sendable -{ +private final class _UIBindingFromOptional, Value>: _UIBinding { var value: Value let base: Base init(initialValue: Value, base: Base) { @@ -740,8 +740,8 @@ private final class _UIBindingOptionalToMember< Base: _UIBinding, Wrapped, Value >: _UIBinding { let base: Base - let keyPath: _SendableWritableKeyPath - init(base: Base, keyPath: _SendableWritableKeyPath) { + let keyPath: WritableKeyPath + init(base: Base, keyPath: WritableKeyPath) { self.base = base self.keyPath = keyPath } diff --git a/Sources/SwiftNavigationMacros/CaseBindableMacro.swift b/Sources/SwiftNavigationMacros/CaseBindableMacro.swift index 80b8c15bb..3244f8c78 100644 --- a/Sources/SwiftNavigationMacros/CaseBindableMacro.swift +++ b/Sources/SwiftNavigationMacros/CaseBindableMacro.swift @@ -17,31 +17,13 @@ conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext ) throws -> [ExtensionDeclSyntax] { - guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { - return [] - } - - var extensions = try CasePathableMacro.expansion( + try CasePathableMacro.expansion( of: node, attachedTo: declaration, providingExtensionsOf: type, conformingTo: protocols, in: context ) - - let conformsToCaseBindable = - enumDecl.inheritanceClause?.inheritedTypes.contains { - ["CaseBindable", "\(moduleName).CaseBindable"].contains($0.type.trimmedDescription) - } ?? false - if !conformsToCaseBindable { - let caseBindableExtension: DeclSyntax = """ - \(declaration.attributes.availability)extension \(type.trimmed): \ - \(raw: moduleName).CaseBindable {} - """ - extensions.append(caseBindableExtension.cast(ExtensionDeclSyntax.self)) - } - - return extensions } } diff --git a/Sources/UIKitNavigation/Bindings/UIControl.swift b/Sources/UIKitNavigation/Bindings/UIControl.swift index a72d581ec..c3cfa9574 100644 --- a/Sources/UIKitNavigation/Bindings/UIControl.swift +++ b/Sources/UIKitNavigation/Bindings/UIControl.swift @@ -71,11 +71,12 @@ ) } // NB: This key path must only be accessed on the main actor - nonisolated(unsafe) let uncheckedKeyPath = keyPath + nonisolated(unsafe) let keyPath = keyPath + nonisolated(unsafe) let binding = binding let observation = observe(keyPath) { control, _ in guard isSetting.withLock({ !$0 }) else { return } MainActor._assumeIsolated { - binding.wrappedValue = control[keyPath: uncheckedKeyPath] + binding.wrappedValue = control[keyPath: keyPath] } } let observeToken = ObserveToken { [weak self] in diff --git a/SwiftNavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftNavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6a95c0f11..a0abacdf8 100644 --- a/SwiftNavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftNavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "6ae399cd9f48f7a1439d7ea7b8ba4803830f408f9917ef1138f51c4f48f58fa2", + "originHash" : "c7e472a7703d0476cbd46ccbf143a43ca81543bf01ba44bb151c7226e7ea9713", "pins" : [ { "identity" : "combine-schedulers", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "1197e80bc7e4b177051b6869ef93d8ac3ad677da", - "version" : "1.8.0" + "branch" : "26", + "revision" : "b636146bc9a96cc6897c6cb7e2527a3a66594005" } }, { diff --git a/Tests/SwiftNavigationMacrosTests/CaseBindableMacroTests.swift b/Tests/SwiftNavigationMacrosTests/CaseBindableMacroTests.swift index be41c20d9..9f7c46606 100644 --- a/Tests/SwiftNavigationMacrosTests/CaseBindableMacroTests.swift +++ b/Tests/SwiftNavigationMacrosTests/CaseBindableMacroTests.swift @@ -34,7 +34,7 @@ case onSale(price: Int, discount: Int) case discontinued - public struct AllCasePaths: CasePaths.CasePathReflectable, Swift.Sendable, Swift.Sequence { + public nonisolated struct AllCasePaths: CasePaths.CasePathReflectable, Swift.Sendable, Swift.Sequence { public subscript(root: Status) -> CasePaths.PartialCaseKeyPath { if root.is(\.inStock) { return \.inStock @@ -51,7 +51,7 @@ return \.never } public var inStock: CasePaths.AnyCasePath { - ._$embed(Status.inStock) { + CasePaths.AnyCasePath(embed: Status.inStock) { guard case let .inStock(v0) = $0 else { return nil } @@ -59,7 +59,7 @@ } } public var outOfStock: CasePaths.AnyCasePath { - ._$embed(Status.outOfStock) { + CasePaths.AnyCasePath(embed: Status.outOfStock) { guard case let .outOfStock(v0) = $0 else { return nil } @@ -67,7 +67,7 @@ } } public var onSale: CasePaths.AnyCasePath { - ._$embed(Status.onSale) { + CasePaths.AnyCasePath(embed: Status.onSale) { guard case let .onSale(v0, v1) = $0 else { return nil } @@ -75,7 +75,7 @@ } } public var discontinued: CasePaths.AnyCasePath { - ._$embed({ + CasePaths.AnyCasePath(embed: { Status.discontinued }) { guard case .discontinued = $0 else { @@ -94,10 +94,28 @@ } } - public static var allCasePaths: AllCasePaths { + public nonisolated static var allCasePaths: AllCasePaths { AllCasePaths() } + public nonisolated static func caseName( + for keyPath: CasePaths.PartialCaseKeyPath + ) -> Swift.String? { + if keyPath == \.inStock { + return "inStock" + } + if keyPath == \.outOfStock { + return "outOfStock" + } + if keyPath == \.onSale { + return "onSale" + } + if keyPath == \.discontinued { + return "discontinued" + } + return nil + } + public enum UIBindingEnumeration { case inStock(SwiftNavigation.UIBinding) case outOfStock(SwiftNavigation.UIBinding) @@ -143,12 +161,6 @@ } #endif } - - extension Status: CasePaths.CasePathable, CasePaths.CasePathIterable { - } - - extension Status: SwiftNavigation.CaseBindable { - } """# } } @@ -169,7 +181,7 @@ case inStock(quantity: Int) case discontinued - public struct AllCasePaths: CasePaths.CasePathReflectable, Swift.Sendable, Swift.Sequence { + public nonisolated struct AllCasePaths: CasePaths.CasePathReflectable, Swift.Sendable, Swift.Sequence { public subscript(root: Status) -> CasePaths.PartialCaseKeyPath { if root.is(\.inStock) { return \.inStock @@ -180,7 +192,7 @@ return \.never } public var inStock: CasePaths.AnyCasePath { - ._$embed(Status.inStock) { + CasePaths.AnyCasePath(embed: Status.inStock) { guard case let .inStock(v0) = $0 else { return nil } @@ -188,7 +200,7 @@ } } public var discontinued: CasePaths.AnyCasePath { - ._$embed({ + CasePaths.AnyCasePath(embed: { Status.discontinued }) { guard case .discontinued = $0 else { @@ -205,10 +217,22 @@ } } - public static var allCasePaths: AllCasePaths { + public nonisolated static var allCasePaths: AllCasePaths { AllCasePaths() } + public nonisolated static func caseName( + for keyPath: CasePaths.PartialCaseKeyPath + ) -> Swift.String? { + if keyPath == \.inStock { + return "inStock" + } + if keyPath == \.discontinued { + return "discontinued" + } + return nil + } + public enum UIBindingEnumeration { case inStock(SwiftNavigation.UIBinding) case discontinued @@ -242,12 +266,6 @@ } #endif } - - extension Status: CasePaths.CasePathable, CasePaths.CasePathIterable { - } - - extension Status: SwiftNavigation.CaseBindable { - } """# } }