Это не волшебство, хотя кажется
Привет! Чтобы уточнить, эта статья посвящена свойству @State
(уникальному для SwiftUI), а не общей концепции состояния. Они связаны, но само состояние - это всего лишь идея, а свойство @State
- это реальный, видимый и осязаемый объект. Кроме того, он очень универсален, почти как по волшебству ...
Фон
Если вы работали со SwiftUI, скорее всего, вы однажды создали свойство и пытались изменить его значение только для того, чтобы получить эту странную ошибку: Невозможно присвоить свойству:« self неизменяемо».
Выше pressedButton
- это var
, поэтому его значение является изменяемым (изменяемым), но в сообщении об ошибке говорится, что «self» неизменяемо!
Итак, pressedButton
изменчив, а self
- нет? Давайте найдем сообщение об ошибке.
Hacking With Swift говорит нам, что если вы хотите изменить значение свойства во время работы вашей программы, вы должны отметить это с помощью @State
. Хорошо, посмотрим, работает ли ...
Это было легко исправить! Но как он избавился от ошибки «self
неизменяемый» - сделал ли self
изменчивым?
Ответ оказался более увлекательным (и сложным * вздох *), чем я ожидал. Еще только на прошлой неделе я думал, что @State
свойств используются исключительно для связывания с элементами пользовательского интерфейса, например:
Здесь скругленный прямоугольник обращается к @State var rectangleIsGreen
и автоматически обновляется при изменении значения этого свойства:
Итак, свойства @State
используются для синхронизации с пользовательским интерфейсом… но какое отношение это имеет к исправлению ошибки «self
неизменяемо»? @State
что-то вроде универсального волшебного порошка? Не совсем …
Исследовать
Похоже, что @State
свойства могут делать две вещи:
- Исправьте ошибку: «Невозможно присвоить свойству:
self
является неизменным». - Синхронизируйте с пользовательским интерфейсом и обновляйте его автоматически.
Это сбивает с толку, так что давайте проведем небольшое исследование!
Способность №1 | Исправьте ошибку: «Невозможно назначить свойству:« self »является неизменным».
Помните, что в Hacking With Swift сказано, что если вы хотите изменить значение свойства во время выполнения программы, вы должны пометить его с помощью State
. Хорошо, давайте вернемся к руководству и прочтем объяснение:
«Представления SwiftUI должны быть структурами, что означает, что они неизменяемы по умолчанию. Если бы это был наш собственный код, мы могли бы помечать методы с помощью
mutating
, чтобы сообщить Swift, что они изменят значения, но мы не можем сделать это в SwiftUI, потому что он использует вычисляемое свойство ».
Конечно, имеет смысл… в некотором роде. Вот краткое изложение:
- Представления SwiftUI - это структуры
- Структуры по умолчанию неизменяемы (поэтому значения внутри также неизменны)
- Методы (функции), отмеченные знаком
mutating
, могут изменять значения свойств. - Но это запрещено в SwiftUI, потому что он использует вычисляемое свойство
Это довольно информативно, и теперь мы знаем, что ошибка каким-то образом связана со структурами, но мы все еще не знаем, почему отметка свойства с помощью @State
исправляет ошибку. Давайте взглянем на другую способность @State
, прежде чем погрузиться в подробности.
Способность №2 | Синхронизация с пользовательским интерфейсом и автоматическое обновление
Как синхронизировать части пользовательского интерфейса со свойствами @State
, рассматривается практически в каждом руководстве по SwiftUI, например в этом. Давай проверим.
То, что мы ищем, находится в третьем абзаце (в руководстве выше) ...
«… Если мы добавим специальный атрибут
@State
перед [properties], SwiftUI будет автоматически отслеживать изменения и обновлять любые части наших представлений, которые используют это состояние».
И, как и в обычном программировании UIKit, чтобы использовать это состояние, то есть получить доступ к значению свойства (в данном случае логическому), просто укажите имя свойства:
struct ContentView: View { @State var rectangleIsGreen = false var body: some View { RoundedRectangle(cornerRadius: 5) .fill(rectangleIsGreen ? Color.green : Color.blue) } }
Здесь мы используем тернарный оператор для определения цвета прямоугольника на основе значения свойства rectangleIsGreen
. Мы можем изменить это так:
rectangleIsGreen = true
Подождите - это звучит знакомо. Разве наша ошибка «self
неизменяемо» не возникала, когда мы пытались изменить значение свойства, и исчезла, когда мы пометили ее с помощью @State
? Интересно, что в @State
должно быть что-то, что позволяет структурам быть изменяемыми… или нет. Оказывается, есть еще много чего, что скрыто в @State
. Погрузимся еще глубже!
Глубокое погружение
Эксперимент: Оставаться СУХИМ
Начнем с этого кода:
В приведенном выше фрагменте у нас есть два скругленных прямоугольника и кнопка в HStack
. Они синхронизируются со свойством @State
, rectangleIsGreen
. В настоящее время мы являемся полной противоположностью Don’t Repeat Yourself (DRY): вместо DRY мы пишем все дважды (WET) 🤣. В любом случае, вот результат:
Но независимо от того, СУХИМ мы или ВЛАЖНЫМ, всегда полезно разбить код на более мелкие компоненты - он станет многоразовым и более легким для чтения.
Допустим, мы хотим переместить код для прямоугольника с закругленными углами - из нашей исходной ContentView
в его собственную структуру, которую мы назовем ColorView
(опять же, представления SwiftUI всегда должны быть структурами).
Прямоугольник с закругленными углами в его собственной структуре все равно должен будет синхронизироваться со значением rectangleIsGreen
из ContentView.
. Но если мы просто передадим ссылку на него из ContentView
в ColorView
, это будет нарушением правил Apple - в официальной документации говорится, что:
«Вы должны получать доступ к свойству состояния только изнутри тела представления ... объявляйте свои свойства состояния как частные, чтобы предотвратить доступ клиентов вашего представления к нему».
Это означает, что в нашем случае rectangleIsGreen
не должен быть доступен из ColorView
. Но нам нужно получить к нему доступ! Хм …
Правильный способ доступа к значению родительского свойства из дочерней структуры (в данном случае ColorView
) - создать двустороннее соединение. Мы можем синхронизировать два отдельных свойства, по одному в каждой структуре, с одним и тем же значением - добавив свойство @Binding
в ColorView
.
Свойство @State
в ContentView
и свойстве @Binding
будет указывать на одно и то же значение, которое обе стороны могут читать и записывать.
Подождите, что? @Binding
? Мы даже не закончили покрывать @State
! Но не волнуйтесь - @Binding
очень легко понять! Но давайте сначала напишем код для ColorView
.
- Создайте новую структуру с именем
ColorView
. - Внутри
ColorView
добавьте код прямоугольника с закругленными углами. - Затем снова внутри
ColorView
создайте свойство@Binding
с именемcolorRectangleIsGreen
(имя не имеет значения и может быть таким же, как свойствоrectangleIsGreen
вContentView
, но я добавил к немуcolor
, чтобы его было легче различать).
Пока мы предоставляем тип (в данном случае логическое), свойство @Binding
не требует значения по умолчанию - структуры автоматически имеют неявные инициализаторы.
Теперь код ColorView
должен выглядеть так:
Теперь, когда мы закончили создание структуры для прямоугольника с закругленными углами, вернувшись в ContentView
, мы можем ...
- Удалите код для двух прямоугольников с закругленными углами.
- Замените их двумя инициализированными
ColorView
.
Удалить легко, поэтому сначала удалите прямоугольники с закругленными углами.
Но тогда как нам создать экземпляр ColorView
s? Нам нужно указать, что rectangleIsGreen
должен синхронизироваться с colorRectangleIsGreen
. Давайте попробуем использовать автозаполнение Xcode, чтобы увидеть, что нам нужно.
Ага! Похоже, что ColorView
ожидает Binding<Bool>
для своего свойства colorRectangleIsGreen
. Теперь единственное, что можно заполнить, - это свойство @State
, то есть rectangleIsGreen
. Давай попробуем!
Но подождите ... произошла ошибка.
Ошибка говорит, что rectangleIsGreen
- это Bool
, а не Binding<Bool>
. Однако компилятор предлагает исправление: просто добавьте $
. Ok…
Итак… это сработало! Но что такое $
?
Краткий ответ: это автоматически созданное свойство. В SwiftUI свойство, помеченное знаком @State
(или @Binding
… я объясню в конце этой статьи), не является просто одним свойством. Это три свойства!
Какие? Три свойства ?!
По сути, когда мы впервые записали свойство rectangleIsGreen
, компилятор Xcode автоматически преобразовал его в три свойства. Они невидимы (содержатся в @State, который является оболочкой свойств) - вы их не видите, но они будут работать, когда вы на них ссылаетесь. Затем Xcode удалил исходное свойство rectangleIsGreen
@State
, но вы все еще можете его увидеть.
Хорошо ... это много информации. Я объясню каждое свойство индивидуально. Начнем с первого.
1. '_rectangleIsGreen'
По сути, это само свойство @State
. Как видите, его начальное значение - это значение, которое мы впервые ввели - false
.
Другие автоматически сгенерированные свойства получат свои значения из этого свойства, потому что ниже это свойство включает:
wrappedValue
, логическое значениеprojectedValue
, значениеBinding
, используемое для синхронизации с другими представлениями… звучит знакомо.
Кроме того, вы можете использовать это свойство, если вам нужно вручную написать блок инициализации, что иногда нужно делать в Swift Playgrounds.
@State var rectangleIsGreen: Bool init(rectangleIsGreen: State<Bool>) { self._rectangleIsGreen = rectangleIsGreen }
2. '$rectangleIsGreen'
Видите {} s? Это означает, что это вычисляемое свойство, что также означает, что оно вычисляет значение, которое оно возвращает.
Но подождите ... не требуется ли вычисляемым свойствам get
(для возврата значения), а иногда и set
(для изменения других свойств)?
var computedProperty: Bool { get { return true } // set is optional }
Ну да, но set
не является обязательным - и если у вас его нет, вы можете полностью опустить get
.
var computedProperty: Bool { // anything inside here is the get! return true }
Вы можете сделать его даже более Swifter (или более ленивым… Я думаю, что предпочитаю Swifter), даже не написав return
.
var computedProperty: Bool { true }
В любом случае, $rectangleIsGreen
получает projectedValue
из _rectangleIsGreen
и возвращает его. Еще раз, projectedValue
предназначен для синхронизации с представлениями. Вот почему нам пришлось использовать $rectangleIsGreen
, а не rectangleIsGreen
при инициализации ColorView
. Ага!
3. 'rectangleIsGreen'
Это кажется простейшим свойством, поскольку перед ним нет подчеркивания или знака доллара… но на самом деле под ним скрывается некоторая логика. Видите get
и set
? Это означает, что это также вычисляемое свойство!
- Когда вы
get
свойство (ссылаетесь на него), оно возвращает значение_rectangleIsGreen
в оболочке. Помните, что_rectangleIsGreen
- это само свойство@State
, которое включает в себяwrappedValue
иprojectedValue
. Здесь нам нужноwrappedValue
, которое является простым логическим значением. - Когда вы
set
свойство, ему также необходимо обновить свойство@State
, которое равно_rectangleIsGreen
.newValue
- это специальное ключевое слово в Swift, которое просто представляет собой новое значение.
Подождите ... а что такое nonmutating
? И почему только перед set
стоит nonmutating
?
Вот как @State
исправляет ошибку «self
неизменяема».
Движение к телу
Помните, что в Hacking With Swift сказано:
«Если бы это был наш собственный код, мы могли бы помечать методы с помощью
mutating
, чтобы сообщить Swift, что они изменят значения, но мы не можем сделать это в SwiftUI, потому что он использует вычисляемое свойство».
Хорошо ... а также помните, что вычисляемые свойства - это свойства, которые производят некоторые вычисления перед возвратом значений? Что ж, посмотрите внимательно на body
- это похоже на вычисляемое свойство?
var body: some View { ... }
Ответ: Да, body
- вычисляемое свойство. В нем нет выражения return
, но фигурные скобки {} выдают его. И хотя вы не видите get
или set
, вы знаете, что все, что находится в фигурных скобках, - это get
(потому что вычисляемые свойства всегда должны иметь get
).
А в Swift внутри вычисляемого свойства структуры вы не можете изменять другие свойства. Вот почему мы получили ошибку «self
is immutable», потому что мы пытались изменить pressedButton
(в первый пример) внутри body
.
Но почему? В Swift структуры - это типы значений. Это означает, что всякий раз, когда вы меняете одну, она на самом деле не мутирует - это просто создает новую структуру с вашими изменениями и заменяет старую.
Вот почему, если вы хотите изменить структуру или ее свойство, вам нужно объявить это с помощью var
.
var contentView = ContentView()
Однако, когда вы создаете структуру, компилятор не знает, будет ли она объявлена с помощью var
или let
. И если он объявлен с let
, функции или вычисляемые свойства внутри структуры не смогут изменять другие свойства внутри той же структуры.
Вот почему вы должны пометить их mutating
, чтобы компилятор знал, что вы собираетесь вносить изменения изнутри, и не допустил, чтобы вы позже объявили struct
как let
.
Итак, чтобы обойти ошибку без использования @State
, мы могли бы добавить ключевое слово mutating
к body
в get
, например:
var body: some View { mutating get { ... } }
Но… поскольку ContentView
должен соответствовать View
, это не сработает - View
SwiftUI не позволяет get
изменяться.
И даже если SwiftUI разрешил это, мы не хотим ограничиваться созданием ContentView
в качестве переменной - помните, поскольку мы добавили атрибут mutating
, компилятор не позволит нам объявить ContentView
как let
. Это может ограничить ваш код.
Но с @State
вы можете объявить структуру с let
. Какие? Как? Здесь снова появляется атрибут nonmutating
. Это не такой уж и секретный соус для @State
.
Давайте еще раз посмотрим на свойство rectangleIsGreen
, созданное компилятором.
По умолчанию в вычисляемых свойствах get
равно nonmutating
, а set
равно mutating
… но если вы сделаете set
nonmutating
, вы сообщите компилятору, что этот не изменяет структуру, в которой он находится. Вернемся к пример pressedButton
и используйте nonmutating
там:
Здесь мы не изменяем ContentView
- мы просто изменяем свойство pressedButtonStorage
, которое является let
.
Итак, нам удалось изменить значение другого свойства из сохраненного свойства, эффективно решив нашу первоначальную проблему!
Однако с приведенным выше кодом сложно справиться. Как следует из названия, UnsafeMutablePointer
небезопасно… нам нужно позаботиться о хранении и освобождении памяти вручную. И это заняло около 10 строк кода, в то время как @State
- это всего лишь шесть символов, добавленных перед свойством!
@State var pressedButton = false ↑↑↑↑↑↑ 6 characters!
СУХОЙ эксперимент
Теперь, когда мы со всем разобрались, мы можем завершить наш эксперимент DRY. Вот последний код!
И последнее: @Binding
. Помните, я сказал, что свойства, отмеченные @State
или @Binding
, автоматически генерируют три свойства?
Единственная разница между @State
и @Binding
состоит в том, что у @Binding
должен быть родитель. Этот родитель может быть @Binding
или @State
- это не имеет значения.
У нас может быть столько дочерних структур, сколько захотим, все синхронизированные вместе, но нам нужно откуда-то получить начальный Binding<Bool>
...
- изнутри свойства
@State var rectangleIsGreen
- изнутри автоматически созданного свойства
_rectangleIsGreen
свойства@State var rectangleIsGreen
- из автоматически созданного свойства
_rectangleIsGreen
свойстваprojectedValue
свойства@State var rectangleIsGreen
.
Спасибо за прочтение!
Ресурсы
Как исправить« Невозможно присвоить свойству: «я является неизменным »
Пол Хадсон @twostraws Полностью обновлено для Xcode 11.5. Представления SwiftUI должны быть структурами, что означает, что они неизменяемы. … www.hackingwithswift.com »