スポンサーリンク

【SwiftUI】二つのScrollViewのスクロールの動きを連動させる

SwiftUI

SwiftUIでScrollViewを連動させる処理にかなり手間取ったので、紹介します。

//
//  Created by JapaneseCityLikers on 2024/11/11.
//

import SwiftUI

struct SynchronizedScrollView<Content: View>: UIViewRepresentable {
    var content: Content
    // 横方向のスクロール状態。横方向で連動させたい場合は、二つのこのViewに同じoffsetをBindしてください
    @Binding var scrollOffsetX: CGFloat
    // 縦方向のスクロール状態
    @Binding var scrollOffsetY: CGFloat
    // スクロールビューの方向。縦横スクロールさせたい場合は、axis: [.horizontal, .vertical]のように指定
    var axis: Axis.Set

    init(scrollOffsetX: Binding<CGFloat>, scrollOffsetY: Binding<CGFloat>, axis: Axis.Set = .horizontal, @ViewBuilder content: () -> Content) {
        self._scrollOffsetX = scrollOffsetX
        self._scrollOffsetY = scrollOffsetY
        self.axis = axis
        self.content = content()
    }

    func makeUIView(context: Context) -> UIScrollView {
        let scrollView = UIScrollView()
        scrollView.delegate = context.coordinator
        scrollView.showsHorizontalScrollIndicator = axis.contains(.horizontal)
        scrollView.showsVerticalScrollIndicator = axis.contains(.vertical)
        scrollView.backgroundColor = .clear

        let hostingController = UIHostingController(rootView: content.background(Color.clear))
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        hostingController.view.backgroundColor = .clear
        scrollView.addSubview(hostingController.view)

        NSLayoutConstraint.activate([
            hostingController.view.topAnchor.constraint(equalTo: scrollView.topAnchor),
            hostingController.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
        ])
        
        if axis.contains(.horizontal) && !axis.contains(.vertical) {
            hostingController.view.heightAnchor.constraint(equalTo: scrollView.heightAnchor).isActive = true
        } else if !axis.contains(.horizontal) && axis.contains(.vertical) {
            hostingController.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
        }

        return scrollView
    }

    func updateUIView(_ uiView: UIScrollView, context: Context) {
        // 横と縦のスクロール位置を個別に同期
        if uiView.contentOffset.x != scrollOffsetX || uiView.contentOffset.y != scrollOffsetY {
            uiView.setContentOffset(CGPoint(x: scrollOffsetX, y: scrollOffsetY), animated: false)
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(scrollOffsetX: $scrollOffsetX, scrollOffsetY: $scrollOffsetY)
    }

    class Coordinator: NSObject, UIScrollViewDelegate {
        @Binding var scrollOffsetX: CGFloat
        @Binding var scrollOffsetY: CGFloat

        init(scrollOffsetX: Binding<CGFloat>, scrollOffsetY: Binding<CGFloat>) {
            _scrollOffsetX = scrollOffsetX
            _scrollOffsetY = scrollOffsetY
        }

        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            scrollOffsetX = scrollView.contentOffset.x
            scrollOffsetY = scrollView.contentOffset.y
        }
    }
}

struct ContentestView: View {
    @State private var scrollOffsetX1: CGFloat = .zero
    @State private var scrollOffsetY1: CGFloat = .zero
    @State private var scrollOffsetX2: CGFloat = .zero
    @State private var scrollOffsetY2: CGFloat = .zero

    var body: some View {
        VStack {
            SynchronizedScrollView(scrollOffsetX: $scrollOffsetX1, scrollOffsetY: $scrollOffsetY1, axis: .horizontal) {
                HStack {
                    ForEach(0 ..< 10) { index in
                        Text("Item \(index)")
                            .frame(width: 100, height: 100)
                            .background(Color.blue)
                            .cornerRadius(10)
                    }
                }
            }.frame(height: 100)

            HStack {
                SynchronizedScrollView(scrollOffsetX: $scrollOffsetX1, scrollOffsetY: $scrollOffsetY2, axis: [.horizontal, .vertical]) {
                    VStack {
                        ForEach(0 ..< 10) { row in
                            HStack {
                                ForEach(0 ..< 10) { column in
                                    Text("(\(row), \(column))")
                                        .frame(width: 100, height: 100)
                                        .background(Color.green)
                                        .cornerRadius(10)
                                }
                            }
                        }
                    }
                }
                
                SynchronizedScrollView(scrollOffsetX: $scrollOffsetX2, scrollOffsetY: $scrollOffsetY2, axis: .vertical) {
                    VStack {
                        ForEach(0 ..< 10) { row in
                            Text("Row \(row)")
                                .frame(width: 100, height: 100)
                                .background(Color.orange)
                                .cornerRadius(10)
                        }
                    }
                }.frame(width: 100)
            }
        }
    }
}

#Preview {
    ContentestView()
}

このコードで、複数のスクロールビューの連動が可能です。

コメント

タイトルとURLをコピーしました