Это не волшебство, хотя кажется

Привет! Чтобы уточнить, эта статья посвящена свойству @State (уникальному для SwiftUI), а не общей концепции состояния. Они связаны, но само состояние - это всего лишь идея, а свойство @State - это реальный, видимый и осязаемый объект. Кроме того, он очень универсален, почти как по волшебству ...

Фон

Если вы работали со SwiftUI, скорее всего, вы однажды создали свойство и пытались изменить его значение только для того, чтобы получить эту странную ошибку: Невозможно присвоить свойству:« self неизменяемо».

Выше pressedButton - это var, поэтому его значение является изменяемым (изменяемым), но в сообщении об ошибке говорится, что «self» неизменяемо!

Итак, pressedButton изменчив, а self - нет? Давайте найдем сообщение об ошибке.

Hacking With Swift говорит нам, что если вы хотите изменить значение свойства во время работы вашей программы, вы должны отметить это с помощью @State. Хорошо, посмотрим, работает ли ...

Это было легко исправить! Но как он избавился от ошибки «self неизменяемый» - сделал ли self изменчивым?

Ответ оказался более увлекательным (и сложным * вздох *), чем я ожидал. Еще только на прошлой неделе я думал, что @State свойств используются исключительно для связывания с элементами пользовательского интерфейса, например:

Здесь скругленный прямоугольник обращается к @State var rectangleIsGreen и автоматически обновляется при изменении значения этого свойства:

Итак, свойства @State используются для синхронизации с пользовательским интерфейсом… но какое отношение это имеет к исправлению ошибки «self неизменяемо»? @State что-то вроде универсального волшебного порошка? Не совсем …

Исследовать

Похоже, что @State свойства могут делать две вещи:

  1. Исправьте ошибку: «Невозможно присвоить свойству: self является неизменным».
  2. Синхронизируйте с пользовательским интерфейсом и обновляйте его автоматически.

Это сбивает с толку, так что давайте проведем небольшое исследование!

Способность №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.

  1. Создайте новую структуру с именем ColorView.
  2. Внутри ColorView добавьте код прямоугольника с закругленными углами.
  3. Затем снова внутри ColorView создайте свойство @Binding с именем colorRectangleIsGreen (имя не имеет значения и может быть таким же, как свойство rectangleIsGreen в ContentView, но я добавил к нему color, чтобы его было легче различать).

Пока мы предоставляем тип (в данном случае логическое), свойство @Binding не требует значения по умолчанию - структуры автоматически имеют неявные инициализаторы.

Теперь код ColorView должен выглядеть так:

Теперь, когда мы закончили создание структуры для прямоугольника с закругленными углами, вернувшись в ContentView, мы можем ...

  1. Удалите код для двух прямоугольников с закругленными углами.
  2. Замените их двумя инициализированными ColorView.

Удалить легко, поэтому сначала удалите прямоугольники с закругленными углами.

Но тогда как нам создать экземпляр ColorViews? Нам нужно указать, что 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 »