From e4bfea1aaa3ef8d5d34e37921bc1940ece22e650 Mon Sep 17 00:00:00 2001 From: Oliver Atkinson Date: Tue, 30 Apr 2024 09:24:32 +0100 Subject: [PATCH 01/12] revert perception checking iOS 17.0.1 and add a boolean to allow consumers to force perception checking --- Sources/Perception/Macros.swift | 18 +++++------ Sources/Perception/Perceptible.swift | 6 ++-- Sources/Perception/PerceptionChecking.swift | 18 +++++++++++ Sources/Perception/PerceptionRegistrar.swift | 32 +++++++++---------- Sources/Perception/PerceptionTracking.swift | 8 ++--- .../Perception/WithPerceptionTracking.swift | 14 ++++---- .../PerceptionMacros/PerceptibleMacro.swift | 2 +- 7 files changed, 58 insertions(+), 40 deletions(-) diff --git a/Sources/Perception/Macros.swift b/Sources/Perception/Macros.swift index f7df1a0..7e80e55 100644 --- a/Sources/Perception/Macros.swift +++ b/Sources/Perception/Macros.swift @@ -12,10 +12,10 @@ #if canImport(Observation) import Observation - @available(iOS, deprecated: 17.0.1, renamed: "Observable") + @available(iOS, deprecated: 17, renamed: "Observable") @available(macOS, deprecated: 14, renamed: "Observable") - @available(tvOS, deprecated: 17.0.1, renamed: "Observable") - @available(watchOS, deprecated: 10.0.1, renamed: "Observable") + @available(tvOS, deprecated: 17, renamed: "Observable") + @available(watchOS, deprecated: 10, renamed: "Observable") @attached( member, names: named(_$id), named(_$perceptionRegistrar), named(access), named(withMutation)) @attached(memberAttribute) @@ -23,19 +23,19 @@ public macro Perceptible() = #externalMacro(module: "PerceptionMacros", type: "PerceptibleMacro") - @available(iOS, deprecated: 17.0.1, renamed: "ObservationTracked") + @available(iOS, deprecated: 17, renamed: "ObservationTracked") @available(macOS, deprecated: 14, renamed: "ObservationTracked") - @available(tvOS, deprecated: 17.0.1, renamed: "ObservationTracked") - @available(watchOS, deprecated: 10.0.1, renamed: "ObservationTracked") + @available(tvOS, deprecated: 17, renamed: "ObservationTracked") + @available(watchOS, deprecated: 10, renamed: "ObservationTracked") @attached(accessor, names: named(init), named(get), named(set)) @attached(peer, names: prefixed(_)) public macro PerceptionTracked() = #externalMacro(module: "PerceptionMacros", type: "PerceptionTrackedMacro") - @available(iOS, deprecated: 17.0.1, renamed: "ObservationIgnored") + @available(iOS, deprecated: 17, renamed: "ObservationIgnored") @available(macOS, deprecated: 14, renamed: "ObservationIgnored") - @available(tvOS, deprecated: 17.0.1, renamed: "ObservationIgnored") - @available(watchOS, deprecated: 10.0.1, renamed: "ObservationIgnored") + @available(tvOS, deprecated: 17, renamed: "ObservationIgnored") + @available(watchOS, deprecated: 10, renamed: "ObservationIgnored") @attached(accessor, names: named(willSet)) public macro PerceptionIgnored() = #externalMacro(module: "PerceptionMacros", type: "PerceptionIgnoredMacro") diff --git a/Sources/Perception/Perceptible.swift b/Sources/Perception/Perceptible.swift index c1bfb28..5bb5f2a 100644 --- a/Sources/Perception/Perceptible.swift +++ b/Sources/Perception/Perceptible.swift @@ -16,8 +16,8 @@ /// type doesn't add observation functionality to the type. Instead, always use /// the ``Perception/Perceptible()`` macro when adding observation /// support to a type. -@available(iOS, deprecated: 17.0.1, renamed: "Observable") +@available(iOS, deprecated: 17, renamed: "Observable") @available(macOS, deprecated: 14, renamed: "Observable") -@available(tvOS, deprecated: 17.0.1, renamed: "Observable") -@available(watchOS, deprecated: 10.0.1, renamed: "Observable") +@available(tvOS, deprecated: 17, renamed: "Observable") +@available(watchOS, deprecated: 10, renamed: "Observable") public protocol Perceptible {} diff --git a/Sources/Perception/PerceptionChecking.swift b/Sources/Perception/PerceptionChecking.swift index 32776ac..151043f 100644 --- a/Sources/Perception/PerceptionChecking.swift +++ b/Sources/Perception/PerceptionChecking.swift @@ -5,6 +5,11 @@ public var isPerceptionCheckingEnabled: Bool { set { perceptionChecking.isPerceptionCheckingEnabled = newValue } } +public var forcePerceptionChecking: Bool { + get { perceptionChecking.isPerceptionCheckingEnabled } + set { perceptionChecking.isPerceptionCheckingEnabled = newValue } +} + private let perceptionChecking = PerceptionChecking() private class PerceptionChecking: @unchecked Sendable { @@ -20,7 +25,20 @@ private class PerceptionChecking: @unchecked Sendable { _isPerceptionCheckingEnabled = newValue } } + var forcePerceptionChecking: Bool { + get { + lock.lock() + defer { lock.unlock() } + return _forcePerceptionChecking + } + set { + lock.lock() + defer { lock.unlock() } + _forcePerceptionChecking = newValue + } + } let lock = NSLock() + var _forcePerceptionChecking = false #if DEBUG var _isPerceptionCheckingEnabled = true #else diff --git a/Sources/Perception/PerceptionRegistrar.swift b/Sources/Perception/PerceptionRegistrar.swift index 2194433..0182628 100644 --- a/Sources/Perception/PerceptionRegistrar.swift +++ b/Sources/Perception/PerceptionRegistrar.swift @@ -9,10 +9,10 @@ import SwiftUI /// /// You don't need to create an instance of `PerceptionRegistrar` when using /// the ``Perception/Perceptible()`` macro to indicate observability of a type. -@available(iOS, deprecated: 17.0.1, renamed: "ObservationRegistrar") +@available(iOS, deprecated: 17, renamed: "ObservationRegistrar") @available(macOS, deprecated: 14, renamed: "ObservationRegistrar") -@available(tvOS, deprecated: 17.0.1, renamed: "ObservationRegistrar") -@available(watchOS, deprecated: 10.0.1, renamed: "ObservationRegistrar") +@available(tvOS, deprecated: 17, renamed: "ObservationRegistrar") +@available(watchOS, deprecated: 10, renamed: "ObservationRegistrar") public struct PerceptionRegistrar: Sendable { private let _rawValue: AnySendable #if DEBUG @@ -27,7 +27,7 @@ public struct PerceptionRegistrar: Sendable { /// ``Perception/Perceptible()`` macro to indicate observably /// of a type. public init(isPerceptionCheckingEnabled: Bool = Perception.isPerceptionCheckingEnabled) { - if #available(iOS 17.0.1, macOS 14, tvOS 17.0.1, watchOS 10.0.1, *) { + if !forcePerceptionChecking, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { #if canImport(Observation) self._rawValue = AnySendable(ObservationRegistrar()) #else @@ -42,7 +42,7 @@ public struct PerceptionRegistrar: Sendable { } #if canImport(Observation) - @available(iOS 17.0.1, macOS 14, tvOS 17.0.1, watchOS 10.0.1, *) + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) private var registrar: ObservationRegistrar { self._rawValue.base as! ObservationRegistrar } @@ -54,7 +54,7 @@ public struct PerceptionRegistrar: Sendable { } #if canImport(Observation) - @available(iOS 17.0.1, macOS 14, tvOS 17.0.1, watchOS 10.0.1, *) + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension PerceptionRegistrar { public func access( _ subject: Subject, keyPath: KeyPath @@ -94,7 +94,7 @@ extension PerceptionRegistrar { self.perceptionCheck(file: file, line: line) #endif #if canImport(Observation) - if #available(iOS 17.0.1, macOS 14, tvOS 17.0.1, watchOS 10.0.1, *) { + if !forcePerceptionChecking, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { func `open`(_ subject: T) { self.registrar.access( subject, @@ -117,7 +117,7 @@ extension PerceptionRegistrar { _ mutation: () throws -> T ) rethrows -> T { #if canImport(Observation) - if #available(iOS 17.0.1, macOS 14, tvOS 17.0.1, watchOS 10.0.1, *), + if !forcePerceptionChecking, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), let subject = subject as? any Observable { func `open`(_ subject: S) throws -> T { @@ -142,7 +142,7 @@ extension PerceptionRegistrar { keyPath: KeyPath ) { #if canImport(Observation) - if #available(iOS 17.0.1, macOS 14, tvOS 17.0.1, watchOS 10.0.1, *), + if !forcePerceptionChecking, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), let subject = subject as? any Observable { func `open`(_ subject: S) { @@ -164,7 +164,7 @@ extension PerceptionRegistrar { keyPath: KeyPath ) { #if canImport(Observation) - if #available(iOS 17.0.1, macOS 14, tvOS 17.0.1, watchOS 10.0.1, *), + if !forcePerceptionChecking, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), let subject = subject as? any Observable { func `open`(_ subject: S) { @@ -315,10 +315,10 @@ extension PerceptionRegistrar: Hashable { #endif #if DEBUG - @available(iOS, deprecated: 17.0.1) + @available(iOS, deprecated: 17) @available(macOS, deprecated: 14) - @available(tvOS, deprecated: 17.0.1) - @available(watchOS, deprecated: 10.0.1) + @available(tvOS, deprecated: 17) + @available(watchOS, deprecated: 10) public func _withoutPerceptionChecking( _ apply: () -> T ) -> T { @@ -327,10 +327,10 @@ extension PerceptionRegistrar: Hashable { } } #else - @available(iOS, deprecated: 17.0.1) + @available(iOS, deprecated: 17) @available(macOS, deprecated: 14) - @available(tvOS, deprecated: 17.0.1) - @available(watchOS, deprecated: 10.0.1) + @available(tvOS, deprecated: 17) + @available(watchOS, deprecated: 10) @_transparent @inline(__always) public func _withoutPerceptionChecking( diff --git a/Sources/Perception/PerceptionTracking.swift b/Sources/Perception/PerceptionTracking.swift index db0df01..2147d91 100644 --- a/Sources/Perception/PerceptionTracking.swift +++ b/Sources/Perception/PerceptionTracking.swift @@ -209,16 +209,16 @@ private func generateAccessList(_ apply: () -> T) -> (T, PerceptionTracking._ /// /// - Returns: The value that the `apply` closure returns if it has a return /// value; otherwise, there is no return value. -@available(iOS, deprecated: 17.0.1, renamed: "withObservationTracking") +@available(iOS, deprecated: 17, renamed: "withObservationTracking") @available(macOS, deprecated: 14, renamed: "withObservationTracking") -@available(tvOS, deprecated: 17.0.1, renamed: "withObservationTracking") -@available(watchOS, deprecated: 10.0.1, renamed: "withObservationTracking") +@available(tvOS, deprecated: 17, renamed: "withObservationTracking") +@available(watchOS, deprecated: 10, renamed: "withObservationTracking") public func withPerceptionTracking( _ apply: () -> T, onChange: @autoclosure () -> @Sendable () -> Void ) -> T { #if canImport(Observation) - if #available(iOS 17.0.1, macOS 14, tvOS 17.0.1, watchOS 10.0.1, *) { + if !forcePerceptionChecking, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { return withObservationTracking(apply, onChange: onChange()) } #endif diff --git a/Sources/Perception/WithPerceptionTracking.swift b/Sources/Perception/WithPerceptionTracking.swift index d2e4008..29d6e32 100644 --- a/Sources/Perception/WithPerceptionTracking.swift +++ b/Sources/Perception/WithPerceptionTracking.swift @@ -38,16 +38,16 @@ import SwiftUI /// To debug this, expand the warning in the Issue Navigator of Xcode (cmd+5), and click through the /// stack frames displayed to find the line in your view where you are accessing state without being /// inside ``WithPerceptionTracking``. -@available(iOS, deprecated: 17.0.1, message: "Remove WithPerceptionTracking") +@available(iOS, deprecated: 17, message: "Remove WithPerceptionTracking") @available(macOS, deprecated: 14, message: "Remove WithPerceptionTracking") -@available(tvOS, deprecated: 17.0.1, message: "Remove WithPerceptionTracking") -@available(watchOS, deprecated: 10.0.1, message: "Remove WithPerceptionTracking") +@available(tvOS, deprecated: 17, message: "Remove WithPerceptionTracking") +@available(watchOS, deprecated: 10, message: "Remove WithPerceptionTracking") public struct WithPerceptionTracking { @State var id = 0 let content: () -> Content public var body: Content { - if #available(iOS 17.0.1, macOS 14, tvOS 17.0.1, watchOS 10.0.1, *) { + if !forcePerceptionChecking, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { return self.instrumentedBody() } else { // NB: View will not re-render when 'id' changes unless we access it in the view. @@ -156,10 +156,10 @@ extension WithPerceptionTracking: View where Content: View { } } -@available(iOS, deprecated: 17.0.1) +@available(iOS, deprecated: 17) @available(macOS, deprecated: 14) -@available(tvOS, deprecated: 17.0.1) -@available(watchOS, deprecated: 10.0.1) +@available(tvOS, deprecated: 17) +@available(watchOS, deprecated: 10) public enum _PerceptionLocals { @TaskLocal public static var isInPerceptionTracking = false @TaskLocal public static var skipPerceptionChecking = false diff --git a/Sources/PerceptionMacros/PerceptibleMacro.swift b/Sources/PerceptionMacros/PerceptibleMacro.swift index 6af2ea9..1911c75 100644 --- a/Sources/PerceptionMacros/PerceptibleMacro.swift +++ b/Sources/PerceptionMacros/PerceptibleMacro.swift @@ -302,7 +302,7 @@ extension PerceptibleMacro: ExtensionMacro { extension \(raw: type.trimmedDescription): \(raw: qualifiedConformanceName) {} """ let obsDecl: DeclSyntax = """ - @available(iOS 17.0.1, macOS 14, tvOS 17.0.1, watchOS 10.0.1, *) + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension \(raw: type.trimmedDescription): Observation.Observable {} """ let ext = decl.cast(ExtensionDeclSyntax.self) From cbe07fa87f7cd9a8e2e7b33065651859458af5fe Mon Sep 17 00:00:00 2001 From: Oliver Atkinson Date: Tue, 30 Apr 2024 09:56:06 +0100 Subject: [PATCH 02/12] Update PerceptionChecking.swift --- Sources/Perception/PerceptionChecking.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Perception/PerceptionChecking.swift b/Sources/Perception/PerceptionChecking.swift index 151043f..636ca92 100644 --- a/Sources/Perception/PerceptionChecking.swift +++ b/Sources/Perception/PerceptionChecking.swift @@ -6,8 +6,8 @@ public var isPerceptionCheckingEnabled: Bool { } public var forcePerceptionChecking: Bool { - get { perceptionChecking.isPerceptionCheckingEnabled } - set { perceptionChecking.isPerceptionCheckingEnabled = newValue } + get { perceptionChecking.forcePerceptionChecking } + set { perceptionChecking.forcePerceptionChecking = newValue } } private let perceptionChecking = PerceptionChecking() From 28a48135028795adcea5cbd759a5006df564cfa5 Mon Sep 17 00:00:00 2001 From: Oliver Atkinson Date: Tue, 30 Apr 2024 18:23:31 +0100 Subject: [PATCH 03/12] Update PerceptionRegistrar.swift --- Sources/Perception/PerceptionRegistrar.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Perception/PerceptionRegistrar.swift b/Sources/Perception/PerceptionRegistrar.swift index 5268b9f..2b78ddc 100644 --- a/Sources/Perception/PerceptionRegistrar.swift +++ b/Sources/Perception/PerceptionRegistrar.swift @@ -27,7 +27,7 @@ public struct PerceptionRegistrar: Sendable { /// ``Perception/Perceptible()`` macro to indicate observably /// of a type. public init(isPerceptionCheckingEnabled: Bool = Perception.isPerceptionCheckingEnabled) { - if !forcePerceptionChecking, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { + if !forcePerceptionChecking, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { #if canImport(Observation) self._rawValue = AnySendable(ObservationRegistrar()) #else From 3cea3b583f95648006490413f5a29e4bcb25a2a2 Mon Sep 17 00:00:00 2001 From: Oliver Atkinson Date: Wed, 1 May 2024 22:15:21 +0100 Subject: [PATCH 04/12] Remove configuration and use a runtime check --- .../Internal/_ForcePerceptionChecking.swift | 54 +++++++++++++++++++ Sources/Perception/PerceptionChecking.swift | 18 ------- Sources/Perception/PerceptionRegistrar.swift | 10 ++-- Sources/Perception/PerceptionTracking.swift | 2 +- .../Perception/WithPerceptionTracking.swift | 2 +- 5 files changed, 61 insertions(+), 25 deletions(-) create mode 100644 Sources/Perception/Internal/_ForcePerceptionChecking.swift diff --git a/Sources/Perception/Internal/_ForcePerceptionChecking.swift b/Sources/Perception/Internal/_ForcePerceptionChecking.swift new file mode 100644 index 0000000..7291b60 --- /dev/null +++ b/Sources/Perception/Internal/_ForcePerceptionChecking.swift @@ -0,0 +1,54 @@ +import Foundation +#if os(watchOS) +import WatchKit +#endif + +// The beta builds which included of Observation used the `@_marker` protocol for Observable. +// Since the API changed and the protocol no longer matches there's a runtime crash when trying +// to cast to `any Observable` on a beta build. +// +// As a result, the following check has been implemented to disable Observation on any of the +// following beta OS versions: +// +// - iOS 17.0.0 +// - watchOS 10.0.0 +// - tvOS 17.0.0 +// +// This safeguard makes sure that `Perception` is used in place of `Observation`. +// +// +----------------------------------------+---------------------+----------------------+ +// | isMinimumSupportedObservationOSVersion | isKernelVersionBeta | isObservationAllowed | +// +----------------------------------------+---------------------+----------------------+ +// | FALSE | FALSE | TRUE | +// | FALSE | TRUE | TRUE | +// | TRUE | FALSE | TRUE | +// | TRUE | TRUE | FALSE | +// +----------------------------------------+---------------------+----------------------+ +// +// ``` +// Example of a crash trace: +// 0 libswiftCore.dylib 0x39be20 tryCast(swift::OpaqueValue*, swift::TargetMetadata const*, swift::OpaqueValue*, swift::TargetMetadata const*, swift::TargetMetadata const*&, swift::TargetMetadata const*&, bool, bool) + +// ``` +var isObservationAllowed: Bool { !(isMinimumSupportedObservationOSVersion && isKernelVersionBeta) } + +private var kernelVersion: String { + var size = 0 + sysctlbyname("kern.osversion", nil, &size, nil, 0) + var version = [CChar](repeating: 0, count: size) + sysctlbyname("kern.osversion", &version, &size, nil, 0) + return String(cString: version) +} + +// isKernelVersionBeta is denoted by a lowercase character as the last character of the string. e.g. 21A5277j +private var isKernelVersionBeta: Bool { kernelVersion.last?.isLowercase == true } + +private var isMinimumSupportedObservationOSVersion: Bool { + let os = ProcessInfo.processInfo.operatingSystemVersion + #if os(iOS) || os(tvOS) + return (os.majorVersion, os.minorVersion, os.patchVersion) == (17, 0, 0) + #elseif os(watchOS) + return (os.majorVersion, os.minorVersion, os.patchVersion) == (10, 0, 0) + #elseif os(macOS) + return false + #endif +} diff --git a/Sources/Perception/PerceptionChecking.swift b/Sources/Perception/PerceptionChecking.swift index 636ca92..32776ac 100644 --- a/Sources/Perception/PerceptionChecking.swift +++ b/Sources/Perception/PerceptionChecking.swift @@ -5,11 +5,6 @@ public var isPerceptionCheckingEnabled: Bool { set { perceptionChecking.isPerceptionCheckingEnabled = newValue } } -public var forcePerceptionChecking: Bool { - get { perceptionChecking.forcePerceptionChecking } - set { perceptionChecking.forcePerceptionChecking = newValue } -} - private let perceptionChecking = PerceptionChecking() private class PerceptionChecking: @unchecked Sendable { @@ -25,20 +20,7 @@ private class PerceptionChecking: @unchecked Sendable { _isPerceptionCheckingEnabled = newValue } } - var forcePerceptionChecking: Bool { - get { - lock.lock() - defer { lock.unlock() } - return _forcePerceptionChecking - } - set { - lock.lock() - defer { lock.unlock() } - _forcePerceptionChecking = newValue - } - } let lock = NSLock() - var _forcePerceptionChecking = false #if DEBUG var _isPerceptionCheckingEnabled = true #else diff --git a/Sources/Perception/PerceptionRegistrar.swift b/Sources/Perception/PerceptionRegistrar.swift index 2b78ddc..b8f8ee8 100644 --- a/Sources/Perception/PerceptionRegistrar.swift +++ b/Sources/Perception/PerceptionRegistrar.swift @@ -27,7 +27,7 @@ public struct PerceptionRegistrar: Sendable { /// ``Perception/Perceptible()`` macro to indicate observably /// of a type. public init(isPerceptionCheckingEnabled: Bool = Perception.isPerceptionCheckingEnabled) { - if !forcePerceptionChecking, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { + if isObservationAllowed, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { #if canImport(Observation) self._rawValue = AnySendable(ObservationRegistrar()) #else @@ -94,7 +94,7 @@ extension PerceptionRegistrar { self.perceptionCheck(file: file, line: line) #endif #if canImport(Observation) - if !forcePerceptionChecking, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { + if isObservationAllowed, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { func `open`(_ subject: T) { self.registrar.access( subject, @@ -117,7 +117,7 @@ extension PerceptionRegistrar { _ mutation: () throws -> T ) rethrows -> T { #if canImport(Observation) - if !forcePerceptionChecking, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), + if isObservationAllowed, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), let subject = subject as? any Observable { func `open`(_ subject: S) throws -> T { @@ -142,7 +142,7 @@ extension PerceptionRegistrar { keyPath: KeyPath ) { #if canImport(Observation) - if !forcePerceptionChecking, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), + if isObservationAllowed, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), let subject = subject as? any Observable { func `open`(_ subject: S) { @@ -164,7 +164,7 @@ extension PerceptionRegistrar { keyPath: KeyPath ) { #if canImport(Observation) - if !forcePerceptionChecking, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), + if isObservationAllowed, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), let subject = subject as? any Observable { func `open`(_ subject: S) { diff --git a/Sources/Perception/PerceptionTracking.swift b/Sources/Perception/PerceptionTracking.swift index 2147d91..b394304 100644 --- a/Sources/Perception/PerceptionTracking.swift +++ b/Sources/Perception/PerceptionTracking.swift @@ -218,7 +218,7 @@ public func withPerceptionTracking( onChange: @autoclosure () -> @Sendable () -> Void ) -> T { #if canImport(Observation) - if !forcePerceptionChecking, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { + if isObservationAllowed, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { return withObservationTracking(apply, onChange: onChange()) } #endif diff --git a/Sources/Perception/WithPerceptionTracking.swift b/Sources/Perception/WithPerceptionTracking.swift index 29d6e32..6ae3b3d 100644 --- a/Sources/Perception/WithPerceptionTracking.swift +++ b/Sources/Perception/WithPerceptionTracking.swift @@ -47,7 +47,7 @@ public struct WithPerceptionTracking { let content: () -> Content public var body: Content { - if !forcePerceptionChecking, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { + if isObservationAllowed, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { return self.instrumentedBody() } else { // NB: View will not re-render when 'id' changes unless we access it in the view. From 6b3ac70c12b2f446c259ae94880af4c6b693ed28 Mon Sep 17 00:00:00 2001 From: Oliver Atkinson Date: Wed, 1 May 2024 22:24:34 +0100 Subject: [PATCH 05/12] Remove unused watchkit import --- Sources/Perception/Internal/_ForcePerceptionChecking.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/Perception/Internal/_ForcePerceptionChecking.swift b/Sources/Perception/Internal/_ForcePerceptionChecking.swift index 7291b60..469e248 100644 --- a/Sources/Perception/Internal/_ForcePerceptionChecking.swift +++ b/Sources/Perception/Internal/_ForcePerceptionChecking.swift @@ -1,7 +1,4 @@ import Foundation -#if os(watchOS) -import WatchKit -#endif // The beta builds which included of Observation used the `@_marker` protocol for Observable. // Since the API changed and the protocol no longer matches there's a runtime crash when trying From 6672138ce13ce758b088d44f1ae844ed424e2b76 Mon Sep 17 00:00:00 2001 From: Oliver Atkinson Date: Thu, 2 May 2024 10:08:10 +0100 Subject: [PATCH 06/12] Documentation --- Sources/Perception/Internal/_ForcePerceptionChecking.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/Perception/Internal/_ForcePerceptionChecking.swift b/Sources/Perception/Internal/_ForcePerceptionChecking.swift index 469e248..43d4e66 100644 --- a/Sources/Perception/Internal/_ForcePerceptionChecking.swift +++ b/Sources/Perception/Internal/_ForcePerceptionChecking.swift @@ -1,8 +1,9 @@ import Foundation -// The beta builds which included of Observation used the `@_marker` protocol for Observable. -// Since the API changed and the protocol no longer matches there's a runtime crash when trying -// to cast to `any Observable` on a beta build. +// Early beta versions used the `@_marker` protocol for Observable, which has minimal runtime impact. +// Changes to the protocol's memory layout in these versions disrupt dynamic casting, +// like `subject as? any Observable`. This occurs because the Swift runtime expects a specific +// protocol metadata layout for casting, and this mismatch leads to a crash. // // As a result, the following check has been implemented to disable Observation on any of the // following beta OS versions: From ec6c2171e536ec378550fdeed1fbf03b86a201b0 Mon Sep 17 00:00:00 2001 From: Oliver Atkinson Date: Thu, 2 May 2024 10:10:02 +0100 Subject: [PATCH 07/12] sp --- Sources/Perception/Internal/_ForcePerceptionChecking.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Perception/Internal/_ForcePerceptionChecking.swift b/Sources/Perception/Internal/_ForcePerceptionChecking.swift index 43d4e66..884fd1b 100644 --- a/Sources/Perception/Internal/_ForcePerceptionChecking.swift +++ b/Sources/Perception/Internal/_ForcePerceptionChecking.swift @@ -1,6 +1,6 @@ import Foundation -// Early beta versions used the `@_marker` protocol for Observable, which has minimal runtime impact. +// Early beta versions used the `@_marker` protocol for Observable. // Changes to the protocol's memory layout in these versions disrupt dynamic casting, // like `subject as? any Observable`. This occurs because the Swift runtime expects a specific // protocol metadata layout for casting, and this mismatch leads to a crash. From 9ef6429e2302688c87bf4a779ce106b2e305bf69 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 2 May 2024 11:20:22 -0700 Subject: [PATCH 08/12] cleanup --- .../Perception/Internal/BetaChecking.swift | 26 ++++++++++ .../Internal/_ForcePerceptionChecking.swift | 52 ------------------- Sources/Perception/PerceptionRegistrar.swift | 10 ++-- Sources/Perception/PerceptionTracking.swift | 2 +- .../Perception/WithPerceptionTracking.swift | 2 +- 5 files changed, 33 insertions(+), 59 deletions(-) create mode 100644 Sources/Perception/Internal/BetaChecking.swift delete mode 100644 Sources/Perception/Internal/_ForcePerceptionChecking.swift diff --git a/Sources/Perception/Internal/BetaChecking.swift b/Sources/Perception/Internal/BetaChecking.swift new file mode 100644 index 0000000..f7a4813 --- /dev/null +++ b/Sources/Perception/Internal/BetaChecking.swift @@ -0,0 +1,26 @@ +import Foundation + +// NB: This boolean is used to work around a crash experienced by beta users of Observation when +// `Observable` was still a marker protocol and we attempt to dynamically cast to +// `any Observable`. +let isObservationBeta: Bool = { + #if os(iOS) || os(tvOS) || os(watchOS) + let os = ProcessInfo.processInfo.operatingSystemVersion + #if os(iOS) || os(tvOS) + if (os.majorVersion, os.minorVersion, os.patchVersion) != (17, 0, 0) { + return false + } + #elseif os(watchOS) + if (os.majorVersion, os.minorVersion, os.patchVersion) != (10, 0, 0) { + return false + } + var size = 0 + sysctlbyname("kern.osversion", nil, &size, nil, 0) + var version = [CChar](repeating: 0, count: size) + sysctlbyname("kern.osversion", &version, &size, nil, 0) + // NB: Beta builds end with a lowercase character (_e.g._, '21A5277j') + return String(cString: version).last?.isLowercase == true + #endif + #endif + return false +}() diff --git a/Sources/Perception/Internal/_ForcePerceptionChecking.swift b/Sources/Perception/Internal/_ForcePerceptionChecking.swift deleted file mode 100644 index 884fd1b..0000000 --- a/Sources/Perception/Internal/_ForcePerceptionChecking.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Foundation - -// Early beta versions used the `@_marker` protocol for Observable. -// Changes to the protocol's memory layout in these versions disrupt dynamic casting, -// like `subject as? any Observable`. This occurs because the Swift runtime expects a specific -// protocol metadata layout for casting, and this mismatch leads to a crash. -// -// As a result, the following check has been implemented to disable Observation on any of the -// following beta OS versions: -// -// - iOS 17.0.0 -// - watchOS 10.0.0 -// - tvOS 17.0.0 -// -// This safeguard makes sure that `Perception` is used in place of `Observation`. -// -// +----------------------------------------+---------------------+----------------------+ -// | isMinimumSupportedObservationOSVersion | isKernelVersionBeta | isObservationAllowed | -// +----------------------------------------+---------------------+----------------------+ -// | FALSE | FALSE | TRUE | -// | FALSE | TRUE | TRUE | -// | TRUE | FALSE | TRUE | -// | TRUE | TRUE | FALSE | -// +----------------------------------------+---------------------+----------------------+ -// -// ``` -// Example of a crash trace: -// 0 libswiftCore.dylib 0x39be20 tryCast(swift::OpaqueValue*, swift::TargetMetadata const*, swift::OpaqueValue*, swift::TargetMetadata const*, swift::TargetMetadata const*&, swift::TargetMetadata const*&, bool, bool) + -// ``` -var isObservationAllowed: Bool { !(isMinimumSupportedObservationOSVersion && isKernelVersionBeta) } - -private var kernelVersion: String { - var size = 0 - sysctlbyname("kern.osversion", nil, &size, nil, 0) - var version = [CChar](repeating: 0, count: size) - sysctlbyname("kern.osversion", &version, &size, nil, 0) - return String(cString: version) -} - -// isKernelVersionBeta is denoted by a lowercase character as the last character of the string. e.g. 21A5277j -private var isKernelVersionBeta: Bool { kernelVersion.last?.isLowercase == true } - -private var isMinimumSupportedObservationOSVersion: Bool { - let os = ProcessInfo.processInfo.operatingSystemVersion - #if os(iOS) || os(tvOS) - return (os.majorVersion, os.minorVersion, os.patchVersion) == (17, 0, 0) - #elseif os(watchOS) - return (os.majorVersion, os.minorVersion, os.patchVersion) == (10, 0, 0) - #elseif os(macOS) - return false - #endif -} diff --git a/Sources/Perception/PerceptionRegistrar.swift b/Sources/Perception/PerceptionRegistrar.swift index b8f8ee8..ac96bc7 100644 --- a/Sources/Perception/PerceptionRegistrar.swift +++ b/Sources/Perception/PerceptionRegistrar.swift @@ -27,7 +27,7 @@ public struct PerceptionRegistrar: Sendable { /// ``Perception/Perceptible()`` macro to indicate observably /// of a type. public init(isPerceptionCheckingEnabled: Bool = Perception.isPerceptionCheckingEnabled) { - if isObservationAllowed, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { + if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), !isObservationBeta { #if canImport(Observation) self._rawValue = AnySendable(ObservationRegistrar()) #else @@ -94,7 +94,7 @@ extension PerceptionRegistrar { self.perceptionCheck(file: file, line: line) #endif #if canImport(Observation) - if isObservationAllowed, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { + if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), !isObservationBeta { func `open`(_ subject: T) { self.registrar.access( subject, @@ -117,7 +117,7 @@ extension PerceptionRegistrar { _ mutation: () throws -> T ) rethrows -> T { #if canImport(Observation) - if isObservationAllowed, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), + if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), !isObservationBeta, let subject = subject as? any Observable { func `open`(_ subject: S) throws -> T { @@ -142,7 +142,7 @@ extension PerceptionRegistrar { keyPath: KeyPath ) { #if canImport(Observation) - if isObservationAllowed, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), + if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), !isObservationBeta, let subject = subject as? any Observable { func `open`(_ subject: S) { @@ -164,7 +164,7 @@ extension PerceptionRegistrar { keyPath: KeyPath ) { #if canImport(Observation) - if isObservationAllowed, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), + if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), !isObservationBeta, let subject = subject as? any Observable { func `open`(_ subject: S) { diff --git a/Sources/Perception/PerceptionTracking.swift b/Sources/Perception/PerceptionTracking.swift index b394304..bb6fdb3 100644 --- a/Sources/Perception/PerceptionTracking.swift +++ b/Sources/Perception/PerceptionTracking.swift @@ -218,7 +218,7 @@ public func withPerceptionTracking( onChange: @autoclosure () -> @Sendable () -> Void ) -> T { #if canImport(Observation) - if isObservationAllowed, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { + if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), !isObservationBeta { return withObservationTracking(apply, onChange: onChange()) } #endif diff --git a/Sources/Perception/WithPerceptionTracking.swift b/Sources/Perception/WithPerceptionTracking.swift index 6ae3b3d..c69a0c2 100644 --- a/Sources/Perception/WithPerceptionTracking.swift +++ b/Sources/Perception/WithPerceptionTracking.swift @@ -47,7 +47,7 @@ public struct WithPerceptionTracking { let content: () -> Content public var body: Content { - if isObservationAllowed, #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { + if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), !isObservationBeta { return self.instrumentedBody() } else { // NB: View will not re-render when 'id' changes unless we access it in the view. From 2e903f4d90b30eefa3142cbcc93201bc3a23abef Mon Sep 17 00:00:00 2001 From: Oliver Atkinson Date: Thu, 2 May 2024 19:32:08 +0100 Subject: [PATCH 09/12] Update Sources/Perception/Internal/BetaChecking.swift --- Sources/Perception/Internal/BetaChecking.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Perception/Internal/BetaChecking.swift b/Sources/Perception/Internal/BetaChecking.swift index f7a4813..56cd867 100644 --- a/Sources/Perception/Internal/BetaChecking.swift +++ b/Sources/Perception/Internal/BetaChecking.swift @@ -14,13 +14,13 @@ let isObservationBeta: Bool = { if (os.majorVersion, os.minorVersion, os.patchVersion) != (10, 0, 0) { return false } + #endif var size = 0 sysctlbyname("kern.osversion", nil, &size, nil, 0) var version = [CChar](repeating: 0, count: size) sysctlbyname("kern.osversion", &version, &size, nil, 0) // NB: Beta builds end with a lowercase character (_e.g._, '21A5277j') return String(cString: version).last?.isLowercase == true - #endif #endif return false }() From 863f635f4dd4644075d08f3c1942d5c6c6b0b3d7 Mon Sep 17 00:00:00 2001 From: Oliver Atkinson Date: Thu, 2 May 2024 19:32:31 +0100 Subject: [PATCH 10/12] Update BetaChecking.swift --- Sources/Perception/Internal/BetaChecking.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Perception/Internal/BetaChecking.swift b/Sources/Perception/Internal/BetaChecking.swift index 56cd867..35459df 100644 --- a/Sources/Perception/Internal/BetaChecking.swift +++ b/Sources/Perception/Internal/BetaChecking.swift @@ -15,12 +15,12 @@ let isObservationBeta: Bool = { return false } #endif - var size = 0 - sysctlbyname("kern.osversion", nil, &size, nil, 0) - var version = [CChar](repeating: 0, count: size) - sysctlbyname("kern.osversion", &version, &size, nil, 0) - // NB: Beta builds end with a lowercase character (_e.g._, '21A5277j') - return String(cString: version).last?.isLowercase == true + var size = 0 + sysctlbyname("kern.osversion", nil, &size, nil, 0) + var version = [CChar](repeating: 0, count: size) + sysctlbyname("kern.osversion", &version, &size, nil, 0) + // NB: Beta builds end with a lowercase character (_e.g._, '21A5277j') + return String(cString: version).last?.isLowercase == true #endif return false }() From 8bc629ce257ba6db9012a98c328d12e3eb6efb04 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 2 May 2024 11:33:10 -0700 Subject: [PATCH 11/12] fix --- Sources/Perception/Internal/BetaChecking.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Perception/Internal/BetaChecking.swift b/Sources/Perception/Internal/BetaChecking.swift index 35459df..0a4ed96 100644 --- a/Sources/Perception/Internal/BetaChecking.swift +++ b/Sources/Perception/Internal/BetaChecking.swift @@ -21,6 +21,7 @@ let isObservationBeta: Bool = { sysctlbyname("kern.osversion", &version, &size, nil, 0) // NB: Beta builds end with a lowercase character (_e.g._, '21A5277j') return String(cString: version).last?.isLowercase == true + #else + return false #endif - return false }() From c4bf2564aa83a26addf9c1141a3388c56744d7b8 Mon Sep 17 00:00:00 2001 From: Oliver Atkinson Date: Fri, 3 May 2024 09:59:38 +0100 Subject: [PATCH 12/12] check return value before creating cString --- Sources/Perception/Internal/BetaChecking.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Perception/Internal/BetaChecking.swift b/Sources/Perception/Internal/BetaChecking.swift index 0a4ed96..dcaa709 100644 --- a/Sources/Perception/Internal/BetaChecking.swift +++ b/Sources/Perception/Internal/BetaChecking.swift @@ -18,9 +18,9 @@ let isObservationBeta: Bool = { var size = 0 sysctlbyname("kern.osversion", nil, &size, nil, 0) var version = [CChar](repeating: 0, count: size) - sysctlbyname("kern.osversion", &version, &size, nil, 0) + let ret = sysctlbyname("kern.osversion", &version, &size, nil, 0) // NB: Beta builds end with a lowercase character (_e.g._, '21A5277j') - return String(cString: version).last?.isLowercase == true + return ret == 0 ? String(cString: version).last?.isLowercase == true : false #else return false #endif