iOS14의 SwiftUI에서는 리스트의 스크롤 처리를 코드로 제어할 수 있게 되었다

소개



iOS13의 SwiftUI에서는 할 수 없었던 코드에 의한 리스트의 스크롤 처리가, iOS14에서는 할 수 있게 되었습니다.
본 기사에서 그 구현 방법을 정리하고 있습니다.

iOS13에서 스크롤 처리



iOS 앱을 개발하고 있다고 자주 있는 「○번째의 셀에 자동으로 스크롤 한다」라고 하는 처리입니다만,
이것을 iOS13의 SwiftUI에서는 실현할 방법이 없었습니다.
struct ContentView: View {
    var body: some View {
        List(0..<100) {
            Text("\($0)")
        }
    }
}

UIKit에서는 다음과 같은 형태로 scrollToRow , scrollToItem 등의 메소드를 호출하는 형태로 해당 요건을 쉽게 구현할 수 있으므로,
이 기능을 위해서만 UIViewRepresentable , UIViewControllerRepresentable 등을 이용하는 일도 자주 있었습니다.
// UITableView
tableView.scrollToRow(at: IndexPath(row: 10, section: 0),
                      at: .top,
                      animated: true)

// UICollectionView
collectionView.scrollToItem(at: .init(item: 10, section: 0), 
                            at: .top,
                            animated: true)

iOS14에서 스크롤 처리



iOS14에서는 ScrollViewReader 라는 스크롤 상태를 제어할 수 있는 새로운 View가 추가되어 있으며,
이것을 이용하면 임의의 지점에 자동으로 스크롤 시키는 처리의 구현이 가능하게 됩니다.

다음은 샘플 코드입니다.

준비



먼저 준비로 스크롤 지점을 지정하는 TextField를 설치합니다.
struct ContentView: View {
    @State private var text: String = ""

    var body: some View {
        VStack {
            HStack {
                TextField("input row number", text: $text)
                Button("Scroll") {
                    guard let row = Int(text) else { return }
                    print(row)
                }
            }.padding()

            List(0..<100) {
                Text("\($0)")
            }
        }
    }
}

이렇게 하면 버튼을 탭하면 사용자가 TextField에 입력한 값을 얻을 수 있습니다.

스크롤 제어



그러면 실제로 스크롤시키는 처리의 구현 부분입니다.

우선은 스크롤 제어를 하고 싶은 부분을 ScrollViewReader 로 둘러쌉니다.
이렇게하면 스크롤 제어가 가능한 ScrollViewProxy 인스턴스를 얻을 수 있습니다.
struct ContentView: View {
    @State private var text: String = ""

    var body: some View {
        VStack {
+           ScrollViewReader { (proxy: ScrollViewProxy) in
                HStack {
                    TextField("input row number", text: $text)
                    Button("Scroll") {
                        guard let row = Int(text) else { return }
                    }
                }.padding()
                List(0..<100) {
                    Text("\($0)")
                }
+           }
        }
    }
}

또한 스크롤 위치를 식별하기위한 ID를 대상 View에 부여하고,
제어를 개시하고 싶은 부분에서 ScrollViewProxyscrollTo 메소드에 그 ID를 지정하는 것만으로 스크롤 처리를 실현할 수가 있습니다.
struct ContentView: View {
    @State private var text: String = ""

    var body: some View {
        VStack {
            ScrollViewReader { (proxy: ScrollViewProxy) in
                HStack {
                    TextField("input row number", text: $text)
                    Button("Scroll") {
                        guard let row = Int(text) else { return }
+                       withAnimation {
+                           proxy.scrollTo(row, anchor: .top)
+                       }
                    }
                }.padding()

                List(0..<100) {
                    Text("\($0)")
+                       .id($0)
                }
            }
        }
    }
}

이것으로 하고 싶은 것을 실현할 수 있게 되었습니다.



덧붙여서 withAnimation 를 부여하지 않으면, 스크롤의 애니메이션은 행해지지 않습니다.
또, 제 2 인수의 anchor 로 스크롤 후의 위치를 ​​세세하게 지정할 수 있습니다.



그리드에 대해서도 문제없이 작동했습니다.
struct ContentView: View {
    @State private var text: String = ""

    var body: some View {
        VStack {
            ScrollViewReader { (proxy: ScrollViewProxy) in
                HStack {
                    TextField("input row number", text: $text)
                    Button("Scroll") {
                        guard let row = Int(text) else { return }
                        withAnimation {
                            proxy.scrollTo(row, anchor: .top)
                        }
                    }
                }.padding()

                ScrollView {
                    LazyVGrid(
                        columns: [
                            GridItem(.flexible(minimum: 0, maximum: .infinity)),
                            GridItem(.flexible(minimum: 0, maximum: .infinity)),
                        ],
                        alignment: .center,
                        spacing: nil
                    ) {
                        ForEach(0..<100) {
                            Text("\($0)")
                                .frame(height: 100)
                        }
                    }
                }
            }
        }
    }
}

좋은 웹페이지 즐겨찾기