Проблема привязки SwiftUI к горизонтальной прокрутке при начальной загрузке, когда `visibleItems` четный

Я создаю горизонтальный ScrollView в SwiftUI, который привязывает элементы к центру экрана. Привязка работает отлично при прокрутке, но при первой загрузке представления исходный элемент немного смещается и не привязывается к центру, как ожидалось.

Я обнаружил, что проблема возникает, когда число visibleItems четное. При нечетном числе visibleItems первоначальное выравнивание и привязка работают правильно с самого начала.

Вот код для моего CircleScrollView:

struct CircleScrollView: View {

    @State(initialValue: 2)
    var initialPosition: Int

    @State(initialValue: 8)
    private var visibleItems: Int

    @State(initialValue: 0)
    private var currentIndex: Int

    private let spacing: CGFloat = 16

    var body: some View {
        ZStack(alignment: .leading) {

            // For visuals of screen centre
            Rectangle()
                .fill(Color.gray.opacity(0.2))
                .ignoresSafeArea()
                .frame(maxWidth: UIScreen.main.bounds.width / 2, maxHeight: .infinity, alignment: .leading)


            GeometryReader { geometry in

                let totalSpacing = spacing * CGFloat(visibleItems - 1)
                let circleSize = (geometry.size.width - totalSpacing) / CGFloat(visibleItems)

                ScrollViewReader { scrollViewProxy in
                    ScrollView(.horizontal) {
                        HStack(spacing: spacing) {
                            ForEach(1..<100) { index in
                                ZStack {
                                    Text("\(index)")
                                    Circle().fill(Color(.tertiarySystemFill))
                                }
                                .frame(width: circleSize, height: circleSize)
                                .id(index)
                            }
                        }
                        .scrollTargetLayout()
                        .padding(.horizontal, (geometry.size.width - circleSize) / 2)
                        .onAppear {
                            scrollViewProxy.scrollTo(initialPosition, anchor: .center)
                            currentIndex = initialPosition
                        }
                    }
                    .scrollIndicators(.never)
                    .scrollTargetBehavior(.viewAligned)
                }
            }
        }
    }
}

Проблема:

  • Начальная позиция прокрутки смещается, если visibleItems четный.
  • Работает корректно при прокрутке или когда visibleItems нечетное число.

Предпринятые шаги:

  • Проверил расчет circleSize и пробела.
  • Проверьте исходное выравнивание, отрегулировав отступы.

Вопрос:

  • Как я могу обеспечить правильное выравнивание исходного элемента, если visibleItems четный?
  • Есть ли лучший способ справиться с выравниванием или привязкой во время начальной загрузки?

Проблема привязки SwiftUI к горизонтальной прокрутке при начальной загрузке, когда `visibleItems` четный

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

Ответы 1

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

Похоже, эта проблема связана с горизонтальным заполнением HStack. Он работает без заполнения (но, очевидно, только для позиций, где оно не требуется).

Я предполагаю, что ViewAlignedScrollTargetBehavior немного сломан. В качестве обходного пути вы можете попробовать реализовать свой собственный ScrollTargetBehavior.

Я попробовал и обнаружил, что функция updateTarget вызывается по-разному, в зависимости от того, первый ли это показ или в ответ на жест прокрутки:

  • При вызове первого показа цель имеет привязку .center, а ширина цели равна ширине круга. Смещение по оси X целевого источника включает половину ширины экрана.

  • Интересно, что для первого показа коррекция не требуется. Похоже, именно здесь ViewAlignedScrollTargetBehavior идет не так.

  • При последующем вызове жестов прокрутки целевая привязка равна нулю, а целевая ширина — это ширина контейнера (ScrollView).

  • Нулевой якорь интерпретируется как .topLeading. Итак, в этом случае смещение цели по оси X относится к переднему краю ScrollView, а не к центру.

  • Я попробовал обновить целевую привязку до .center и отрегулировать целевую ширину, но не смог заставить ее работать при таком подходе (он всегда прокручивался слишком сильно). Кажется, лучше всего оставить целевую привязку и ширину без изменений и признать, что они относятся к передней кромке.

Вот пользовательское поведение, специфичное для вашего макета:

struct StickyCentrePosition: ScrollTargetBehavior {
    let itemWidth: CGFloat
    let spacing: CGFloat
    let sidePadding: CGFloat

    func updateTarget(_ target: inout ScrollTarget, context: TargetContext) {

        // dx is the distance from the target anchor to the
        // leading edge of a centered item
        let dx = (target.anchor?.x ?? 0) == 0
            ? (context.containerSize.width / 2) - (itemWidth / 2)
            : 0
        let currentTargetIndex = (target.rect.origin.x + dx - sidePadding) / (itemWidth + spacing)
        let roundedTargetIndex = currentTargetIndex.rounded()
        let scrollCorrection = (roundedTargetIndex - currentTargetIndex) * (itemWidth + spacing)
        target.rect.origin.x += scrollCorrection
    }
}

Поскольку вы используете переменную состояния currentIndex для записи выбранной позиции, хорошо будет использовать ее в качестве .scrollPosition для ScrollView. Таким образом, он обновляется по мере прокрутки, и ScrollViewReader не требуется. Чтобы это работало, переменную просто нужно изменить на необязательную.

Вот полностью обновленный пример, который теперь работает как для четного, так и для нечетного числа видимых элементов:

struct CircleScrollView: View {
    let initialPosition = 2
    let visibleItems = 8
    let spacing: CGFloat = 16
    @State private var currentIndex: Int?

    var body: some View {
        ZStack {

            HStack(spacing: 0) {
                Color.gray.opacity(0.2)
                Color.clear
            }
            .ignoresSafeArea()

            GeometryReader { geometry in
                let screenWidth = geometry.size.width
                let totalSpacing = spacing * CGFloat(visibleItems - 1)
                let circleSize = (screenWidth - totalSpacing) / CGFloat(visibleItems)
                let sidePadding = (screenWidth - circleSize) / 2

                ScrollView(.horizontal) {
                    HStack(spacing: spacing) {
                        ForEach(1..<100) { index in
                            ZStack {
                                Text("\(index)")
                                Circle().fill(Color(.tertiarySystemFill))
                            }
                            .frame(width: circleSize, height: circleSize)
                            .id(index)
                        }
                    }
                    .scrollTargetLayout()
                    .padding(.horizontal, sidePadding)
                }
                .scrollIndicators(.never)
                .scrollTargetBehavior(
                    StickyCentrePosition(
                        itemWidth: circleSize,
                        spacing: spacing,
                        sidePadding: sidePadding
                    )
                )
                .scrollPosition(id: $currentIndex, anchor: .center)
                .onAppear { currentIndex = initialPosition }
            }
        }
    }
}

Я наконец-то понял, как интерпретировать значения в свитке target, ответ обновлен с лучшей реализацией. Теперь он правильно привязывается к ближайшему элементу (вместо того, чтобы всегда переходить к следующему), и ему не требуется снабжать флагом, указывающим, отображается ли четное или нечетное количество элементов.

Benzy Neez 04.09.2024 20:28

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