У меня есть ScrollView
, содержащий ForEach
, перебирающий массив элементов типа Verse
. Каждое представление внутри ForEach
представляет собой VStack
с текстом на арабском и текстом на английском языке.
Текст на английском языке виден только тогда, когда readingMode
стоит false
. Текст на арабском языке выравнивается по центру, когда readingMode
равен false
.
Есть 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)
}
}
}
Я бы предложил использовать .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
работает, если представление прокручивается между переключателями, но не при переключении обратно без прокрутки.
Используя LazyVStack
, я могу выполнить часть 3 задачи, где я могу выбрать любой стих для прокрутки. Однако часть вращения по-прежнему работает с ошибками и дает разные результаты. Как вы упомянули, это можно улучшить, используя вместо этого VStack
. К сожалению, на самом деле это работало менее надежно. Кроме того, я попытался интегрировать вторую часть проблемы, передав значение verseId
в надежде, что представление начнется со стиха с этим идентификатором. Это тоже не сработало.
Я обновил исходный вопрос, включив в него код, который более точно представляет мою ситуацию и может повлиять на ваше решение. Спасибо!
@Ali Ваши комментарии об использовании VStack
заставили меня задуматься. Ответ исправлен.
Думаю, я наполовину понимаю, в чем проблема. Когда я пытаюсь использовать ваше представление в качестве основного представления приложения, оно работает, как и ожидалось. Однако, когда я заключаю его в NavigationStack
, кажется, что он работает неправильно. Вместо этого, когда устройство вращается, оно прокручивается к случайной части изображения, но когда оно поворачивается назад, оно возвращается в правильное положение. Представление, находящееся внутри NavigationStack
, может быть причиной того, что другие решения, которые я пробовал, также не работают. Хотя не знаю, как это обойти.
@Ali Пример обновлен, чтобы работать, когда GeometryReader
окружен NavigationStack
. Размер экрана обновляется более одного раза, поэтому важно не сохранять фиктивное значение идентификатора в обработчике .onChange
. Это просто требует дополнительной проверки: if let newVal, newVal != dummyId
Большое спасибо! Это сработало ОТЛИЧНО, но я обнаружил, что использование LazyVStack
вместо VStack
вокруг ForEach
значительно повышает производительность. Кроме того, во второй части проблемы я попытался установить для verseId
начальное значение в инициализаторе представления, но это ничего не дает. Знаете ли вы, как я мог бы это сделать?
@Ali Рад слышать, что он работает лучше. Чтобы установить исходное положение, попробуйте установитьverseId
в .onAppear
. Пс. Если вас устраивает решение, возможно, вы захотите принять ответ, поставив галочку ;)
Это отлично работает, но не могли бы вы объяснить, почему это не работает, когда я устанавливаю verseId внутри init
, но работает внутри .onAppear
.
@Ali Применяются те же правила, что и для ScrollViewReader
- см. примечание, помеченное как важное в документации . Пс. Спасибо, что приняли ответ!
Добро пожаловать в Stack Overflow! Пожалуйста, прочитайте Как создать минимальный воспроизводимый пример. , затем отредактируйте свой вопрос.