Вот что я хочу сделать:
Однако по какой-то причине, хотя я обновляю переменную состояния, она не обновляется при передаче в следующее представление.
Вот пример кода, который показывает проблему:
struct NumberView: View {
@State var number: Int = 1
@State private var showNumber = false
var body: some View {
NavigationStack {
VStack(spacing: 40) {
// Text("\(number)")
Button {
number = 99
print(number)
} label: {
Text("Change Number")
}
Button {
showNumber = true
} label: {
Text("Show Number")
}
}
.fullScreenCover(isPresented: $showNumber) {
SomeView(number: number)
}
}
}
}
struct SomeView: View {
let number: Int
var body: some View {
Text("\(number)")
}
}
Если вы нажмете «Изменить номер», локальное состояние обновится до 99. Но когда я создаю другое представление и передаю его в качестве параметра, оно показывает 1 вместо 99. Что происходит?
Некоторые вещи, которые следует отметить:
Text("\(number)")
, это сработает. Но это не должно быть необходимо ИМО.SomeView
использовать привязку. Но для моего приложения это не сработает. Мой фактический вариант использования — это представление «выбрать параметры игры». Затем я создам представление игры без SwiftUI и хочу передать эти параметры в качестве параметров. Итак, я не могу иметь привязки по всему игровому коду только из-за этой ошибки. Я хочу просто зафиксировать то, что вводит пользователь, и создать объект Parameters с этими данными.navigationDestination
вместо fullScreenCover
. ¯\(ツ)/¯ понятия не имею об этом...Вы имеете в виду let number: Int внутри SomeView? Я не думаю, что это проблема, потому что если вы раскомментируете Text("\(number)"), это сработает. На момент создания этого представления число должно быть 99. И это представление создается после того, как мы обновим число (проверено с помощью отладчика).
Но этот текст находится в другом представлении и не имеет отношения к моему комментарию. В любом случае, это интересный момент в отношении того, что вы видите в отладчике, и я должен сказать, что понять жизненный цикл представления в SwiftUI непросто.
Представление — это структура, поэтому его свойства неизменяемы, поэтому представление не может изменять свои собственные свойства. Вот почему изменение свойства с именем number внутри тела представления требует, чтобы это свойство было аннотировано с помощью оболочки свойства @State. Благодаря Swift и SwiftUI прозрачные обратные вызовы чтения и записи позволяют изменить отображаемое значение. Таким образом, вы не должны передавать number в качестве параметра SomeView() при вызове fullScreenCover(), а передавать ссылку на number, чтобы обратные вызовы вызывались систематически: $number. Поскольку вы больше не передаете целое число для построения структуры SomeView, тип свойства с именем number в этой структуре больше не может быть целым числом, а должен быть ссылкой на целое число (а именно привязку): используйте аннотацию @Binding для этот.
Итак, замените SomeView(number: number) на SomeView(number: $number) и let number: Int на @Binding var number: Int, чтобы выполнить задание.
Вот правильный исходный код:
import SwiftUI
struct NumberView: View {
@State var number: Int = 1
@State private var showNumber = false
var body: some View {
NavigationStack {
VStack(spacing: 40) {
// Text("\(number)")
Button {
number = 99
print(number)
} label: {
Text("Change Number")
}
Button {
showNumber = true
} label: {
Text("Show Number")
}
}
.fullScreenCover(isPresented: $showNumber) {
SomeView(number: $number)
}
}
}
}
struct SomeView: View {
@Binding var number: Int
var body: some View {
Text("\(number)")
}
}
После всего сказанного, чтобы получить действительный исходный код, есть небольшой трюк, который до сих пор не объяснялся: если вы просто замените в своем исходном коде Text("Change Number") на Text("Change Number \(number)"), не используя $ ссылку или @Binding ключевые слова, вы увидите, что проблема также автоматически решается! Не нужно использовать @binding в SomeView! Это связано с тем, что SwiftUI оптимизирует построение дерева представлений. Если он знает, что отображаемое представление изменилось (не только его свойства), оно вычислит представление с обновленными значениями @State. Добавление number к метке кнопки заставляет SwiftUI отслеживать изменения свойства состояния number, и теперь он обновляет свое кэшированное значение для отображения метки кнопки Text, поэтому это новое значение будет правильно использоваться для создания SomeView. Все это может показаться странным, но это просто связано с оптимизацией в SwiftUI. Apple не полностью объясняет, как она реализует оптимизации, строя дерево представлений, есть некоторая информация, предоставленная во время мероприятий WWDC, но исходный код не открыт. Поэтому вам нужно строго следовать шаблону проектирования, основанному на @State и @Binding, чтобы быть уверенным, что все это работает так, как должно.
Все это еще раз говорит о том, что Apple говорит, что вам не нужно использовать @Binding для передачи значения в дочернее представление, если это дочернее представление хочет только получить доступ к значению: поделитесь состоянием с любыми дочерними представлениями, которым также нужен доступ , либо непосредственно для доступа только для чтения, либо в качестве привязки для доступа для чтения и записи (https://developer.apple.com/documentation/swiftui/state). Это правильно, но Apple говорит в той же статье, что вам нужно поместить [state] в самое высокое представление в иерархии представлений, которому требуется доступ к значению. В Apple необходимость доступа к значению означает, что оно нужно для отображения представления, а не только для выполнения других вычислений, которые не влияют на экран. Именно эта интерпретация позволяет Apple оптимизировать вычисление свойства состояния, когда ему необходимо обновить NumberView, например, при вычислении содержимого строки Text("Change Number \(number)"). Вы можете найти это действительно сложно. Но есть способ это понять: возьмите исходный код, который вы написали, уберите @State перед var number: Int = 1. Чтобы скомпилировать его, вам нужно переместить эту строку изнутри структуры наружу, например, в самой первой строке вашего исходного файла, сразу после объявления импорта. И вы увидите, что это работает! Это связано с тем, что вам не нужно это значение для отображения NumberView. И, таким образом, совершенно законно поставить значение выше, чтобы построить представление с именем SomeView. Будьте осторожны, здесь вы не хотите обновлять SomeView, поэтому нет граничных эффектов. Но это не сработает, если вам придется обновить SomeView.
Вот код для этого последнего трюка:
import SwiftUI
// number is declared outside the views!
var number: Int = 1
struct NumberView: View {
// no more state variable named number!
// No more modification: the following code is exactly yours!
@State private var showNumber = false
var body: some View {
NavigationStack {
VStack(spacing: 40) {
// Text("\(number)")
Button {
number = 99
print(number)
} label: {
Text("Change Number")
}
Button {
showNumber = true
} label: {
Text("Show Number")
}
}
.fullScreenCover(isPresented: $showNumber) {
SomeView(number: number)
}
}
}
}
struct SomeView: View {
let number: Int
var body: some View {
Text("\(number)")
}
}
Вот почему вы обязательно должны следовать шаблону проектирования @State и @Binding, принимая во внимание, что если вы объявляете состояние в представлении, которое не использует его для отображения своего содержимого, вы должны объявить это состояние как @Binding в дочерних представлениях, даже если этим детям не нужно вносить изменения в это состояние. Лучший способ использовать @State — это объявить его в самом высоком представлении, которое нуждается в нем для отображения чего-либо: никогда не забывайте, что @State должно быть объявлено в представлении, которому принадлежит эта переменная; создание представления, которому принадлежит переменная, но которое не должно использовать ее для отображения своего содержимого, является антишаблоном.
Да, я так понимаю, что это работает с привязкой. Но да, у меня сложилось впечатление, что вы должны использовать Binding только в том случае, если вам нужно отредактировать состояние. Использование Binding для каждой отдельной переменной SwiftUI в вашем приложении похоже на дырявый шаблон.
Кроме того, что, если мне нужно передать переменную чему-то, что не является представлением SwiftUI? В моем реальном приложении я передаю эти значения в GameScene (не SwiftUI) и не хочу использовать привязки в своем коде, отличном от SwiftUI... собираюсь провести дополнительное исследование. Я уверен, что Apple рекомендовала использовать непривязки, когда вам не нужно редактировать значение.
Да, посмотрите это руководство от Apple: developer.apple.com/tutorials/app-dev-training/…. Если вы ищете MeetingFooterView, они не используют Binding здесь. Это «немного» отличается, но, похоже, это рекомендуемый шаблон. И я начинаю думать, что это баг, что не работает в данном случае?
Да, хорошо... Я имею в виду, я понимаю твой ответ. И я думаю, что это правильный ответ ... но мне все еще кажется, что это ошибка ... кажется, что если вы обновите состояние, а затем прочитаете состояние, оно должно иметь последнее значение. И мне действительно не нравится идея сделать все изменяемым в моем приложении, когда оно может быть неизменным. Я думаю, мне придется сесть с этим и попытаться выяснить, какими должны быть лучшие практики для SwiftUI...
Apple сказала бы, что «это не ошибка, это функция», но я согласен с отсутствием документации от Apple и поведением, которое трудно предсказать. То, является ли это ошибкой или функцией, зависит от того, что могли бы сказать эксперты Apple во время многих выступлений на WWDC: это то место, где стоит документация.
Да, кажется, все это связано с тем, что fullScreenCover использует экранирующее закрытие, которое обычно кажется антипаттерном в SwiftUI (спасибо malhal за ссылку на эту статью: rensbr.eu/blog/swiftui-escaping-closures). Мне, безусловно, было бы интересно услышать от инженеров Apple, почему они решили избежать этого.
Поскольку number не читается в теле, отслеживание зависимостей SwiftUI обнаруживает его. Вы можете дать ему толчок следующим образом:
.fullScreenCover(isPresented: $showNumber) { [number] in
Теперь новое замыкание будет создаваться с обновленным значением number при каждом изменении number. К вашему сведению, синтаксис [number] in называется «список захвата», читайте об этом здесь.
Ах да. Это похоже на то, что сказал мне Натан Таннар (см. мой собственный ответ). Другой способ сделать это — случайно разбросать где-нибудь let _ = number, но мне больше нравится ваше решение, так как оно является локальным для этого кода.
@plivesey К сожалению, код в вашем ответе вызовет другие проблемы. Замыкания ViewBuilder, предоставляемые представлениям, должны оцениваться в инициализации, а не в теле. Вы можете узнать почему здесь: rensbr.eu/blog/swiftui-escaping-closes
Я немного запутался... насколько я понимаю, смысл кода в моем ответе - удалить ускользающие замыкания. В настоящее время реализация fullScreenCover от Apple использует экранирующее закрытие, но вы можете избежать этого с помощью структуры FullScreenModifier (или, по крайней мере, скрыть ее).
Натан Таннар дал мне это объяснение по другому каналу, и я думаю, что он доходит до сути моей проблемы. Похоже, что это странность SwiftUI, вызванная знанием того, когда и как он обновляет представления в зависимости от состояния. Спасибо Натан!
Это потому, что число не «читается» в теле представления. SwiftUI умен тем, что запускает обновления представления только при изменении зависимости представления. Причина, по которой это вызывает проблемы с модификатором fullScreenCover, заключается в том, что он захватывает замыкание @escaping для тела. Это означает, что он не будет прочитан, пока не будет представлена обложка. Поскольку не прочитанное тело представления не будет переоценено при изменении @State, вы можете проверить это, установив точку останова в теле представления. Поскольку тело представления не оценивается повторно, закрытие @escaping никогда не захватывается повторно и, следовательно, будет содержать копию исходного значения.
В качестве примечания вы обнаружите, что как только вы представите обложку в первый раз, а затем закроете ее, последующие презентации будут обновляться правильно. Возможно, это похоже на ошибку SwiftUI, fullScreenCover, вероятно, не должен быть @escaping. Вы можете обойти это, прочитав число внутри тела или обернув модификатор чем-то вроде этого, поскольку здесь пункт назначения не захвачен @escaping, поэтому число будет прочитано в оценке тела представления.
struct FullScreenModifier<Destination: View>: ViewModifier {
@Binding var isPresented: Bool
@ViewBuilder var destination: Destination
func body(content: Content) -> some View {
content
.fullScreenCover(isPresented: $isPresented) {
destination
}
}
}
Может ли быть так, что поскольку он let объявлен, SwiftUI решает, что ему не нужно перерисовывать представление?