Повний посібник з потоку даних у SwiftUI
Навчіться керувати своїми даними у потрібних місцях у SwiftUI
Що таке дані у SwiftUI?
Щоразу при зміні даних подання необхідно оновлювати. View залежить від даних. У нашому випадку: подання встановлює пряму залежність від певних даних/певного джерела даних .
При створенні нової вистави спочатку запитайте себе:
- Які дані потрібні цьому уявленню?
- Як дані взаємодіятимуть із поданням? Наприклад, чи відображає він лише дані або також вимагає доступу до запису (маніпулювання даними)?
- Звідки уявлення отримує дані?
Є два способи надати джерело правди у SwiftUI:
1. View-local
Запитайте себе: чи будуть дані використовуватися тільки в цьому уявленні і в цьому уявленні?
І тут розумно зберегти ці дані локально при цьому представлення. Приклад:
2. Надається ззовні
Запитайте себе: чи він поділяє ті самі дані з іншими уявленнями?
Краще ініціалізувати ці дані в їхньому спільному предку, якщо це так, щоб вам не потрібно було ініціалізувати дві копії тих самих даних. Керуючи даними в загальному предку, і SwiftUI буде знати, як оновлювати ваші дочірні уявлення, які залежать від даних при їх зміні.
Якщо дані використовуються разом із братами та сестрами уявлення, підніміть ініціалізацію до їхнього спільного предка.
Способи встановлення залежності даних для ваших уявлень
Зважаючи на наведені вище міркування, давайте розглянемо кілька способів вказати SwiftUI на пошук змін даних та автоматичне керування оновленнями уявлень.
@Стан
Створює нове джерело істини, яке належить вашій точці зору. Чи не ділиться з іншими джерелами істини.
@State належить і управляється SwiftUI. Він знає, як підтримувати стан за кількох оновлень.Оскільки ви створюєте нове джерело істини, вам завжди потрібно вказувати значення за умовчанням під час створення екземпляра стану.
@ Прив'язка
Створює прив'язку до джерела істини і не створює окрему копію джерела істини, але надає доступ до будь-якого джерела істини.
У результаті:
- Використовується @State для тимчасових даних, що належать представленню. Інакше кажучи, стан локально уявлення.
- Використовується @Binding для зміни даних, що належать до іншого представлення.
Інші міркування при виборі способу надання цінності:
Вам потрібен доступ для читання або для читання-записи ?
Якщо доступ лише для читання, достатньо швидкої властивості або об'єкта середовища.
var isPresent = false
@Binding var isPresent
Досліджуванийоб'єкт це протокол, який може бути прийнятий лише типами посилань (класами).
Тип об'єкта з видавцем, який випромінює перед зміною об'єкта.
Використовуйте @Published для позначення будь-яких властивостей, які повинні керуватися автоматично.
З ObservableObject вам надається видавець, викликаний objectWillChange з коробки. Ви можете створити власного видавця. Об'єкти, які приймають, ObservableObject створюють нове джерело істини і вчать SwiftUI реагувати на його зміни.
Ви встановлюєте залежність між вашими даними та вашим поданням. SwiftUI відстежує зміни ваших даних і автоматично показує правильне уявлення ваших даних, використовуючи цю залежність.
Як тільки у вас є об'єкт, що спостерігається, ви можете встановити залежність вашого представлення і моделі даних за допомогою одного з трьох методів:
1. @ObservedObject
Використовується для анотування властивостей вашого уявлення, що містить тип, що відповідає ObservableObject. У @State на відміну від SwiftUI не керує об'єктами, що спостерігаються за вас. Ви повинні керувати його життєвим циклом.
2. @StateObject (нове в iOS14)
SwiftUI володіє файлом ObservableObject , тому його створення та знищення прив'язані до життєвого циклу уявлення. SwiftUI зберігає об'єкт живим протягом усього життєвого циклу уявлення. Об'єкт створюється перед першим викликом тіла.
Це приносить користь. Оскільки SwiftUI управляє життєвим циклом об'єкта за вас, об'єкти, позначені як об'єкти, @StateObject виділяються та звільняються відповідно до життєвого циклу уявлення, а не виділяються та звільняються щоразу, коли уявлення набуває нового значення .
Наприклад, якщо наш @ObservedObject створюється на рівні подання, а подання візуалізується як дочірнє подання у поданні контейнера; потім, кожного разу, коли наше уявлення змінюється, створюється нове значення уявлення, а об'єкт, що спостерігається, відтворюється з новим значенням уявлення. Це означає, що наш об'єкт (в даному випадку store) скидається при кожній зміні BookList. У нас є два варіанти вирішення цієї проблеми:
- Створіть джерело істини в іншому місці, щоб життєвий цикл об'єкта не був прив'язаний до значення уявлення .
- Якщо ви підтримуєте iOS 14, ви можете помітити його,@StateObjectі SwiftUI буде досить розумний, щоб пов'язати його життєвий цикл із життєвим циклом уявлення, а не з його цінністю .
Корисно, коли ви хочете отримати доступ до ObservableObject в глибині ієрархії уявлення, тому що передача об'єкта через кілька рівнів скрутна. Подання можуть бути введені в середу через view.environmentObject(YourObjectInstance)
EnvironmentObject є модифікатором уявлення та оболонкою властивостей.
Життєві цикли
SwiftUI переглянути кращі практики будівництва:
У SwiftUI перегляди дешеві; таким чином, ви повинні спробувати розбити великі уявлення на безліч невеликих уявлень. Не тільки в SwiftUI, ви також повинні дотримуватися цього правила в більшості декларативних фреймворків інтерфейсу користувача .
2. Зробіть тіло чистою функцією
Тіло уявлення має бути чистою функцією: уникати побічних ефектів. Ви повинні описати свою думку та повернутися. Жодної диспетчеризації, жодної іншої роботи, створіть кілька переглядів і поверніться.
3. Уникайте припущень.
Оскільки SwiftUI робить багато речей автоматично, він оптимізований для розумної роботи. Таким чином , не покладайтеся і не структуруйте свій код, спираючись на припущення про те, коли і як часто викликається тіло .
Останнє та важке питання, на яке потрібно відповісти: звідки беруться дані?
1. Перенести дані до спільного предка
Якщо подання A має подання B і подання C, A є загальним предком B і C.Якщо B і C повинні мати доступ на запис до одного і того ж фрагмента даних, краще створити і зберегти дані в A, потім передайте посилання на доступ для запису B і C. Таким чином, потоком даних буде легше керувати.
2. Використовуйте @StateObject
Використовуйте @StateObject щоб дозволити SwiftUI автоматично керувати життєвим циклом об'єкта, що спостерігається, вам не потрібно писати для цього додатковий код. Вам більше не потрібно турбуватися про відтворення або скидання об'єкта зі значенням уявлення.
3. Розгляньте можливість розміщення глобальних даних у додатку.
В iOS 14 нам вказано App де все додаток може бути написано на SwiftUI. З наявними інструментами керування станом, що надаються SwiftUI, це здається чудовим місцем для зберігання нашого глобального стану.
Ми розглянули, що вам слід враховувати при підключенні даних до ваших уявлень у SwiftUI. Ми дізналися кілька факторів, які впливатимуть на те, де ви вирішите розмістити код ініціалізації даних. Коли справа доходить до того, як ви керуєте своїми даними, не існує єдиної правильної відповіді.Однак деякі методи більш ефективні та безболісні, якщо дозволити SwiftUI виконувати свою роботу.
Дякую за читання, і я сподіваюся, що ви краще розумієте, як уникнути замикань і можете швидше писати асинхронні функції.
Ми завжди шукаємо талановитих та захоплених Swift-розробників! Завітайте до нашого розділу письменників та дізнайтесь, як ви можете поділитись своїми знаннями із спільнотою Next Level Swift!
Як працювати з даними у SwiftUI
Так що в цій статті ми розглянемо практичне застосування існуючих обгорток для роботи з даними SwiftUI.
Основний момент: якщо змінюється значення всередині змінної будь-якої із зазначених обгорток, і ця змінна використовується всередині змінної body, - змінна body буде обчислена повторно, таким чином View оновиться (перемальовується).
Отже, першою обгорткою є State.
@State
Виходячи з документації Apple State, необхідно використовувати для зберігання даних конкретної View. Тут Apple радить @State зберігати саме UI стан, а не бізнес логіку. "Manage transient UI state locally within a view by wrapping value types as State properties."
Більше того, дуже важливо працювати з цією змінною тільки всередині View, де вона була оголошена, причому тільки зі змінної body, або з методів, які викликаються зі змінної body. Тому завжди варто @State оголошувати приватною змінною.
"Ви повинні тільки користуватися державним майном від всередині view's body, або від методів названі ним. Для цієї умови, зазначте ваші статки власників як приватний, щоб забезпечити клієнтів з вашого перегляду від accessing them. any thread."
Наведу простий приклад використання @State, він штучний (якраз View-лічильник зазвичай корисно ініціалізувати значеннями ззовні), але з ним простіше грати.
import SwiftUI struct SimplestState: View
< @State private var counter = 0
var body: some
View
< VStack(Alignment: .center, spacing: 20) < Text("counter: \(counter)") .font(.title) HStack(spacing: 20) < Button("Increase") < counter += 1 >
Button("Decrease") < counter = max(0, counter - 1) > .disabled(counter == 0) > > > >
struct SimplestState_Previews: PreviewProvider
< static var previews: some
View
< SimplestState() > >
Хотів би звернути увагу на ще одну річ. Зберігати в @State варто лише прості типи (Int, String, Bool) ну або в крайньому випадку – структури, але в жодному разі не класи. Чому? Ну просто тому, що з класами State не працюватиме.
Про це побічно згадується у документації фразою "Коли статеві зміни змінюються, відображення нездатності його appearance і recomputes the body." Підозрюю, що якби був протокол, який обмежує, що його може реалізувати тільки структура, але не клас - дана обгортка б його використала (за аналогією з AnyObject для класів).
Якщо ми використовуємо @State для структури, то будь-яка зміна будь-якого її поля за фактом "під капотом" призведе до створення нової копії структури, яка перепише значення State змінної, що викличе перемальовку View.
У разі ж класу цього не станеться. Але не варто мені вірити на слово, show me the code!
import SwiftUI struct Datas < //class Datas var int: Int
var string: String
internal init(int: Int, string: String) < self.int = int self.string = string > > struct StateExample: View
< @State private var datas = Datas(int: 1, string: "String") var body: some
View
< VStack(Alignment: .center, spacing: 14) < Text("datas int: \(datas.int)") Button("Change int") < datas.int += 1 >
Text("datas string: \(datas.string)") Button("Change string") < datas.string += "." > > > >
struct StateExample_Previews: PreviewProvider
< static var previews: some
View
< StateExample() > >
Натискання на кнопки призводить до очікуваних змін тільки якщо Datas - struct, якщо ж Datas зробити class - зміна полів класу не призводить до зміни всієї змінної datas, і код, що стоїть за propertyWrapper @State, не підхопить зміну об'єкта і не виконає повторне обчислення змінної body.
Таким чином використовуємо @State:
- для того, щоб зберігати внутрішній стан View, який не потрібно зберігати між сесіями роботи програми.
Наприклад у View, яка є кастомною кнопкою, можна додати прапор натиснута кнопка чи ні. Маючи на увазі, що якщо ми створюємо кнопку – вона завжди у значенні за умовчанням. Або для id активний елемент списку. Зазвичай при заході на екран зі списком жоден елемент не виділено, і це не зовні.
- так само дуже часто @State використовують як чернетку, під час прототипування екрану, щоб згодом перенести змінну на той же ObservedObject.
@Binding
З @State начебто все зрозуміло, але як передавати значення змінних оголошених як State внутрішнім компонентам, щоб діти могли їх змінювати і щоб зміни відображалися й у батьківській View? (Ми ж за модульну структуру).
Для цього і була створена обгортка @Binding, щоб встановити двосторонній зв'язок у плані даних стану View між батьківським View та її дитиною.
Заглянемо в документацію, або щоб прискорити процес, можна скористатися інструментом Jump to Definition у Xcode. Якщо навести курсор миші над словом @State в Xcode, натиснути на клавіатурі ⌃ + ⌘ і натиснути ліву кнопку (за фактом виконати Jump to Definition), ми побачимо документацію по State прям всередині Xcode. Взагалі раджу частіше заглядати в документацію.
Завдяки цій документації можна переконатися, що projectedValue має тип Binding (додаткову інформацію про projectedValue можна отримати з попередньої статті)
У наведеному нижче прикладі read/write доступ мають як parent view (BindingExample), вона відображає ім'я і може його обнулити за допомогою кнопки Reset, так і child view ChildTextControl, яка делегує зміну імені в системний контроль TextField
import SwiftUI struct BindingExample: View
< @State private var name = ""
var body: some
View
< VStack(Alignment: .center, spacing: 14) < Text("Current name: \(name)") Button("Reset") < name = "" >.disabled(Name.count == 0) ChildTextControl(text: $name) Spacer() >.padding([.top, .bottom]) > >
struct ChildTextControl: View
< @Binding var text: String
var body: some
View
< TextField("Enter name", Text: $text) .padding() > >
struct BindingExample_Previews: PreviewProvider
< static var previews: some
View
< BindingExample() > >
У прикладі можна побачити, що ChildTextController очікує в конструкторі @Binding var text: String а ми передаємо $name .
У документації можна дізнатися, що підставляючи символ $ перед State змінною ми фактично отримуємо projectedValue у State, а State projectedValue має тип Binding , тобто. $name і є Binding
І знову таки застосувавши Jump to Definition вже на @Binding ми можемо побачити, що конструктор структури Binding як параметри приймає замикання get/set
public init(get: @escaping () ->
Value, Set: @escaping (Value) ->
Void)
Таким чином, Binding - це засіб, який дозволяє працювати з value типами як з reference. Адже якщо ми передамо Int з однієї View в іншу - відбудеться її копіювання і зміни в parent не відбиватимуться в child, втім як і навпаки. А Binding - це фасад, який приховує що під капотом він несе не самі дані, а функції, що дозволяють читати/писати в оригінальне сховище даних, де лежить той же наш Int.
Також з документації можна побачити, що Binding можна ініціалізувати константою, і корисно це при використанні Preview
public static func constant(_ value: Value) ->
BindingValue>
Наприклад, Preview до нашого компоненту ChildTextControl міг би виглядати так:
struct ChildTextControl_Previews: PreviewProvider
< static var previews: some
View
< ChildTextControl(Text: .constant("test")) > >
Ну і якщо @State змінна зберігає якусь структуру зі своїми полями (як наведена вище struct Datas), то можна передавати не всю її дитині а тільки потрібне поле за допомогою того ж $ із зазначенням потрібного поля
ChildTextControl(text: $datas.string)
Таким чином область застосування @Binding:
- передати доступ змінних позначених як @State / @Published всередину дочірніх компонентів
Протокол ObservableObject
Обертки @StateObject, @EnvironmentObject, @ObservedObject будуть працювати з класами, що реалізують протокол ObservableObject, ось його визначення
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public protocol ObservableObject : AnyObject
< /// The type of publisher that emits before the object has changed.
поєднаноготипу ObjectWillChangePublisher : Publisher = ObservableObjectPublisher
where
Self.ObjectWillChangePublisher.Failure == Never
/// A publisher that emits before the object has changed.
var objectWillChange: Self.ObjectWillChangePublisher
< get > >
З визначення протоколу видно, що його можуть лише класи, насправді класи реалізують цей протокол будуть повноцінними контейнерами для даних, зберігати дані бізнес логіки, а чи не внутрішній стан View (як @State).
І працювати можна буде з кожною властивістю окремо за рахунок того, що у кожної властивості ми проставлятимемо @Published, що під капотом використовує фреймворк Combine. Як зазначено в документації, для ObservableObject визначено властивість objectWillChange, яка генеруватиме подію перед кожну зміну властивостей позначені як @Published.
І якби ми використовували ObservableObject безпосередньо, то код міг би виглядати якось так:
import UIKit import Combine var cancellables = SetAnyCancellable>() class Product: ObservableObject
< @Published var title: String
@Published var price: Double
init(title: String, price: Double) < self.title = title self.price = price > func increasePrice(by value: Double) ->
Double < price += value return price > > let product = Product(title: "Phone", price: 100.00) product.objectWillChange .sink
< _ in
print("price'(product.price)' will change") > .store(in: &cancellables) print(product.increasePrice(by: 50))
Це виведе в консоль 100, а не 150, на те й willChange а не didChange
price '100.0' will change 150.0
Ми також можемо за допомогою Combine підписатися і на зміну конкретного поля
product.$price.sink < value in
print("direct $price access: price '(product.price)' will change") >.store(in: &cancellables)
Якщо вставити цей код перед print(product.), то консолі буде
direct $price access: price '100.0' will change price '100.0' will change direct $price access: price '100.0' will change 150.0
т.к. на момент підписки на $price там вже було значення - ми відразу його отримуємо, а наступний print з product.$price.sink відпрацює вже під час збільшення price. Переконатися в цьому можна закоментувавши print(product.increasePrice(by: 50)) таким чином нехай ми взагалі не змінюємо об'єкт product - все одно передплата product.$price.sink відпрацює, надрукувавши при цьому в консоль один рядок:
direct $price access: price '100.0' will change
У принципі нам ніхто не забороняє в класі, що реалізує ObservableObject самим генерувати нові події в objectWillChange (хоча за таймером) при необхідності.
Але ми в основному зіштовхуватимемося з ObservableObject саме у зв'язці з SwiftUI, і ми будемо безпосередньо працювати з властивостями об'єктів, що реалізують ObservableObject, а події з objectWillChange буде перехоплювати сам SwiftUI для того, щоб знати, що треба перемалювати View. Очевидно для оптимізації ефективності генерується willSet, а чи не didSet, т.к.це дозволяє накопичувати зміни та оновлювати View рідше.
Що ж, розглянемо першу обгортку, що працює з ObservableObject - ObservedObject.
@ObservedObject
Документація досить небагатослівна, і каже, що @ObservedObject це обгортка, яка підписується на об'єкт ObservableObject і оновлює View, коли дані об'єкта змінюються. По суті це міст між даними, що зберігаються в ObservableObject і нашим View.
До речі, при виході SwiftUI нам була доступна тільки ця обгортка, @StateObject була додана пізніше (у так званому SwiftUI 2.0, представленому на WWDC 2020), давайте на цьому прикладі зрозуміємо чому довелося додати ще одну обгортку.
import SwiftUI class AppState: ObservableObject
< @Published var counter: Int = 0 >
struct ObservedObjectExample: View
< @State private var mainCounter = 0
var body: some
View
< VStack(Alignment: .center, spacing: 20) < Text("Main counter: \(mainCounter)") .font(.title) Button("Increase main") < mainCounter += 1 >
Divider().padding() CounterView() >.padding() > >
struct CounterView: View
< @ObservedObject var appState = AppState() var body: some
View
< VStack(Alignment: .center, spacing: 20) < Text("Child counter: (appState.counter)") .font(.title) HStack(spacing: 20) < Button("Increase main") <appState.counter += 1 >
Button("Decrease") <appState.counter = max(0, appState.counter - 1) > .disabled(appState.counter == 0) > > > >
struct ObservedObjectExample_Previews: PreviewProvider
< static var previews: some
View
< ObservedObjectExample() > >
У View верхнього рівня ObservedObjectExample ми відображаємо як @State лічильник mainCounter (у Text), так і дитину CounterView, у якої всередині створюється змінна типу AppState із зазначеною обгорткою @ObservedObject
На перший погляд, проблем немає ніяких, ми можемо натиснути кнопку "Increase main" для Main counter і це чесно збільшить @State private var mainCounter , оновивши ObservedObjectExample.Ми можемо натиснути Increase/Decrease для дитини CounterView, що змінить дані у змінній AppState та SwiftUI чесно перемалює CounterView. Де каверза?
А каверза в тому, що якщо ще раз натиснути "Increase main" для Main counter, лічильник Child counter обнулиться.
Чому так сталося? Насправді все просто при натисканні на "Increase main" змінюється mainCounter, що викликає перерахунок змінної body для ObservedObjectExample, що в свою чергу викликає перестворення CounterView. А при повторному створенні CounterView у нас знову ініціалізується змінна appState з лічильником counter = 0. Якщо не очікувати на це - несподіване перестворення змінної може стати дуже неприємним сюрпризом.
Таким чином, сферою застосування @ObservedObject можна було б вважати:
- якщо вам потрібно, щоб відбувалося перестворення стану. Хоча важко уявити навіщо таке може бути потрібне, т.к. одна справа коли це очікувана поведінка (перехід зі списку List в DetailView), а інша справа - приклад, коли дочірній компонент несподівано скидає свій стан.
- якщо ви впевнені, що змінна ніколи не буде знищена, створюємо, наприклад, на старті програми всередині того ж WindowGroup
Але як буде пояснено у розділі з @StateObject, навіть це не варто робити.
Залишається одне застосування @ObservedObject:
- якщо вам потрібно вже створену змінну класу реалізує ObservableObject передати вниз по ієрархії.
@StateObject
Після WWDC 2020 нам додали ще одну обгортку - @StateObject, рятівника від перестворення змінної при перемальовці View.
Для того, щоб полагодити баг зі скиданням стану дочірньої View при збільшенні батьківського лічильника - достатньо в CounterView змінити @ObservedObject на @StateObject.
Повернемося до теорії, в документації для @StateObject написано "A property wrapper type that instantiates an observable object.", в той час як для @ObservedObject "A property wrapper type that subscribes to an observable object and invalidates a view whenver the observable object changes."
Таким чином Apple прямо говорить - не варто створювати самим ObservedObject всередині View, яке буде його використовувати, створювати потрібно тільки StateObject, а вже в дочірніх View визначати змінну як @ObservedObject.
Простежити шлях appState і переконатися, що всі причетні View працюють з одним і тим самим об'єктом appState можна в цьому прикладі
import SwiftUI class AppState: ObservableObject
< @Published var counter: Int = 0 >
struct StateObjectTestView: View
< @StateObject private var appState = AppState() var body: some
View
< NavigationView
< List
< Section(header: Text("Observed Object")) < NavigationLink( destination: NestedObservedObjectExample(state: appState), label: < Text("Nested ObservedObject Example") >) > > > > >
struct NestedObservedObjectExample: View
< @ObservedObject var state: AppState
var body: some
View
< VStack(Alignment: .center, spacing: 20) < Text("Main counter: \(state.counter)") .font(.title) Button("Increase main") <state.counter += 1 >
Divider().padding() NestedCounterView(appState: state)>.padding() > >
struct NestedCounterView: View
< @ObservedObject var appState: AppState
var body: some
View
< VStack(Alignment: .center, spacing: 20) < Text("Child counter: (appState.counter)") .font(.title) HStack(spacing: 20) < Button("Increase") <appState.counter += 1 >
Button("Decrease") <appState.counter = max(0, appState.counter - 1) > .disabled(appState.counter == 0) > > > >
struct StateObjectTestView_Previews: PreviewProvider
< static var previews: some
View
< StateObjectTestView() > >
Видно, що в кореневому View (а можна було і в WindowGroup) ми визначаємо змінну як @StateObject, а у всіх дочірніх як @ObservedObject, і де ми не змінювали appState.counter - дані будуть оновлені у всіх View.
Важливо, що @StateObject прив'язується до конкретного екземпляра View, немає типу View.
Область застосування @StateObject:
- створювати об'єкти з даними всередині View, будучи впевненим, що вони не обнулиться під час перемальовування цього View. І це важливий момент, якщо перетвориться батьківська View (повернулися по навігаційному стеку назад і зайшли знову на батьківську View наприклад), то вже перетворяться і дочірні View, відповідно перетворяться і дочірні змінні, хоч вони і @StateObject. Тобто. не варто думати, що якщо ми помітили змінну як @StateObject, то вона взагалі ніколи не перетвориться, це працює тільки в рамках перестворення всередині body батьківської View.
@EnvironmentObject
Фактично Dependency Injection з коробки. Насправді мало чим відрізняється від ObservedObject, у тому сенсі, що parent створює змінну, а дочірня View отримує до цієї змінної доступ. Різниця в тому, що прикріплюючи цю змінну до батьківського View - усі діти, і діти дітей тощо отримують до неї доступ. Особливо це зручно, якщо ця змінна потрібна лише на нижніх рівнях, т.к. немає необхідності прокидати через всі View посередники. Існує підводний камінь, який не завжди зрозуміло, чи буде передано навколишнійоб'єкт для дочірнього перегляду, створеного за допомогою alert, sheet, navigationLink. На WWDC 2021 розробники apple рекомендували для цих випадків проставляти вручну envirionmentObject
Original Question: I've had several intermittent crashes з objects environment being nil when I pass them to a sheet or NavigationLink. Це tricky to replicate due to being intermittent і я зазвичай працює architecting my code differently to avoid passing environment objects. Do you know of reasons this might happen? All I can think of is that the views that originate the environmentObject further up the view hierarchy are being taken out of memory. Thanks for any help you can provide!
Answer (engineer #1): NavigationLink by design doesn’t flow EnvironmentObjects через його destination as it’s unclear where the environmentObject should be inherited from.I suspect this might what’s causing your issue. У порядку, щоб отримати ваше розуміння, ви повинні explicitly pass the environmentObject through at that point.
Answer (engineer #2):You can also apply the environmentObject to NavigationView itself, which will make it available to all pushed content.
struct ContentView: View
< @StateObject private var appState = AppState() var body: some
View
< NavigationView
< List
< . Section(header: Text("Environment Object")) < NavigationLink( destination: EnvironmentObjectExample() .environmentObject(appState), label: < Text("EnvironmentObject Example") >) > . >
import SwiftUI struct EnvironmentObjectExample: View
< var body: some
View
< VStack
< Text("Main") Child1EnvironmentObjectExample() > > >
struct Child1EnvironmentObjectExample: View
< var body: some
View
< VStack
< Text("Child1") Child2EnvironmentObjectExample() > > >
struct Child2EnvironmentObjectExample: View
< @EnvironmentObject var appState: AppState
var body: some
View
< VStack
< Text("Child2") Text("Environment counter: (appState.counter)") .font(.title) Button("Increase main") <appState.counter += 1 > > > >
З прикладу можна отримати таке:
- щоб проставити environmentObject потрібно для кореневої View проставити .environmentObject()
- отримувати доступ можна в будь-якій дочірній View на будь-якій глибині за допомогою
@EnvironmentObject var varName: Type
- створювати об'єкти для environment так само варто за допомогою @StateObject. Але треба бути дуже обережним, т.к. якщо ми забули проставити .environmentObject(object) - при спробі відкрити View, що використовує відсутній EnvironmentObject - вилетить Fatal error.
Також природним обмеженням і те, що можна покласти в Environment лише одне об'єкт певного типу конкретної ієрархії View, т.к. доступ іде за типом, а чи не по імені (.environmentObject())
Область застосування @EnvironmentObject:
- якщо об'єкт використовується дітьми View і потрібен всього 1 об'єкт даного типу, особливо якщо об'єкт потрібен не всім View в ланцюжку створення, а десь глибоко внизу в ланцюжку створених View, щоб не передавати постійно ObservedObject явно від батька до дитини, якщо більшості View це все І це не потрібно.
Про інші обгортки коротко:
Є також обгортки для окремих випадків. Докладно я їх не розбиратиму, просто згадаю
- @AppStorage, по суті проксі для UserDefaults
- @FetchRequest дозволяє отримати доступ до CoreData об'єктів з View
- @SceneStorage, в основному використовується для state restoration, щоб після перезапуску програми ми могли показати додаток у тому вигляді, в якому він знаходився перед закриттям (становище, позиція всередині екрану і т.д.)
