SwiftUI

컨텐츠 크기에 따른 자동 ScrollView 전환하기 (feat. SwiftUI)

GREEN.1229 2024. 10. 17. 18:59

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

이번 포스팅에서는 SwiftUI에서 간단히 컨텐츠 크기에 따른 자동 ScrollView로 전환하는 코드 구현을 소개해볼까 합니다 🙋🏻

 


자동 ScrollView 전환하기

일반적으로 우리가 컨텐츠를 채워서 구현할때 코드 상 스크롤뷰로 감싸지 않으면 스크롤되지가 않죠.

물론 List와 같은 그런 컴포넌트 사용은 제외하고요..!

 

그런데, 데이터를 받아오는 입장에서 이 데이터를 그려줄때 디바이스의 어느정도 사이즈를 차지할지 미리 알 순 없죠.

그렇기에, 스크롤이 되게 스크롤뷰로 미리 감싸놓는 경우가 많습니다.

 

만약 요구하는 UI / UX가 컨텐츠가 적어서 디바이스에 스크롤을 하지 않아도 다 표시가 된다면 스크롤이 되지 않도록하고, 컨텐츠가 많아서 디바이스의 사이즈를 넘어가면 스크롤이 적용되도록 해야한다고 가정해볼께요 😃

 

그럴때는 우리가 그려주려는 컨텐츠의 사이즈와 디바이스의 사이즈를 비교하여 스크롤뷰로 감쌀지 그냥 보여줄지 분기를 구현해봐야 합니다.

 

이 생각을 한번 SwiftUI에서 구현해봤어요 🙋🏻

 

import SwiftUI

struct DynamicScrollView<Content: View>: View {
  let content: Content
  @State private var contentHeight: CGFloat = .zero
  
  init(@ViewBuilder content: () -> Content) {
    self.content = content()
  }
  
  var body: some View {
    GeometryReader { geometry in
      if contentHeight > geometry.size.height {
        ScrollView {
          contentView
        }
      } else {
        contentView
      }
    }
  }
  
  private var contentView: some View {
    content
      .background(
        GeometryReader { geometry in
          Color.clear.preference(
            key: ContentHeightKey.self,
            value: geometry.size.height
          )
        }
      )
      .onPreferenceChange(ContentHeightKey.self) { height in
        self.contentHeight = height
      }
  }
}

struct ContentHeightKey: PreferenceKey {
  static var defaultValue: CGFloat = 0
  
  static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
    value = max(value, nextValue())
  }
}

 

코드는 간단해요.

 

동적으로 적용하기 위해 DynamicScrollView라는 컴포넌트를 새로 만듭니다.

해당 구조체는 제네릭 구조체로, 우리가 이제 앞으로 넘겨와서 그려줄 뷰를 Content 제네릭으로 타입 파라미터를 가집니다.

당연히 View 타입이겠죠?

 

그리고 가장 중요한 상태 변수인 contentHeight가 있어요.

해당 변수는 컨텐츠의 사이즈 중 height를 저장하게 됩니다.

 

그리고 body 구현으로 넘어가볼께요!

 

body에서는 GeometryReader를 활용해 해당 DynamicScrollView가 할당받은 사이즈를 얻습니다.

if문에서 보듯 넘어온 컨텐츠의 높이가 할당받은 높이보다 크면 ScrollView로 감싸고 작거나 같다면 그냥 스크롤뷰로 감싸지 않고 그대로 다 표현하게 됩니다.

 

마지막으로 contentView는 어떻게 다룰까요?

 

우리가 필요한 값은 contentHeight죠?

즉, 해당 컨텐츠의 높이를 알아야해요.

그렇기에 background를 이용해 내부에서 GeometryReader로 감사써 실제 컨텐츠의 크기를 측정합니다.

그리고 onPreferenceChange 모디파이어를 활용하여 ContentHiehgtKey의 값이 변경될때마다 contentHeight 값을 업데이트 합니다.

 

여기서 ContentHeightKey는 일반적으로 사이즈 측정을 위해 많이들 구현하는 PreferenceKey입니다.

 

정리하면, 컨텐츠 데이터가 들어오면 백그라운드 모디파이어에서 해당 컨텐츠의 사이즐르 측정하고 해당 값을 ContentHeightKey에 저장하죠.

그리고, 저장된 해당 값은 Change에서 contentHeight 상태 변수를 업데이트 시켜줍니다.

그럼 contentView를 부르는 상단 DynamicScrollView의 body에선 contentHeight 상태 값이 변경되었으니 현재 해당 구조체가 할당받은 높이와 contentHeight를 비교해 스크롤뷰로 감싸주거나 그냥 들어온 컨텐츠 그 자체로 보여주게 되는것이죠!

 

아주 간단하죠!?

 

그럼 한번 사용해볼까요?

 

struct ContentView: View {
  @State var nums: [Int] = [1, 2, 3]
  
  var body: some View {
    VStack {
      Text("START")
        .font(.title)
      
      Spacer()
      
      DynamicScrollView {
        VStack(spacing: 20) {
          ForEach(nums, id: \.self) { i in
            Text("Number \(i)")
              .frame(height: 50)
              .frame(maxWidth: .infinity)
              .background(Color.green.opacity(0.1))
          }
        }
        .padding()
      }
      .padding(.vertical, 20)
      
      Spacer()
      
      Button(
        action: {
          let randomCount = Int.random(in: 1...20)
          nums = Array(1...randomCount)
        },
        label: {
          Text("CHANGE")
            .font(.title)
        }
      )
    }
  }
}

 

이렇게 VStack으로 감싸진 뷰가 있어요.

nums라는 1부터 20까지의 숫자 배열을 데이터로 받아옵니다.

여기서 상단에 START 텍스트가 있고 하단에 CHANGE 버튼이 있어요.

하단에 버튼을 누르면 이제 1부터 20의 범위 안에서 순차적인 숫자가 들어있는 배열이 데이터로 업데이트 됩니다.

 

그 사이, DynamicScrollView를 사용하여 Text를 그려줍니다.

여기서는 감싸진 VStack이 컨텐츠겠죠!

 

그럼 이제, 버튼을 눌러서 컨텐츠의 크기가 변경될 때, 스크롤이 필요없다면 그냥 그대로 나타나고 스크롤을 해야 다 보이는 환경이라면 스크롤이 가능하도록 스크롤뷰로 감싸질겁니다.

 

한번 돌려볼까요?

 

 

정확히 의도대로 구현되었죠?

스크롤뷰로 감싸진 여부는 우측 스크롤바의 존재 여부를 봐도 좋습니다.

위아래 어떤 뷰가 들어오던 해당 DynamicScrollView가 사용할 수 있는 공간만큼에서 계산을 하니 더 다른 높이 계산이 필요없는 장점이 있어요!

 


마무리

되게 간단하게 동적인 스크롤뷰를 만들어볼 수 있었습니다ㅎㅎ

해당 예제 프로젝트는 아래 깃헙 레포에도 있으니 편하게 이용해도 좋습니다!

 

playground/dynamic at main · GREENOVER/playground

학습을 하며 간단한 예제들을 만들어 보는 작고 소중한 놀이터. Contribute to GREENOVER/playground development by creating an account on GitHub.

github.com