Проблема привязки 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` четный


1
51
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 }
            }
        }
    }
}