ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SwiftUI에서 View의 Size 측정하기
    SwiftUI 2023. 9. 27. 11:04

    안녕하세요. 그린입니다 🍏

    이번 포스팅에서는 SwiftUI에서 View의 Size를 측정하는 코드를 공유해볼까합니다 🙋🏻

     


    어떨때 View Size 값이 필요할까요?

    우선, SwiftUI로 구현을 하다보면 원하는 해당 영역의 뷰 사이즈를 가지고 어떠한 작업을 해줘야하는 경우가 발생합니다.

     

    예를들어, VStack으로 하위 뷰들이 쌓일때 헤더 영역에 여러 뷰들을 조합하여 넣고 이에 대한 헤더 영역의 뷰 높이를 가지고 어떠한 작업을 해줄때가 종종 발생합니다.

     

    버튼 탭 등 인터랙션이 발생했을때 offset을 최상단으로 이동시키는것이 아닌 헤더 영역 이후부터 노출시키고 싶다면 이 헤더 영역의 height를 구해 y offset을 변경해줘야 할 수 있죠.

     

    물론 이럴 경우 헤더 영역에 담긴 하위 뷰들의 프레임이 고정적으로 지정되어 있다면 개발자가 수동으로 이 사이즈들을 다 더한 헤더 프레임 높이를 계산해줘도 되고 헤더 영역의 프레임을 직접 고정시켜줘도 되겠죠.

     

    그런데 만약 헤더 영역에 콘텐츠에 따라 가변적인 프레임을 갖게 되는 경우라면 이는 사용할 수 없을겁니다.

     

    그렇기에 우리는 View의 Size를 측정할 수 있는 별도의 뷰 모디파이어를 만들어서 활용한다면 좋지 않을까요?

     

    바로 한번 구현해보로 가시죠!


    View의 Size 측정하는 뷰 모디파이어 구현하기

     

    우선 코드로 볼까요?

     

    extension View {
      @ViewBuilder
      func onReadSize(_ perform: @escaping (CGSize) -> Void) -> some View {
        self.customBackground {
          GeometryReader { geometryProxy in
            Color.clear
              .preference(key: SizePreferenceKey.self, value: geometryProxy.size)
          }
        }
        .onPreferenceChange(SizePreferenceKey.self, perform: perform)
      }
      
      @ViewBuilder
      func customBackground<V: View>(alignment: Alignment = .center, @ViewBuilder content: () -> V) -> some View {
        self.background(alignment: alignment, content: content)
      }
    }
    
    struct SizePreferenceKey: PreferenceKey {
      static var defaultValue: CGSize = .zero
      static func reduce(value: inout CGSize, nextValue: () -> CGSize) { }
    }

     

    먼저 View를 익스텐션하여 만들 수 있겠죠?

    하나씩 살펴보시죠 🧙🏻‍♀️

     


    SizePreferenceKey

    우선 SizePreferenceKey 구조체를 만들어서 PreferenceKey 프로토콜을 준수하도록 합니다.

    여기에서는 키에 대응되는 값으로 CSSize를 가집니다.

    실제로 PreferenceKey를 따랐기에 뷰 계층 내에서 값을 공유할 수 있게 됩니다.

     


    customBackground

    해당 메서드는 그냥 단순히 SwiftUI의 background 메서드와 같은 역할을 해줍니다.

    사실 만들지않고 바로 써도되는데 조금 더 구분을 하기 위해 만들었습니다.

     


    onReadSize

    해당 메서드가 핵심인데요.

    View의 크기를 읽어서 외부로 전달하는 역할을 지닌 메서드입니다.

    인자로 주어진 클로저에 크기 정보를 전달해주죠.

    이러한 동작을 위해서 내부적으로 GeometryReader와 PreferenceKey를 사용합니다.

     

    설명이 진짜 단순할 수 밖에 없을정도로 간단합니다!

     

    그럼 한번 정말 이 뷰 사이즈를 잘 측정하고 전달하는지 확인해볼까요?


    View Size 측정 확인하기

    struct ContentView: View {
      var body: some View {
        VStack {
          Text("This is Top")
          Spacer()
          Text("This is Bottom")
        }
        .onReadSize {
          print($0)
        }
      }
    }
    
    // (109.0, 763.0)

     

    이렇게 간단히 사용하면 해당 VStack의 Size를 잘 전달받을 수 있습니다.

    여기서 109가 width, 763이 height겠죠!

     

    자 그럼 이렇게 사용하는건 알았는데 실제적으로 어떨때 사용하면 좋을까요?

     

    우선 스크롤 뷰로 구성하여 버튼을 누르면 특정 영역으로 이동하고 싶다면 우리는 SwiftUI에서 ScrollViewReader를 사용하고 id 값을 부여하여 scorllTo 메서드를 이용해 스크롤 오프셋을 조정해줄 수 있어요!

     

    혹시 이 방법을 처음 들어보셨다면 아래 포스팅을 먼저 보고 오시면 훨씬 더 좋습니다 😀

     

    SwiftUI - ScrollViewReader

    안녕하세요. 그린입니다🍏 이번 포스팅에서는 SwiftUI에서 ScrollViewReader라는것에 대해 학습해보겠습니다🙌 SwiftUI를 사용하면서 ScrollView라는 View를 아주 많이 사용하게 됩니다. 이때 현재 스크롤

    green1229.tistory.com

     

    즉, SwiftUI에서도 충분히 내장되어 있기에 굳이 SwiftUI로 스크롤뷰를 다 구성한 이런 스크롤 환경에서는 의미가 없을 수 있습니다.

     

    그러나 현재 SwiftUI를 현업에서 많이 사용한다 하더라도 안되는것들이 많아 UIKit을 끌어와 사용하는 경우가 많죠!?

    그렇기에 우리도 UIKit의 UIScrollView를 SwiftUI에서 사용한다는 가정에서 한번 구현해볼께요 😇

     


    UIKit의 UIScrollView 사용 시 onReadSize의 역할 살펴보기

    자 그럼 먼저 UIScrollView를 SwiftUI에서 사용할 수 있게 UIViewRepresentable을 통해 만들어볼까요?

     


    ScrollViewRepresentable

    struct ScrollViewRepresentable<Content: View>: UIViewRepresentable {
      @Binding var isMovedTop: Bool
      @Binding var headerSize: CGSize
      private let content: Content
      
      init(
        isMovedTop: Binding<Bool>,
        headerSize: Binding<CGSize>,
        @ViewBuilder content: @escaping () -> Content
      ) {
        _isMovedTop = isMovedTop
        _headerSize = headerSize
        self.content = content()
      }
      
      func makeUIView(context: Context) -> UIScrollView {
        let scrollView = UIScrollView()
        let hostVC = UIHostingController(rootView: self.content)
        hostVC.view.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(hostVC.view)
        
        NSLayoutConstraint.activate([
          hostVC.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
          hostVC.view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
          hostVC.view.topAnchor.constraint(equalTo: scrollView.topAnchor),
          hostVC.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
          hostVC.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
        ])
        
        return scrollView
      }
      
      func updateUIView(_ uiView :UIScrollView, context :Context) {
        if isMovedTop {
          uiView.setContentOffset(CGPoint(x: 0, y: headerSize.height), animated: true)
          DispatchQueue.main.async {
            isMovedTop = false
          }
        }
      }
    }

     

    코드 별거 없습니다!

    UIKit의 UIScrollView를 SwiftU에서 사용할 수 있게 기본적인 구현이에요.

    보시면 isMovedTop과 headerSize를 바인딩시켜 updateUIView 메서드쪽에서 판단합니다.

    즉, isMovedTop이 true이면 해당 스크롤 뷰의 오프셋을 headerSize의 height만큼 내려온곳을 위치시켜 보여주는것이죠!

     

    그럼 이거 한번 SwiftUI에서 올려볼까요?


    ContentView

    struct ContentView: View {
      @State var isMovedTop: Bool = false
      @State var headerSize: CGSize = .zero
      let color: [Color] = [.red, .blue, .orange, .green, .black, .indigo, .mint]
      
      var body: some View {
        ScrollViewRepresentable(
          isMovedTop: $isMovedTop,
          headerSize: $headerSize
        ) {
          VStack(spacing: 10) {
            Text("This is Top")
              .font(.title)
              .onReadSize {
                headerSize = $0
              }
            
            ForEach(color, id: \.self) { color in
              Rectangle()
                .fill(color)
                .frame(width: 300, height: 300)
            }
            
            Button(
              action: {
                isMovedTop = true
              },
              label: {
                Text("Go to Top")
              }
            )
          }
        }
      }
    }

     

    이렇게 단순할 수가 없습니다.

    ScrollViewRepresentable로 스크롤뷰를 만들때 isMovedTop과 headerSize를 넣어주고 클로저로 스크롤뷰에 들어갈 원하는 뷰를 구성해주면 되죠.

     

    실제 동작에서는 가장 하단 Go to Top 버튼이 눌리면 isMovedTop이 true가 되고 그럼 updateUIView에서 로직을 돌겠죠?

    이때 최상단에 위치한 This is Top이라는 텍스트의 사이즈를 우리가 위에서 미리 구현해온 onReadSize를 가지고 사이즈를 측정하고 이를 headerSize에 담아줬습니다.

    headerSize의 height만큼 떨어진 y위치로 오프셋을 이동시켜주게 되는것이죠.

     

    그렇기에 우리는 onReadSize와 UIScrollView를 통해 SwiftUI의 ScrollViewReader를 사용하지 않는 환경에서도 원하는 위치로 스크롤 이동과 관련된 기능을 해줄 수 있습니다 🙋🏻

     

    그럼 실제로 한번 동작 시켜볼까요!?


    대망의 시뮬레이터 동작 😉

     

    짜잔 잘 되죠?

    보시면 의도한것처럼 최상단으로 스크롤이 되는게 아니고 우리가 최상단 텍스트의 뷰 사이즈를 알아와서 이 height만큼 떨어진 y offset으로 이동시키고 싶었는데 아주아주 잘됩니다!

     


    마무리

    어쩌다 조금씩 살이 붙었는데 SwiftUI에서 아직도 UIKit의 사용이 필수적이라 한번 구현을 공유해봤습니다.

    해당 예제 프로젝트는 아래 제 깃헙 레포에 있으니 편하게 사용하셔도 됩니다 🙋🏻

    https://github.com/GREENOVER/playground/tree/main/readSize

    'SwiftUI' 카테고리의 다른 글

    BorderlessButtonStyle의 활용  (44) 2023.10.19
    allowsHitTesting을 통한 뷰 터치 이벤트 넘기기  (54) 2023.10.13
    ScrollTargetBehavior  (49) 2023.09.18
    DatePicker & Picker 사용하기  (43) 2023.09.11
    SwiftUI의 onChange 사용 시 주의할 부분  (41) 2023.09.05
Designed by Tistory.