궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
SWIFTUI/Image

SwiftUI : AspectRatio / GeometryReader / GeometryProxy

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

AspectRatio / GeometryReader / GeometryProxy

SwiftUI View에서 Image를 만들 때, 그것의 내용의 크기에 따라 자체의 크기를 자동으로 불러옵니다.

 

따라서 사진이 1000x500이면 Image view1000x500이 됩니다.

 

이것은 가끔은 사용자가 원할 때도 있지만, 대부분은 더 낮은 크기로 이미지를 보여주고 싶을 때가 있습니다.

어떻게 이미지를 사용자의 화면 너비에 맞게 만들 수 있는지 알아보도록 하겠습니다.

 

일단 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   /   오: GeometryReader

// 좌측

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)

Textbackground 를 이용해서 오른쪽 왼쪽 버튼을 만들 때 자동으로 사용 가능한 너비와 높이를 알려주는 것도 가능합니다.

 

즉, 동일한 너비를 차지하는 두 개의 뷰를 얻으려면 다음과 같이 사용 가능한 공간을 절반으로 나눌 수 있는 것입니다.

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)
    }
}

TIP
 
 

GeometryReader가 사용된 환경에 따라 매개변수의 값이 달라지므로, 결괏값으로 나온 것이 항상 같이 않다는 점을 꼭 염두에 두어야 합니다.

frame

GeometryProxy는 프레임에 대한 정보도 제공을 하고 있는데, 여기서 프레임은 단순히 그 자신의 CGRect[각주:1] 값을 전달하는 것이 아니라 CoordinateSpace라는 열거형 타입이 가진 세 가지 값 중 하나를 지정하면 그 좌표 공간에 관한 정보를 반환합니다.

enum CoordinateSpace {
   case global
   case local
   case named(AnyHashable)
}   

각 좌표계가 어떤 값을 반환하는지 살펴보면 다음과 같습니다.

구분 설명
global 화면전체영역(윈도우의 bounds)을 기준으로 한 좌표 정보
local 지오메트리 리더의 bounds를 기준으로 한 좌표 정보
named 명시적으로 이름을 할당한 공간을 기준으로 한 좌표 정보
  1. global은 윈도의 bounds를 기준으로 한 좌표 정보를 반환합니다. 즉 윈도우의 원점으로부터 계산된 좌표에 해당합니다.
  2. local은 지오메트리 자기 자신에 대한 bounds값을 반환합니다.
  3. named는 지정한 뷰의 원점을 기준으로 한 상대적인 좌표를 구하고 싶을 때 사용합니다. 부모 뷰나 조상 뷰 중에서 미리 관심 있는 뷰에 대해 coordinateSpace수식어를 이용해 이름을 지정하고, 그 이름을 named의 연관 값으로 전달해주면 해당 뷰와의 상대적인 거리를 구할 수 있습니다.

 

읽어주셔서 감사합니다🤟

 

 

  1. CGSize 에는 너비와 높이가 있고, CGRect 에는 x, y, 너비 및 높이가 있습니다. 즉, CGRect는 좌표가 있는 CGSize로 생각할 수 있습니다. CGRect는 일반적으로 상위 뷰의 (위, 왼쪽) 좌표에 상대적인 (위, 왼쪽) 좌표 인 UIView 프레임을 지정하는 데 사용됩니다. [본문으로]

잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.