Untitled

mail@pastecode.io avatar
unknown
plain_text
7 months ago
4.9 kB
3
Indexable
Never
//
//  LoopingScrollView.swift
//  InfiniteLoopingScrollView
//
//  Created by Ruslan Magomedov on 12.12.2023.
//

import SwiftUI

struct LoopingScrollView<Content: View, Item: RandomAccessCollection>: View where Item.Element: Identifiable {
    //Ширина элементов внутри ScrollView
    var width: CGFloat
    //Высота элементов внутри ScrollView
    var height: CGFloat
    //Промежуток между элементами
    var spacing: CGFloat = 0
    //Коллекция данных, которую нужно отобразить
    var items: Item
    //Флаг, определяющий, должны ли отображаться индикаторы прокрутки
    var showsIndicators = false
    
    var pagingEffect = true
    
    //Замыкание, которое принимает элемент коллекции и возвращает View для его отображения
    @ViewBuilder var content: (Item.Element) -> Content
    
    var body: some View {
        GeometryReader {
            let size = $0.size
            let repeatingCount = width > 0 ? Int((size.width / width).rounded()) + 1 : 1
            
            ScrollView(.horizontal, showsIndicators: showsIndicators) {
                LazyHStack(spacing: spacing) {
                    ForEach(items) { item in
                        content(item)
                            .frame(width: width, height: height)
                    }
                    
                    ForEach(0..<repeatingCount, id: \.self) { index in
                        let item = Array(items)[index % items.count]
                        content(item)
                            .frame(width: width, height: height)
                    }
                }
                .background {
                    ScrollViewHelper(
                        width: width,
                        spacing: spacing,
                        itemsCount: items.count,
                        repeatingCount: repeatingCount
                    )
                }
            }
            .frame(height: height)
        }
    }
}


// Внутренний UIViewRepresentable используется для добавления UIScrollViewDelegate
// к ScrollView, чтобы обеспечить бесконечную прокрутку
fileprivate struct ScrollViewHelper: UIViewRepresentable {
    var width: CGFloat
    var spacing: CGFloat
    var itemsCount: Int
    var repeatingCount: Int
    
    //Создаёт объект Coordinator
    func makeCoordinator() -> Coordinator {
        return Coordinator(
            width: width,
            spacing: spacing,
            itemsCount: itemsCount,
            repeatingCount: repeatingCount
        )
    }
    
    //Создаёт пустой UIView
    func makeUIView(context: Context) -> UIView {
        return UIView()
    }
    
    //Настраиваем и обновляем пораметры координатора
    func updateUIView(_ uiView: UIView, context: Context) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.06) {
            if let scrollView = uiView.superview?.superview?.superview as? UIScrollView, !context.coordinator.isAdded {
                scrollView.delegate = context.coordinator
                context.coordinator.isAdded = true
            }
        }
        
        context.coordinator.width = width
        context.coordinator.spacing = spacing
        context.coordinator.itemsCount = itemsCount
        context.coordinator.repeatingCount = repeatingCount
    }
    
    class Coordinator: NSObject, UIScrollViewDelegate {
        var width: CGFloat
        var spacing: CGFloat
        var itemsCount: Int
        var repeatingCount: Int
        
        init(width: CGFloat, spacing: CGFloat, itemsCount: Int, repeatingCount: Int) {
            self.width = width
            self.spacing = spacing
            self.itemsCount = itemsCount
            self.repeatingCount = repeatingCount
        }
        
        var isAdded: Bool = false
        
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            guard itemsCount > 0 else { return }
            let minX = scrollView.contentOffset.x
            let mainContentSize = CGFloat(itemsCount) * width
            let spacingSize = CGFloat(itemsCount) * spacing

            
            if minX > (mainContentSize + spacingSize) {
                scrollView.contentOffset.x -= (mainContentSize + spacingSize)
            }
            
            if minX < 0 {
                scrollView.contentOffset.x += (mainContentSize + spacingSize)
            }
        }
    }
}

#Preview {
    ContentView()
}
Leave a Comment