0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

個人でiOSアプリ開発を行なっている田中幹久です。

今回は水平のScrollViewをピッタリ中央で停止させる方法を紹介します。
Vertivalのリストだと簡単にできるような記事を見たことがあるような気がしますが、HorizontalScrollViewに実装している記事はなかったので実装してみました。

スクロール量をonPreferenceChangeに渡し、スクロールが終わればアイテムの幅や、アイテム間のスペースから中央に表示するインデックスを求め、目的地にスクロールさせる、という手順です。

完成形

ScrollAdjuster.gif

こんな感じでスクロールが終われば一番中央に表示されているアイテムが中央に移動します。

全体のコードはこちらのGitHubに載せてます!

各変数が何を表しているか分かりやすくするために図を用意してみました。

ScrollAdjuster図1.png

スクロール量の変化の監視

このScrollOffsetKeyPreferenceChangeに投げることで、スクロールの変化を検知しています。reduce()PreferenveKeyプロトコルに準拠するための関数です。

ScrollOffsetKey.swift
import SwiftUI

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

全体コード

ContenView.swift
import SwiftUI

struct ContentView: View {
    let ITEMS = ["🍎","🍊","🍇","🍌","🍉","🍐","🍒"]
    let ITEM_SPACE : CGFloat = 70  //アイテム間のスペース
    let ITEM_WIDTH : CGFloat = 100  //アイテムの幅
    @State var centerIndex : Int = 0  //中央に表示するアイテムのインデックス
    @State var scrollStopTask: DispatchWorkItem?  //遅延アクション管理
    var body: some View {
        GeometryReader{ geometry in
            
            VStack{
                Text(centerIndex.description)
                
                ScrollViewReader { proxy in
                    ScrollView(.horizontal,showsIndicators: false) {
                        LazyHStack(spacing:ITEM_SPACE){
                            ForEach(Array(ITEMS.enumerated()), id: \.offset) { index , item in
                                ZStack{
                                    Rectangle()
                                        .frame(width: ITEM_WIDTH, height: 100)
                                        .cornerRadius(12)
                                        .foregroundColor(Color(.systemGray6))
                                    Text(item)
                                }
                            }
                        }  //LazyStack終点
                        .overlay(
                            GeometryReader { proxy in
                                Color.clear.preference(
                                    key: ScrollOffsetKey.self,
                                    value: proxy.frame(in: .global).minX
                                )
                            }
                        )
                    }
                    .onPreferenceChange(ScrollOffsetKey.self) { value in  //スクロール管理
                        detectScrollEnd(value, proxy: proxy, geometry: geometry)
                    }
                }
            }
        }
    }
    // スクロールの変化を検知し、一定時間後にスクロール終了を判定
    private func detectScrollEnd(_ newOffset: CGFloat,proxy:ScrollViewProxy, geometry : GeometryProxy) {
        let moveIndex = calculateIndex(newOffset, geometry: geometry)

        
        // すでにスケジュールされたタスクをキャンセル
        scrollStopTask?.cancel()
        
        
        // 新しいタスクを作成して 0.2 秒後にスクロール停止と判定
        let task = DispatchWorkItem {
            Task{
                print("📌 スクロールが終了しました!\(moveIndex)")
                withAnimation {
                    proxy.scrollTo(moveIndex, anchor: .center)
                }
            }
        }
        
        scrollStopTask = task
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: task)
        
    }
    
    private func calculateIndex(_ newOffset: CGFloat, geometry : GeometryProxy) -> Int {
        
        let adjustedContentOffset = -newOffset + geometry.size.width/2  //画面中央から右向きに計測したスクロール量
        
        let double_index : Double = ((adjustedContentOffset) / (ITEM_SPACE + ITEM_WIDTH))
        
        
        centerIndex = Int(double_index)
        return Int(double_index)
        
    }
    
    
}

#Preview {
    ContentView()
}

全体のコードはこちらのGitHubに載せてます!

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?