AspectRatio / GeometryReader / GeometryProxy
SwiftUI View에서 Image
를 만들 때, 그것의 내용의 크기에 따라 자체의 크기를 자동으로 불러옵니다.
따라서 사진이 1000x500
이면 Image view
도 1000x500
이 됩니다.
이것은 가끔은 사용자가 원할 때도 있지만, 대부분은 더 낮은 크기로 이미지를 보여주고 싶을 때가 있습니다.
어떻게 이미지를 사용자의 화면 너비에 맞게 만들 수 있는지 알아보도록 하겠습니다.
일단 Example 이라는 이미지를 Assets
에 넣어준 후, 간단하게 코드를 입력합니다.
struct ContentView: View {
var body: some View {
VStack {
Image("Example")
}
}
}
→ 프리뷰에서 사용 가능한 공간에 비해 너무 크다는 걸 확인할 수 있죠? 이미지에는 .frame()
을 통해 사이즈를 조절할 수 있습니다.
VStack {
Image("Example")
.frame(width: 300, height: 300)
.clipped()
→ 프레임은 잘 잡혔지만, 이미지는 그대로이고 파란색 박스만 형성된 것을 볼 수 있는데 .clipped()
를 넣어줘서 이미지를 자를 수 있습니다. 하지만, .resizable()
와.aspectRatio
를 사용하는 게 더 간편합니다.
Resizable / AspectRatio
Image("Example")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)
Image("Example")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 300, height: 300)
.aspectRatio(contentMode: )
두 가지 옵션이 있는데 .fit
과 .fill
입니다.
고정된 크기의 이미지를 원하는 경우에는 잘 작동하지만, 화면을 채우기 위해 자동으로 확장되는 이미지를 원하는 경우가 많죠?
이럴 때는 GeometryReader
를 사용하면 됩니다.
GeometryReader
SwiftUI에서는 GeometryReader
를 제공하고 있고 이것은 아주 유용한 생성자입니다.
GeometryReader
는 지금 배우려는 것보다 더 많은 기능이 있지만, 지금 챕터에서는 이미지가 컨테이너 뷰의 전체 너비를 채우도록 하기 위해 한 가지 작업만 하도록 합시다.
GeometryReader
는 자식 뷰에 부모 뷰와 기기에 대한 크기 및 좌표계 정보를 전달하는 기능을 수행하는 컨테이너 뷰입니다. 아이폰이 회전하는 경우처럼 뷰의 크기가 변경되더라도 그 값이 자동으로 경신됩니다.
먼저 GeometryReader
의 생성자를 살펴보면, content 매개 변수 하나만 있는 것을 볼 수 있습니다. 이 매개 변수는 GeomeryProxy
타입의 정보를 받아 콘텐츠를 정의하는 함수를 전달하는데, 대부분의 컨테이너 뷰에서 아무런 입력 값도 주어지지 않는 것과 비교해 대조적입니다.
GeometryReader
의 뷰가 배열되는 방식은 ZStack과 같이 겹겹이 쌓이는 계층 구조를 가지게 됩니다. 그런데 뷰가 정렬되는 방식에는 조금 독특한 방식을 가집니다.
// 좌측
ZStack{
Circle().fill(Color.yellow)
.frame(width: 350, height: 350)
.overlay(Text("서근").font(.title))
Circle().fill(Color.green)
.frame(width: 280, height: 280)
Circle().fill(Color.blue)
.frame(width: 200, height: 200)
}
// 우측
GeometryReader { geo in
Circle().fill(Color.yellow)
.frame(width: 350, height: 350)
.overlay(Text("서근").font(.title))
Circle().fill(Color.green)
.frame(width: 280, height: 280)
Circle().fill(Color.blue)
.frame(width: 200, height: 200)
}
그리고GeometryReader
에 크기를 지정해 주지 않았는데도 화면 전체 크기만큼 확장된 것이 보이죠? Color.Rectangle 등을 사용하는 것처럼 GeometryReader
도 크기를 지정하지 않으면, 주어진 공간 내에서 최대 크기를 가지게 됩니다.
이 GeometryProxy
를 사용하여 다음과 같이 이미지의 너비를 설정할 수 있습니다.
VStack {
GeometryReader { geo in
Image("strawberries")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: geo.size.width, height: 300)
}
}
이제 우리가 사용하는 장치에 관계없이 화면 너비를 자동으로 채워지도록 해봅시다.
마지막 트릭 프레임의 height
를 제거해 보면, 아래와 같이 이미지가 보이는 것을 확인할 수 있습니다.
VStack {
GeometryReader { geo in
Image("Example")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: geo.size.width)
Text
와 background
를 이용해서 오른쪽 왼쪽 버튼을 만들 때 자동으로 사용 가능한 너비와 높이를 알려주는 것도 가능합니다.
즉, 동일한 너비를 차지하는 두 개의 뷰를 얻으려면 다음과 같이 사용 가능한 공간을 절반으로 나눌 수 있는 것입니다.
struct ContentView: View {
var body: some View {
GeometryReader { geometry in //geometry를 in한다.
HStack(spacing: 0) {
Text("Left")
//뷰에서 사용가능한 width를 2로 즉, 절반으로 나눔
.frame(width: geometry.size.width / 2, height: 50)
.background(Color.yellow)
Text("Right")
.frame(width: geometry.size.width / 2, height: 50)
.background(Color.orange)
}
}
}
}
두 개의 뷰가 있고 하나가 화면의 1/3
을 차지하고 다른 하나가 2/3
를 차지하기를 원하면 다음과 같이 작성할 수 있습니다.
.frame(width: geometry.size.width * 0.33)
.frame(width: geometry.size.width * 0.67)
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
HStack(spacing: 0) {
Text("Left")
.foregroundColor(.black)
.frame(width: geometry.size.width * 0.33, height: 50)
.background(Color.yellow)
Text("Right")
.foregroundColor(.black)
.frame(width: geometry.size.width * 0.67, height: 50)
.background(Color.orange)
}
}
}
}
텍스트가 화면의 맨 위로 올라왔는데 이것을 화면 중앙으로 옮겨주기 위해서는
GeometryReader{}
외부에 .frame(height: )
을 넣어주면 됩니다.
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
HStack(spacing: 0) {
Text("Left")
.foregroundColor(.black)
.frame(width: geometry.size.width * 0.33, height: 50)
.background(Color.yellow)
Text("Right")
.foregroundColor(.black)
.frame(width: geometry.size.width * 0.67, height: 50)
.background(Color.orange)
}
}
.frame(height: 50)
}
}
이미지의 가운데를 기준으로 자르기
어떠한 이미지를 resizable()한뒤에 scaleToFill()으로 이미지의 크기를 화면에 맞춰줄 수 있는 기능이 있는데 이미지의 크기에 따라 화면에 보여질 모습이 중앙이 아닌 오른쪽이나 왼쪽으로 치우쳐서 보일때가 있습니다. 아래처럼 말이죠
저는 캐릭터의 얼굴을 보이게 하고싶은데 원하지 않는 화면으로 출력되어있죠. 이럴때는 이미지의 중앙을 기준으로 화면을 확대하고 자를 수 있습니다.
GeometryReader { geo in
Image("SomeImage")
.resizable()
.scaleToFill()
.frame(width: geo.size.width, height: geo.size.height)
.clipped
}
GeometryProxy ⭐️⭐️
이제 GeometryReader
의 핵심인 GeometryProxy
에 대해 다뤄보도록 하겠습니다. GeometryProxy
는 다음과 같이 두 개의 프로퍼티와 하나의 메서드, 하나의 첨자를 제공하여 GeometryReader
의 레이아웃 정보를 자식 뷰에 제공할 수 있습니다.
struct GeometryProxy {
var size: CGSize { get }
var safeAreaInsets: EdgeInsets { get }
func frame(in coordinateSpace: CoordinateSpace) -> CGRect
subscrip<T>(anchor: Anchor<T>) -> T { get }
}
구분 | 설명 |
Size | GeometryReader의 크기를 반환 |
safeAreaInsets | GeometryReader가 사용된 환경에서의 안전 영역에 대한 크기를 반환 |
frame(in:) | 특정 좌표걔를 기준으로 한 프레임 정보를 제공 |
subscript(anchor:) | 자식뷰에서 anchorPreference 수식어를 이용해 제공한 좌표나 프레임을 GeometryReader의 좌표계를 기준으로 다시 변환하여 사용하여 사용하는 첨자입니다. 이때 Anchor의 제네릭 매개 변수에는 CGRect 또는 CGPoint 타입 두 가지를 사용 할 수 있습니다. |
이것을 사용해보도록 할게요.
Size, SafeAreaInsets
먼저 GeometryReader
와 safeArea의 크기를 바탕으로 자식 뷰에 상대적인 크기나 위치를 지정해 주는 예시를 보도록 할게요 :)
struct ContentView : View {
var body: some View {
GeometryReader { geometry in
VStack {
//자식뷰가 10개가 넘어가기때문에 Group으로 자정
Group {
Spacer()
Text("GeometryProxy").bold().font(.system(size: 40)).padding(.bottom,30)
//GeometryProxy size
Text("Size").bold().font(.title)
Text("width: \(Int(geometry.size.width))")
Text("height: \(Int(geometry.size.height))")
.padding(.bottom)
Divider()
//GeometryProxy safeAreaInsets
Text("SafeAreaInsets").bold().font(.title)
Text("top: \(Int(geometry.safeAreaInsets.top))")
Text("bottom: \(Int(geometry.safeAreaInsets.bottom))")
}
Spacer()
Color.yellow
.frame(
width: geometry.size.width,
height: geometry.safeAreaInsets.top,
alignment: .center)
}
}
.edgesIgnoringSafeArea(.bottom)
}
}
GeometryReader
가 사용된 환경에 따라 매개변수의 값이 달라지므로, 결괏값으로 나온 것이 항상 같이 않다는 점을 꼭 염두에 두어야 합니다.
frame
GeometryProxy
는 프레임에 대한 정보도 제공을 하고 있는데, 여기서 프레임은 단순히 그 자신의 CGRect
값을 전달하는 것이 아니라 1CoordinateSpace
라는 열거형 타입이 가진 세 가지 값 중 하나를 지정하면 그 좌표 공간에 관한 정보를 반환합니다.
enum CoordinateSpace {
case global
case local
case named(AnyHashable)
}
각 좌표계가 어떤 값을 반환하는지 살펴보면 다음과 같습니다.
구분 | 설명 |
global | 화면전체영역(윈도우의 bounds)을 기준으로 한 좌표 정보 |
local | 지오메트리 리더의 bounds를 기준으로 한 좌표 정보 |
named | 명시적으로 이름을 할당한 공간을 기준으로 한 좌표 정보 |
global
은 윈도의bounds
를 기준으로 한 좌표 정보를 반환합니다. 즉 윈도우의 원점으로부터 계산된 좌표에 해당합니다.local
은 지오메트리 자기 자신에 대한bounds
값을 반환합니다.named
는 지정한 뷰의 원점을 기준으로 한 상대적인 좌표를 구하고 싶을 때 사용합니다. 부모 뷰나 조상 뷰 중에서 미리 관심 있는 뷰에 대해coordinateSpace
수식어를 이용해 이름을 지정하고, 그 이름을named
의 연관 값으로 전달해주면 해당 뷰와의 상대적인 거리를 구할 수 있습니다.
읽어주셔서 감사합니다🤟
- CGSize 에는 너비와 높이가 있고, CGRect 에는 x, y, 너비 및 높이가 있습니다. 즉, CGRect는 좌표가 있는 CGSize로 생각할 수 있습니다. CGRect는 일반적으로 상위 뷰의 (위, 왼쪽) 좌표에 상대적인 (위, 왼쪽) 좌표 인 UIView 프레임을 지정하는 데 사용됩니다. [본문으로]
'SWIFTUI > Image' 카테고리의 다른 글
SwiftUI : AsyncImage [Placeholder,Extension,Phase,Transaction] (0) | 2022.01.07 |
---|---|
SwiftUI : Path / Shape (1) | 2021.04.22 |
SwiftUI : ContainerRelativeShape (위젯에서만 사용가능) (0) | 2021.03.13 |
SwiftUI : trim( ) - Shape의 일부 그리기 (Timer) (0) | 2021.03.13 |
SwiftUI : Shape (Rectangle, Circle, Capsule...) (0) | 2021.01.22 |