Обнаружить, когда пользователь прокрутил до конца горизонтального ScrollView в SwiftUI

Я создал горизонтальную карусель, используя ScrollView и HStack.

Это мой код:

struct CarouselView<Content: View>: View {
    let content: Content
    
    private let spacing: CGFloat
    private let shouldSnap: Bool
    
    init(spacing: CGFloat = .zero,
         shouldSnap: Bool = false,
         @ViewBuilder content: @escaping () -> Content) {
        self.content = content()
        self.spacing = spacing
        self.shouldSnap = shouldSnap
    }
    
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: spacing) {
                content
            }.apply {
                if #available(iOS 17.0, *), shouldSnap {
                    $0.scrollTargetLayout()
                } else {
                    $0
                }
            }
        }
        .clipped()
        .apply {
            if #available(iOS 17.0, *), shouldSnap {
                $0.scrollTargetBehavior(.viewAligned)
            } else {
                $0
            }
        }
    }
}

Затем я могу использовать его следующим образом:

CarouselView(spacing: 10) {
    ForEach(0 ..< imagesNames.count, id: \.self) { index in
        DemoView()
    }
}

Как я могу определить, когда пользователь прокрутил до конца просмотра прокрутки.

Я попробовал некоторые ответы здесь - основной из них - добавление представления в конец hstack и выполнение работы над onAppear этого представления не работает, поскольку оно запускается сразу же при создании HStack.

Это могло бы сработать, если бы я использовал LazyHStack, однако мне приходится использовать HStack из-за некоторых ограничений.

Есть ли другие способы добиться этого?

Вы можете попробовать что-то простое, например, использовать .scrollPosition(id: $scrollID), см. ScrollPosition, чтобы определить, когда пользователь прокрутил до конца представления прокрутки.

workingdog support Ukraine 03.05.2024 05:44
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
1
230
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Вы можете использовать этот DetectOnScreen модификатор представления, который я написал в этом ответе, чтобы определить, находится ли на экране невидимое представление.

struct IsOnScreenKey: PreferenceKey {
    
    static let defaultValue: Bool? = nil
    static func reduce(value: inout Value, nextValue: () -> Value) {
        if let next = nextValue() {
            value = next
        }
    }
}

struct DetectIsOnScreen: ViewModifier {
    func body(content: Content) -> some View {
        GeometryReader { reader in
            content
                .preference(
                    key: IsOnScreenKey.self,
                    // bounds(of: .scrollView) gives us the scroll view's frame
                    // frame(in: .local) gives us the frame of the invisible view
                    // both are in the local coordinate space
                    value: reader.bounds(of: .scrollView)?.intersects(reader.frame(in: .local)) ?? false
                )
        }
    }
}

Использование:

ScrollView(.horizontal, showsIndicators: false) {
    HStack(spacing: spacing) {
        content
        Color.clear
            .frame(width: 0, height: 0)
            .modifier(DetectIsOnScreen())
            .onPreferenceChange(IsOnScreenKey.self) { value in
                if value == true {
                    print("Has reached end!")
                }
            }
    }
}

Это означает, что HStack будет иметь дополнительный вид, поэтому между последним представлением и невидимым представлением будет некоторое дополнительное пространство (размером spacing). Если это нежелательно, вы можете обернуть последнее представление в собственное HStack и поместить невидимое представление во внутреннее HStack. Но это нужно делать на месте использования.

ForEach(0..<imagesNames.count, id: \.self) { index in
    if index == imageNames.count - 1 {
        HStack(spacing: 0) {
            DemoView()
            Color.clear
                .frame(width: 0, height: 0)
                ...
        }
    } else { DemoView() }
}

Если вы не против использовать View Extractor, вы можете сделать это в CarouselView

HStack(spacing: spacing) {
    ExtractMulti(content) { views in
        ForEach(views) { view in
            if view.id == views.last?.id {
                HStack(spacing: 0) {
                    view
                    Color.clear
                        .frame(width: 0, height: 0)
                        ...
                }
            } else {
                view
            }
        }
    }
}

Спасибо, ваше первое решение действительно работает хорошо. Я попытался использовать его для вертикальной прокрутки и сделал дополнительный интервал, который показался неожиданным. Отдельный вопрос здесь: stackoverflow.com/questions/78488242/…

Shawn Frank 16.05.2024 09:41

Вы можете попробовать этот подход, используя простой .scrollPosition(id: $scrolledID), чтобы определить, когда пользователь прокрутил страницу до конца.

Пример кода:

struct ContentView: View {
    @State private var scrolledID: Int? = 0 // <-- here
    
    var body: some View {
        Text("scroll position: \(scrolledID ?? 0)") // <-- for testing
        Text("is at end: \(scrolledID == 4 ? "YES" : "NO") ") // <-- for testing
        
        CarouselView(spacing: 10, scrolledID: $scrolledID) {  // <-- here
            ForEach(0 ..< 5, id: \.self) { index in
                DemoView()
            }
        }
    }
}

struct CarouselView<Content: View>: View {
    let content: Content
    
    private let spacing: CGFloat
    private let shouldSnap: Bool
    
    @Binding var scrolledID: Int?  // <-- here
    
    init(spacing: CGFloat = .zero, shouldSnap: Bool = false, scrolledID: Binding<Int?>, @ViewBuilder content: @escaping () -> Content) {
        self.content = content()
        self.spacing = spacing
        self.shouldSnap = shouldSnap
        self._scrolledID = scrolledID // <-- here
    }
    
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: spacing) {
                content
            }
            .scrollTargetLayout() // <-- here
        }
        .scrollPosition(id: $scrolledID) // <-- here
        .scrollTargetBehavior(.paging)
        .clipped()
    }
}

// -- for testing
struct DemoView: View {
    var body: some View {
        ZStack {
            Text("DemoView")
            Rectangle().stroke(Color.red, lineWidth: 6)
        }.frame(width: 345, height: 345)
    }
}

Другие вопросы по теме