iOS/SwiftUI
@NameSpace
Hamp
2025. 10. 25. 16:10
반응형

🏁 학습할 내용
- coordinateSpace
- @NameSpace
- 활용
🧭 CoordinateSpace
🤔 어디서 봤을까?

GeometryReader proxy 쪽에서 frame정보를 얻을 때, 어떤 Coordinate로부터 가져올 지
결정하게 되는데, 거기서 처음 접했던 것 같다.
🧱 구조
CoordinateSpace
public enum CoordinateSpace {
// 뷰 계층구조의 최상단을 기준으로하는 global 좌표계
case global
// 현재 뷰를 기준으로한 local 좌표계
case local
// Hashable한 값으로 구분된 뷰의 좌표계
case named(AnyHashable)
}
CoordinateSpaceProtocol
- 특정 좌표계를 갖고 있어야함.
public protocol CoordinateSpaceProtocol {
/// The resolved coordinate space.
var coordinateSpace: CoordinateSpace { get }
}
NamedCoordinateSpace
- iOS17부터 사용가능한 객체로
- CoordinateSpaceProtocl과 Equatable을 채택하고 있음
- 같음을 이용할 수 있는 형태
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
public struct NamedCoordinateSpace : CoordinateSpaceProtocol, Equatable {
/// The resolved coordinate space.
public var coordinateSpace: CoordinateSpace { get }
public static func == (a: NamedCoordinateSpace, b: NamedCoordinateSpace) -> Bool
}
마찬가지로 iOS17부터는 CoordinateSpace를 바로 쓰기보다는,
CoordinateSpaceProtocol을 채택하고 있는 구조체들을 씀
- GlobalCoordinateSpace,
- LocalCoordinateSpace,
- NamedCoordinateSpace

🚀 @NameSpace
눈치챘겠지만, 이 녀석은 NamedCoordinateSpace와 관려이 있을 것 같다..
쓸 수는 있지만, 실제로는 matchedGeometryEffect 같이 매개변수에 in: NameSpace.ID가 있는 녀석들과 매우 밀접하다.

🧱 구조
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
@frozen @propertyWrapper public struct Namespace : DynamicProperty, Sendable {
@inlinable public init()
public var wrappedValue: Namespace.ID { get }
@frozen public struct ID : Hashable {
public func hash(into hasher: inout Hasher)
public static func == (a: Namespace.ID, b: Namespace.ID) -> Bool
public var hashValue: Int { get }
}
}
- 영구적인 Identity로 정의된 객체에 접근할 수 있는 namespace
- propertyWrapper기 때문에, wrappedValue를 자세히보면 ID, 즉 Hashable한 타입
- DynamicProperty를 채택하고 있는 것을 보면, View Update 사이클과도 관련이 있음
♻️ 활용
iOS17부터, 기본적으로 유용하게 사용되는 NamedCoordinateSpace 제공해주는데 바로 scrollView다.
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
extension CoordinateSpaceProtocol where Self == NamedCoordinateSpace {
public static func scrollView(axis: Axis) -> Self
public static var scrollView: NamedCoordinateSpace { get }
}
아래와 같이, 원하는 axis을 명시해주면, 스크롤 axis가 일치하는 가장 가까운 조상의
ScrollView의 좌표공간을 얻을 수 있다.
GeometryReader { proxy in
let frame = proxy.frame(in: .scrollView(axis: .vertical)) // 기준이 되는 건?
Text("Item \(index)\nY: \(Int(frame.minY))")
.frame(width: 120, height: 120)
.background(Color.blue.opacity(0.3))
.cornerRadius(10)
}

그런데 여기서 중요한점은 iOS17부터 가능하다는거다...
이 기능을 똑같이 만들려면 @NameSpace + CoordinateSpace의 .named를 이용하면 만들 수 있다.
struct NestedScrollViewExample: View {
@Namespace var namespace
var body: some View {
ScrollView(.vertical) { // 바깥 스크롤뷰
VStack(spacing: 40) {
Text("Outer ScrollView")
.font(.headline)
ScrollView(.horizontal) { // 안쪽 스크롤뷰
HStack {
ForEach(0..<5) { index in
GeometryReader { proxy in
let frame = proxy.frame(in: .named(namespace)) //namespace로 부터 좌표공간 얻기
Text("Item \(index)\nY: \(Int(frame.minY))")
.frame(width: 120, height: 120)
.background(Color.blue.opacity(0.3))
.cornerRadius(10)
}
.frame(width: 120, height: 120)
}
}
.padding()
}
.background(Color.yellow.opacity(0.2))
}
}
.coordinateSpace(name: namespace) // ✅ 얻고싶은 coordinateSpace 표시
}
}
📘 요약 비교
| 항목 | coordinateSpace | @Namespace |
| 역할 | 좌표 계산 기준 정의 | 뷰 간 애니메이션 매칭 |
| 관련 함수 | GeometryReader, .frame(in:) | matchedGeometryEffect |
| 주요 용도 | 위치 추적, 스크롤, 정렬 | 자연스러운 뷰 전환 애니메이션 |
| 범위 | 좌표계 이름 단위 | 애니메이션 그룹 단위 |
출처
반응형