diff --git a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/CoordinateSpace.swift b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/CoordinateSpace.swift new file mode 100644 index 000000000..6c9443822 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/CoordinateSpace.swift @@ -0,0 +1,75 @@ +// +// CoordinateSpace.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +/// A resolved coordinate space created by the coordinate space protocol. +/// +/// You don't typically use `CoordinateSpace` directly. Instead, use the static +/// properties and functions of `CoordinateSpaceProtocol` such as `.global`, +/// `.local`, and `.named(_:)`. +public enum CoordinateSpace { + /// The global coordinate space at the root of the view hierarchy. + case global + + /// The local coordinate space of the current view. + case local + + /// A named reference to a view's local coordinate space. + case named(AnyHashable) + + @_spi(ForOpenSwiftUIOnly) + public struct ID: Equatable, Sendable { + let value: UniqueID + + public init() { + value = .init() + } + } + + @_spi(ForOpenSwiftUIOnly) + case id(CoordinateSpace.ID) + + package static let root: CoordinateSpace = .global + + package var canonical: CoordinateSpace { self } + + package enum Name: Equatable { + case name(AnyHashable) + case id(CoordinateSpace.ID) + package var space: CoordinateSpace { + switch self { + case let .name(name): .named(name) + case let .id(id): .id(id) + } + } + } +} + +@available(*, unavailable) +extension CoordinateSpace: Sendable {} + +extension CoordinateSpace { + public var isGlobal: Bool { self == .global } + + public var isLocal: Bool { self == .local } +} + +extension CoordinateSpace: Equatable, Hashable { + public func hash(into hasher: inout Hasher) { + switch self { + case .global: + hasher.combine(0) + case .local: + hasher.combine(1) + case let .named(anyHashable): + hasher.combine(2) + anyHashable.hash(into: &hasher) + case let .id(id): + hasher.combine(3) + hasher.combine(id.value) + } + } +} diff --git a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/CoordinateSpaceProtocol.swift b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/CoordinateSpaceProtocol.swift new file mode 100644 index 000000000..dc97d676a --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/CoordinateSpaceProtocol.swift @@ -0,0 +1,36 @@ +// +// CoordinateSpaceProtocol.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +/// A frame of reference within the layout system. +/// +/// All geometric properties of a view, including size, position, and +/// transform, are defined within the local coordinate space of the view's +/// parent. These values can be converted into other coordinate spaces +/// by passing types conforming to this protocol into functions such as +/// `GeometryProxy.frame(in:)`. +/// +/// For example, a named coordinate space allows you to convert the frame +/// of a view into the local coordinate space of an ancestor view by defining +/// a named coordinate space using the `coordinateSpace(_:)` modifier, then +/// passing that same named coordinate space into the `frame(in:)` function. +/// +/// VStack { +/// GeometryReader { geometryProxy in +/// let distanceFromTop = geometryProxy.frame(in: "container").origin.y +/// Text("This view is \(distanceFromTop) points from the top of the VStack") +/// } +/// .padding() +/// } +/// .coordinateSpace(.named("container")) +/// +/// You don't typically create types conforming to this protocol yourself. +/// Instead, use the system-provided `.global`, `.local`, and `.named(_:)` +/// implementations. +public protocol CoordinateSpaceProtocol { + /// The resolved coordinate space. + var coordinateSpace: CoordinateSpace { get } +} diff --git a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/GlobalCoordinateSpace.swift b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/GlobalCoordinateSpace.swift new file mode 100644 index 000000000..707b55304 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/GlobalCoordinateSpace.swift @@ -0,0 +1,21 @@ +// +// GlobalCoordinateSpace.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +/// The global coordinate space at the root of the view hierarchy. +public struct GlobalCoordinateSpace: CoordinateSpaceProtocol { + public init() {} + + public var coordinateSpace: CoordinateSpace { .global } +} + +@available(*, unavailable) +extension GlobalCoordinateSpace: Sendable {} + +extension CoordinateSpaceProtocol where Self == GlobalCoordinateSpace { + /// The global coordinate space at the root of the view hierarchy. + public static var global: GlobalCoordinateSpace { .init() } +} diff --git a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/LocalCoordinateSpace.swift b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/LocalCoordinateSpace.swift new file mode 100644 index 000000000..73fe67076 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/LocalCoordinateSpace.swift @@ -0,0 +1,21 @@ +// +// LocalCoordinateSpace.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +/// The local coordinate space of the current view. +public struct LocalCoordinateSpace: CoordinateSpaceProtocol { + public init() {} + + public var coordinateSpace: CoordinateSpace { .local } +} + +@available(*, unavailable) +extension LocalCoordinateSpace: Sendable {} + +extension CoordinateSpaceProtocol where Self == LocalCoordinateSpace { + /// The local coordinate space of the current view. + public static var local: LocalCoordinateSpace { .init() } +} diff --git a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/NamedCoordinateSpace.swift b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/NamedCoordinateSpace.swift new file mode 100644 index 000000000..ec30b61ed --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/NamedCoordinateSpace.swift @@ -0,0 +1,43 @@ +// +// NamedCoordinateSpace.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +/// A named coordinate space. +/// +/// Use the `coordinateSpace(_:)` modifier to assign a name to the local +/// coordinate space of a parent view. Child views can then refer to that +/// coordinate space using `.named(_:)`. +public struct NamedCoordinateSpace: CoordinateSpaceProtocol, Equatable { + package var name: CoordinateSpace.Name + + package init(name: CoordinateSpace.Name) { + self.name = name + } + + public var coordinateSpace: CoordinateSpace { name.space } +} + +@available(*, unavailable) +extension NamedCoordinateSpace: Sendable {} + +extension CoordinateSpaceProtocol where Self == NamedCoordinateSpace { + /// Creates a named coordinate space using the given value. + /// + /// Use the `coordinateSpace(_:)` modifier to assign a name to the local + /// coordinate space of a parent view. Child views can then refer to that + /// coordinate space using `.named(_:)`. + /// + /// - Parameter name: A unique value that identifies the coordinate space. + /// + /// - Returns: A named coordinate space identified by the given value. + public static func named(_ name: some Hashable) -> NamedCoordinateSpace { + NamedCoordinateSpace(name: .name(name)) + } + + package static func id(_ id: CoordinateSpace.ID) -> NamedCoordinateSpace { + NamedCoordinateSpace(name: .id(id)) + } +} diff --git a/Sources/OpenSwiftUICore/Layout/CoordinateSpace/ScrollCoordinateSpace.swift b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/ScrollCoordinateSpace.swift new file mode 100644 index 000000000..741773a0c --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/CoordinateSpace/ScrollCoordinateSpace.swift @@ -0,0 +1,44 @@ +// +// ScrollCoordinateSpace.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +package enum ScrollCoordinateSpace { + package static let vertical: CoordinateSpace.ID = .init() + package static let horizontal: CoordinateSpace.ID = .init() + package static let all: CoordinateSpace.ID = .init() + package static let content: CoordinateSpace.ID = .init() +} + +extension CoordinateSpace { + package static var verticalScrollView: CoordinateSpace { .id(ScrollCoordinateSpace.vertical) } + package static var horizontalScrollView: CoordinateSpace { .id(ScrollCoordinateSpace.horizontal) } + package static var scrollView: CoordinateSpace { .id(ScrollCoordinateSpace.all) } + package static var scrollViewContent: CoordinateSpace { .id(ScrollCoordinateSpace.content) } +} + +extension CoordinateSpace.Name { + package static var verticalScrollView: CoordinateSpace.Name { .id(ScrollCoordinateSpace.vertical) } + package static var horizontalScrollView: CoordinateSpace.Name { .id(ScrollCoordinateSpace.horizontal) } + package static var scrollView: CoordinateSpace.Name { .id(ScrollCoordinateSpace.all) } + package static var scrollViewContent: CoordinateSpace.Name { .id(ScrollCoordinateSpace.content) } +} + +extension CoordinateSpaceProtocol where Self == NamedCoordinateSpace { + /// The named coordinate space that is added by the system for the innermost + /// containing scroll view that allows scrolling along the provided axis. + public static func scrollView(axis: Axis) -> Self { + switch axis { + case .horizontal: NamedCoordinateSpace(name: .horizontalScrollView) + case .vertical: NamedCoordinateSpace(name: .verticalScrollView) + } + } + + /// The named coordinate space that is added by the system for the innermost + /// containing scroll view. + public static var scrollView: NamedCoordinateSpace { NamedCoordinateSpace(name: .scrollView) } + + package static var scrollViewContent: NamedCoordinateSpace { NamedCoordinateSpace(name: .scrollViewContent) } +}