SwiftUI отслеживает и поддерживает положение прокрутки при вращении устройства

У меня есть ScrollView, содержащий ForEach, перебирающий массив элементов типа Verse. Каждое представление внутри ForEach представляет собой VStack с текстом на арабском и текстом на английском языке.

Текст на английском языке виден только тогда, когда readingMode стоит false. Текст на арабском языке выравнивается по центру, когда readingMode равен false.

Есть 3 части проблемы:

  1. Когда устройство поворачивается, вид не сохраняет положение прокрутки.
  2. Когда появляется представление, может быть начальная позиция прокрутки (стих, с которого должно начинаться представление)
  3. Я хочу иметь возможность обновлять положение прокрутки на основе выбранного стиха в средстве выбора колеса.

Вот упрощенный пример имеющегося у меня кода, который содержит только основные части представления. Пожалуйста, дайте мне знать, если вам нужна дополнительная информация.

ScrollView {
    ForEach(verses) { verse in
        VStack {
            HStack {
                if !readingMode {
                    Spacer()
                }
                
                Text("Some Arabic Text")
            }
            
            if !readingMode {
                Text("Some English Text")
            }
        }.id(verse.id)
    }
}

Я пробовал использовать модификаторы, такие как .onChange, в сочетании с UIDevice.current.orientation и модификатором .scrollPosition, чтобы получить позицию прокрутки перед поворотом и установить ее как позицию прокрутки после вращения, но это очень глючно и часто не работает.

Мне удалось решить часть 2 и 3 проблемы, используя функции ScrollViewReader и scrollTo, но в результате ScrollView может случайно перейти в другую часть представления, казалось бы, без всякой причины.

Судя по тому, что я нашел в Интернете в результате многочасовых исследований, SwiftUI должен автоматически сохранять положение прокрутки при повороте устройства, поэтому я могу делать что-то совершенно неправильно, что является причиной всех моих проблем.

Заранее спасибо за вашу помощь!


Обновлено: БОЛЬШЕ КОНКРЕТНОГО КОДА

Следующий код более точно представляет мой конкретный контекст, который может повлиять на возможные решения.

ScrollView {
    Text("Some Header")
    
    LazyVStack {
        ForEach(verses) { verse in
            
            VStack {
                HStack {
                    if !readingMode {
                        Spacer()
                    }
                    
                    Text(verse.arabic)
                        .multilineTextAlignment(readingMode ? .center : .trailing)
                }
                
                if !readingMode {
                    HStack {
                        Text(verse.english)
                            .multilineTextAlignment(.leading)
                        
                        Spacer()
                    }
                }
                
                Divider()
            }
            .id(verse.id)
            
        }
    }
}

Добро пожаловать в Stack Overflow! Пожалуйста, прочитайте Как создать минимальный воспроизводимый пример. , затем отредактируйте свой вопрос.

soundflix 14.07.2024 21:39
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
90
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Я бы предложил использовать .scrollPosition в сочетании с .scrollTargetLayout. Однако это еще не все.

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

  • Когда используется ленивый контейнер, например LazyVStack, предыдущий выбор обычно остается в поле зрения или почти в поле зрения. Вероятно, это связано с тем, что ленивый контейнер не имеет в памяти много дочерних представлений.

  • Когда контейнер не ленив, например VStack, предыдущий выбор обычно находится вне поля зрения.

Чтобы сохранить выбор в поле зрения, вы можете использовать обработчик .onChange для обнаружения обновлений привязки и сохранения нового значения, если оно не равно нулю. Затем:

  • Используйте отдельный обработчик .onChange для обнаружения изменения ориентации.

  • Один из способов сделать это — использовать GeometryReader для измерения размера экрана. Это работает и для разделенного экрана iPad.

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

Пример ниже показывает, что это работает. Здесь используется якорь .center для позиции прокрутки:

struct Verse: Identifiable {
    let someVerses = [
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
    ]
    let id: Int
    let text: String
    let bgColor: Color
    let fgColor: Color

    init(id: Int) {
        self.id = id
        text = someVerses[.random(in: 0..<someVerses.count)]
        let red = Double.random(in: 0...1)
        let green = Double.random(in: 0...1)
        let blue = Double.random(in: 0...1)
        bgColor = Color(red: red, green: green, blue: blue)
        fgColor = red + green + blue < 1.5 ? .white : .black
    }
}

struct ContentView: View {
    private let dummyId = 0
    private let verses: [Verse]
    @State private var verseId: Int?
    @State private var previousVerseId: Int?

    init() {
        var verses = [Verse]()
        for i in 1...100 {
            verses.append(Verse(id: i))
        }
        self.verses = verses
    }

    var body: some View {
        NavigationStack {
            GeometryReader { proxy in
                ScrollView {
                    VStack {
                        ForEach(verses) { verse in
                            VStack {
                                Text("Verse \(verse.id)")
                                Text(verse.text)
                            }
                            .padding()
                            .frame(maxWidth: .infinity)
                            .background(verse.bgColor)
                            .foregroundStyle(verse.fgColor)
                        }
                    }
                    .scrollTargetLayout()
                }
                .scrollPosition(id: $verseId, anchor: .center)
                .onChange(of: verseId) { oldVal, newVal in
                    if let newVal, newVal != dummyId {
                        previousVerseId = newVal
                    }
                }
                .onChange(of: proxy.size) {
                    if let previousVerseId {
                        verseId = dummyId
                        Task { @MainActor in
                            verseId = previousVerseId
                        }
                    }
                }
            }
        }
    }
}

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

Animation

Используя LazyVStack, я могу выполнить часть 3 задачи, где я могу выбрать любой стих для прокрутки. Однако часть вращения по-прежнему работает с ошибками и дает разные результаты. Как вы упомянули, это можно улучшить, используя вместо этого VStack. К сожалению, на самом деле это работало менее надежно. Кроме того, я попытался интегрировать вторую часть проблемы, передав значение verseId в надежде, что представление начнется со стиха с этим идентификатором. Это тоже не сработало.

Ali 14.07.2024 13:22

Я обновил исходный вопрос, включив в него код, который более точно представляет мою ситуацию и может повлиять на ваше решение. Спасибо!

Ali 14.07.2024 13:34

@Ali Ваши комментарии об использовании VStack заставили меня задуматься. Ответ исправлен.

Benzy Neez 14.07.2024 16:13

Думаю, я наполовину понимаю, в чем проблема. Когда я пытаюсь использовать ваше представление в качестве основного представления приложения, оно работает, как и ожидалось. Однако, когда я заключаю его в NavigationStack, кажется, что он работает неправильно. Вместо этого, когда устройство вращается, оно прокручивается к случайной части изображения, но когда оно поворачивается назад, оно возвращается в правильное положение. Представление, находящееся внутри NavigationStack, может быть причиной того, что другие решения, которые я пробовал, также не работают. Хотя не знаю, как это обойти.

Ali 14.07.2024 18:35

@Ali Пример обновлен, чтобы работать, когда GeometryReader окружен NavigationStack. Размер экрана обновляется более одного раза, поэтому важно не сохранять фиктивное значение идентификатора в обработчике .onChange. Это просто требует дополнительной проверки: if let newVal, newVal != dummyId

Benzy Neez 14.07.2024 19:57

Большое спасибо! Это сработало ОТЛИЧНО, но я обнаружил, что использование LazyVStack вместо VStack вокруг ForEach значительно повышает производительность. Кроме того, во второй части проблемы я попытался установить для verseId начальное значение в инициализаторе представления, но это ничего не дает. Знаете ли вы, как я мог бы это сделать?

Ali 14.07.2024 22:30

@Ali Рад слышать, что он работает лучше. Чтобы установить исходное положение, попробуйте установитьverseId в .onAppear. Пс. Если вас устраивает решение, возможно, вы захотите принять ответ, поставив галочку ;)

Benzy Neez 14.07.2024 22:33

Это отлично работает, но не могли бы вы объяснить, почему это не работает, когда я устанавливаю verseId внутри init, но работает внутри .onAppear.

Ali 14.07.2024 23:10

@Ali Применяются те же правила, что и для ScrollViewReader- см. примечание, помеченное как важное в документации . Пс. Спасибо, что приняли ответ!

Benzy Neez 14.07.2024 23:17

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