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()
}
このコードで、複数のスクロールビューの連動が可能です。
コメント