ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SwiftUI - ScrollViewReader
    SwiftUI 2022. 11. 3. 17:28

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

    이번 포스팅에서는 SwiftUI에서 ScrollViewReader라는것에 대해 학습해보겠습니다🙌

     

    SwiftUI를 사용하면서 ScrollView라는 View를 아주 많이 사용하게 됩니다.

    이때 현재 스크롤링을 감지하거나 자동으로 스크롤되어 필요한 포인트로 위치 변경을 하는 등의 기능이 필요한 경우가 많죠🥲

    이럴때 GeometryReader처럼 ScollView를 읽어 감지할 수 있는 녀석이 필요해요!

     

    그럴때 사용하는것이 바로 이 ScrollViewReader입니다😀

     

    그럼 자세히 어떤건지 알아보시죠🕺🏻

     

    ScrollViewReader란?

    위의 설명에서 조금 연장해보자면 ScrollViewReader는 하위 View, ScrollView를 스크롤하기 위해 프록시라는것과 함께 작업할 수 있도록 프로그래밍 방식의 스크롤을 제공하는 뷰입니다.

    결국 ScrollView를 다루면서 위에서 말한 일련의 이동 및 감지를 위해서 그런 타입으로 만들 수 있도록 감싸버리는 View입니다.

    이정도의 어느정도 부연 설명이면 충분할 것 같아 바로 선언 및 사용법으로 가보시죠.

     

    ScrollViewReader의 선언

    @frozen struct ScrollViewReader<Content> where Content : View

    간단합니다.

    앞서 말한것처럼 Content 즉, ScrollView를 제네릭 타입의 Content로 담아옵니다.

    결국 하위인 ScrollView를 가져와 여러 기능들이 구현되어 있는 View 타입이죠.

     

    그럼 이제 찐으로 사용법 고고🕺🏻

     

    ScrollViewReader 사용하기

    우선 공식문서를 통해 학습이 제일 정확하니 코드 부터 보시면서 부연 설명 이어가겠습니다.

    @Namespace var topID
    @Namespace var bottomID
    
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                Button("Scroll to Bottom") {
                    withAnimation {
                        proxy.scrollTo(bottomID)
                    }
                }
                .id(topID)
    
                VStack(spacing: 0) {
                    ForEach(0..<100) { i in
                        color(fraction: Double(i) / 100)
                            .frame(height: 32)
                    }
                }
    
                Button("Top") {
                    withAnimation {
                        proxy.scrollTo(topID)
                    }
                }
                .id(bottomID)
            }
        }
    }
    
    func color(fraction: Double) -> Color {
        Color(red: fraction, green: 1 - fraction, blue: 0.5)
    }

    ScrollViewReader의 Content View Builder ScrollViewProxy라는 프록시 인스턴스를 받습니다.

    즉 이 프록시를 사용해 스크롤링에 필요한 기능들을 해줄 수 있습니다.

    이걸 명심하고 가보자구요!

     

    위 코드는 단순히 100개의 하위 색상 뷰가 있고 상/하단에 각각 버튼이 있습니다.

    이 버튼을 클릭하면 서로 상단은 하단으로, 하단은 상단으로 스크롤하여 위치하도록 할 수 있어요.

    자 그걸 위해 ScrollView를 ScrollViewReader로 감쌉니다.

    그 다음 이동될 포인트들에 id값을 심어줘요.

    그럼 이제 추후 scrollTo 메서드를 통해 해당 id값이 위치한 곳으로 이동합니다.

    코드에서는 버튼이 눌렸을때 proxy.scrollTo(id)를 실행 시켜 자동으로 해당 위치로 이동하게 만들어줬습니다.

    그럼 한번 동작을 볼까요?

    자 버튼을 눌렀을때 원하는 위치로 슝하고 갑니다🙌

     

    그런데 아주 중요한점은 ScrollViewProxy를 컨텐츠 뷰 빌더가 실행되는 도중에는 사용하면 안됩니다.
    뷰가 다 그려지기전이기에 런타임 오류가 발생하게 됩니다.
    대신, 제스쳐 핸들러 및 onChange 메서드와 같이 컨텐츠 내에서 생성된 액션에서만 프록시를 호출할 수 있습니다.

     

    자 그런데 조금 더 알아보기전에 ScrollViewProxy가 뭔지도 한번 알아보시죠.

     

    ScrollViewProxy란?

    struct ScrollViewProxy

    선언은 구조체 타입으로, 뷰 계층 구조 내에서 스크롤 가능한 뷰의 프로그래밍 방식 스크롤을 지원하는 프록시 값입니다.

    직접 인스턴스를 만들지 않고 뷰 빌더에서의 인스턴스를 받습니다.

     

    아.. 더 나아가서 ScrollTo 메서드도 ㅎㅎ..

     

    scrollTo(_:anchor:)란?

    func scrollTo<ID>(
        _ id: ID,
        anchor: UnitPoint? = nil
    ) where ID : Hashable

    해당 선언으로 구성되어 있습니다.

    위치 이동될 id값과 anchor 즉, 위치 이동 시 top에 붙일지 bottom에 붙일지, 왼.오 어디쪽에 붙일지 정해주는거라 생각하면 됩니다.

    즉, 정렬 동작이죠!

    해당 메서드는 식별자가 존재하는 하위 뷰의 첫번째 프록시에 포함된 모든 스크롤 뷰를 id로 스캔한 뒤 해당 뷰로 스크롤 지원해줍니다.

    위 구동 영상에서 보셨다시피 자동 스크롤링이 되어 포커싱되죠!

     

    자 그러면 기본 구현에서 아주~ 조금 더 나아가볼께요.

    기존 위 코드에서 하나만 더 추가해볼께요.

    import SwiftUI
    
    struct ContentView: View {
      @Namespace var topID
      @Namespace var bottomID
      @State var isTapped: Bool = false
      
      var body: some View {
        ScrollViewReader { proxy in
          ScrollView {
            Button("Scroll to Bottom") {
              withAnimation {
                isTapped.toggle()
                proxy.scrollTo(bottomID)
              }
            }
            .id(topID)
            
            VStack(spacing: 0) {
              ForEach(0..<100) { i in
                color(fraction: Double(i) / 100)
                  .frame(height: 32)
                
                if isTapped {
                  VStack {
                    Text("이상한걸 보여드리겠습니다.")
                    Text("눈뜨고 지켜봐주세요!")
                    Text("버튼은 어디에..?")
                  }
                }
              }
            }
            
            Button("Top") {
              withAnimation {
                proxy.scrollTo(topID)
              }
            }
            .id(bottomID)
          }
        }
      }
    }

    버튼이 클릭 되었을때 텍스트 3개가 나타나도록 해주려해요.

    이랬을때 버튼 클릭 시 어떻게 될까요?

    우리가 원하던 아까처럼 맨 하단 버튼으로 정상적으로 갈까요?

    으악... 정상적으로 가지 않는걸 확인할 수 있어요ㅠ🥲

    왜 그럴까요?

    그건 아까 공식문서에서 나온걸로 유추할 수 있는데 버튼이 눌려 정상 동작을 하는것과 뷰를 그려주는것에서의 미스가 발생해요.

    즉 뷰를 다 그려주지 않고 이동하려니 뷰가 나오기전만 이동되는것이죠.

    이걸 해결하기 위해 저는 LazyVStack을 사용했습니다.

    LazyVStack(spacing: 0) {
      ForEach(0..<100) { i in
        color(fraction: Double(i) / 100)
          .frame(height: 32)
        
        if isTapped {
          VStack {
            Text("이상한걸 보여드리겠습니다.")
            Text("눈뜨고 지켜봐주세요!")
            Text("버튼은 어디에..?")
          }
        }
      }
    }

    자 이러면 정상적일까요?

    아.. gif로 변환하니 너무 빠르게 지나가버리고 시작되지만 ㅠㅠ top 버튼으로 정상적으로 내려갑니다!

    뷰를 다 그려주고 진행하도록 하니 문제없어요 역시!

     

    마무리

    이렇게 ScrollViewReader를 쓸 일이 생겨서 알아봤습니다.

    역시 유용하네요!👍

     

    [참고 자료]

    https://developer.apple.com/documentation/swiftui/scrollviewreader

     

    Apple Developer Documentation

     

    developer.apple.com

    https://developer.apple.com/documentation/swiftui/scrollviewproxy

     

    Apple Developer Documentation

     

    developer.apple.com

    https://developer.apple.com/documentation/swiftui/scrollviewproxy/scrollto(_:anchor:) 

     

    Apple Developer Documentation

     

    developer.apple.com

Designed by Tistory.