В этом руководстве мы научимся планировать UI приложения при помощи View и узнаем, как использовать переменные состояния (State variables) для модификации UI.

Примерное время чтения публикации: 25 минут.

SwiftUI позволяет нам полностью забыть про Interface Builder (IB) и storyboard'ы. IB и Xcode были самостоятельными приложениями до Xcode 4, но «стыковка» между ними видна до сих пор, когда мы редактируем имя IBAction или IBOutlet и наше приложение крашится, так IB ничего не знает про изменения в коде. Или когда мы задаём идентификаторы для segues или для ячеек таблиц, но Xcode не может их проверить, так как они строковые.

SwiftUI спешит на помощь! Мы сразу же видим изменения во View, как только вводим код. Изменения на одной стороне приводят к обновлению на другой, так что они всегда связаны. Нет никаких строковых идентификаторов, в которых можно было бы ошибиться. И это всё код, но его гораздо меньше, чем если бы мы писали его с использованием UIKit, так что его легче понимать, редактировать и отлаживать. Скажите, разве же это не прекрасно?

Поехали


Начнём новый проект в Xcode project (Shift-Command-N), выберем iOS ? Single View App, назовём RGBullsEye, и обязательно выберем в качестве интерфейса SwiftUI.

Теперь AppDelegate.swift разбит на два файла: AppDelegate.swift и SceneDelegate.swift, и SceneDelegate содержит window:



SceneDelegate почти не имеет отношения к SwiftUI, за исключением этой строчки:

window.rootViewController = UIHostingController(rootView: ContentView())

UIHostingController создаёт вьюконтроллер для SwiftUI-view ContentView.
Замечание: UIHostingController позволяет нам интегрировать SwiftUI-view в уже существующее приложение. Добавляем Hosting View Controller в наш storyboard и создаем segue на него из UIViewController. Затем используем Control-drag с segue на код вьюконтроллера чтобы создать IBSegueAction, где мы задаём у хостинг контроллера значение rootView — SwiftUI-view.
При старте приложения окно отображает экземпляр ContentView, который определён в файле ContentView.swift. Это struct, который соответствует протоколу View:

struct ContentView: View {
  var body: some View {
    Text("Hello World")
  }
}

Это SwiftUI-объявление содержимого ContentView (body). Там сейчас Text-view с текстом Hello World.

Чуть ниже ContentView_Previews возвращает экземпляр ContentView.

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

Здесь мы можем задавать тестовые данные для превью. Но где именно находится это самое превью?

Сразу за кодом — большое пустое пространство вот с этим сверху:



Кликните Resume, подождите немного и…



Набросаем наш UI


Кое-чего привычного не видно — это файла Main.storyboard. Мы собираемся создать наш UI при помощи SwiftUI, прямо в коде, в процессе посмотривая на превью: что там у нас получается? Но не волнуйтесь, нам не придется писать сотни строк кода, чтобы создать наши view.

SwiftUI декларативен: вы заявляете, как должен выглядеть ваш UI, а SwiftUI преобразует всё это в эффективный код, выполняющий всю работу. Apple позволяет создавать столько view, сколько необходимо, чтобы код был простым и понятным. Особенно рекомендованы повторно используемые view с параметрами — это похоже на выделение кода в отдельную функцию. Чуть позже вы сделаете это сами.

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

Заменим Text(«Hello World») вот этим:

Text("Target Color Block")

Если необходимо, кликните Resume чтобы обновить превью.

Теперь сделайте Command-Click на этой view в превью, и выберите Embed in HStack:



Обратите внимание, что ваш код также изменился:

HStack {
  Text("Target Color Block")
}

Скопипастите оператор Text, и отредактируйте его внутри нашего HStack. Обратите внимание: мы не разделяем операторы запятой, а пишем каждый из них на новой строке::

HStack {
  Text("Target Color Block")
  Text("Guess Color Block")
}

А так это выглядит в превью:



Теперь подготовим место для заглушек слайдеров, поместив HStack в VStack. На этот раз сделаем Command-Click на HStack в нашем коде:



Выберите Embed in VStack; появится новый код, но превью не изменится. Чуть позже мы добавим view под будущими цветными блоками.

Добавьте новую строку сразу после HStack, нажмите + на панели инструментов, чтобы открыть библиотеку и перетащите Vertical Stack на новую строку:



Как и следовало ожидать, и код и превью изменились:



Закончим работу над наброском UI, чтобы всё выглядело так:

VStack {
  HStack {
    Text("Target Color Block")
    Text("Guess Color Block")
  }

  Text("Hit me button")

  VStack {
    Text("Red slider")
    Text("Green slider")
    Text("Blue slider")
  }
}

В новом VStack чуть позже появятся три слайдера, а также кнопка между слайдерами и цветными блоками.

Продолжаем работу над UI


Теперь попрактикуемся в SwiftUI для заполнения HStack, содержащего цветные блоки:

HStack {
  // Target color block
  VStack {
    Rectangle()
    Text("Match this color")
  }
  // Guess color block
  VStack {
    Rectangle()
    HStack {
      Text("R: xxx")
      Text("G: xxx")
      Text("B: xxx")
    }
  }
}

У каждого цветового блока есть Rectangle. У целевого (Target) цветового блока есть один Text view под прямоугольником, а у подбираемого (Guess) — три Text view. Чуть позже мы заменим 'xxx' на реальные значения слайдеров.

Использование переменных '@State'


В SwiftUI мы можем использовать обычные переменные, но, если мы хотим, чтобы изменение переменной влияло на UI, тогда мы помечаем такие переменные как '@State'. В нашем приложении мы подбираем цвет, так что все переменные, которые влияют на подбираемый цвет, являются '@State' переменными.

Добавьте эти строчки внутри struct ContentView, перед объявлением body:

let rTarget = Double.random(in: 0..<1)
let gTarget = Double.random(in: 0..<1)
let bTarget = Double.random(in: 0..<1)
@State var rGuess: Double
@State var gGuess: Double
@State var bGuess: Double

Значения R, G и B лежат в пределах между 0 и 1. Мы инициализируем искомые значения случайными величинами. Мы могли бы также инициализировать подбираемые значения величиной 0.5, но оставим их пока неициализированными, чтобы показать, что в таком случае нужно сделать.

Опустимся чуть ниже к struct ContentView_Previews, которая инициализирует экземпляр ContentView для превью. Теперь инициалайзеру нужны первоначальные значения подбираемых величин. Изменим ContentView() таким образом:

ContentView(rGuess: 0.5, gGuess: 0.5, bGuess: 0.5)

Когда мы сделаем слайдеры, на превью их значения будут в середине.

Мы должны также исправить инициалайзер в SceneDelegate, в функции scene(_:willConnectTo:options:) — замените ContentView() следующим:

window.rootViewController = UIHostingController(rootView:
  ContentView(rGuess: 0.5, gGuess: 0.5, bGuess: 0.5))

Когда приложение загрузится, указатели слайдеров будут в центре.

Теперь добавим модификатор цвета к искомому (target) прямоугольнику:

Rectangle()
  .foregroundColor(Color(red: rTarget, green: gTarget, blue: bTarget, opacity: 1.0))

Модификатор .foregroundColor создает новый Rectangle view с цветом, заданным случайно сгенерированными значениями RGB.

Похожим образом модифицируем подбираемый (guess) прямоугольник:

Rectangle()
  .foregroundColor(Color(red: rGuess, green: gGuess, blue: bGuess, opacity: 1.0))

Со значениями R, G и B по 0.5 мы получим серый цвет.

Нажмите Resume и слегка подождите.



Делаем view повторно используемыми


Сначала не будем думать о повторном использовании и просто сделаем слайдер для красного цвета. В VStack для слайдеров заменим заглушку Text(«Red slider») вот таким HStack:

HStack {
  Text("0")
    .foregroundColor(.red)
  Slider(value: $rGuess)
  Text("255")
    .foregroundColor(.red)
}

Мы сделали цвет текста в текстовых view красным. И добавили Slider со значением по умолчанию. Диапазон слайдера по умолчанию от 0 до 1, это как то, что нас вполне устраивает.
Замечание: Мы-то знаем, что слайдер ходит от 0 до 1, а текстовая метка '255' для удобства пользователей, которые привыкли представлять RGB значения в диапазоне от 0 до 255.
Но что за значок $ у переменной? Мы знаем про ? и ! при работе с optionals, а теперь ещё и $?

Несмотря на то, что он такой маленький и незаметный, он очень важен. Сам по себе rGuess — это просто значение, только на чтение. А вот $rGuess — это биндинг, он нам нужен, чтобы обновлять прямоугольник подбираемого цвета, когда пользователь перемещает слайдер.

Чтобы понять разницу, установим значения для трёх текстовых View под угадываемым прямоугольником:

HStack {
  Text("R: \(Int(rGuess * 255.0))")
  Text("G: \(Int(gGuess * 255.0))")
  Text("B: \(Int(bGuess * 255.0))")
}

Здесь мы только используем значения, не изменяем их, поэтому префикс $ нам не нужен.

Дождитесь обновления превью:



Цветные прямоугольники слегка ужались, чтобы поместился слайдер. Но текстовы метки слайдера выглядят неаккуратно — они слишком прижаты к краям. Давайте добавим HStack еще один модификатор — отступ (padding):

HStack {
  Text("0")
    .foregroundColor(.red)
  Slider(value: $rGuess)
  Text("255")
    .foregroundColor(.red)
}
  .padding()

Теперь гораздо лучше!



Сделаем Command-Click на HStack красного слайдера, and и выберем Extract Subview:



Это работает так же, как Refactor ? Extract to Function, но для SwiftUI view.

В этот момент появятся несколько сообщений об ошибках, не переживайте, сейчас мы всё исправим.

Назовите полученный view ColorSlider и добавьте этот код вверху, перед body нашего нового view:

@Binding var value: Double
var textColor: Color

Теперь заменим $rGuess на $value, а .red на textColor:

Text("0")
  .foregroundColor(textColor)
Slider(value: $value)
Text("255")
  .foregroundColor(textColor)

Вернёмся к определению ColorSlider() в VStack и добавим наши параметры:

ColorSlider(value: $rGuess, textColor: .red)

Убедитесь, что на превью все в порядке с красным слайдером и замените текстовые заглушки на слайдеры зеленого и синего цвета. Не забудьте вставить там правильные параметры:

ColorSlider(value: $gGuess, textColor: .green)
ColorSlider(value: $bGuess, textColor: .blue)

Нажмите Resume, чтобы обновить превью:


Замечание: вы могли обратить внимание, что вам часто приходится нажимать Resume. Если вы любите шорткаты, вам наверняка понравится сочетание Option-Command-P.
А теперь кое-что приятное! В нижнем правом углу превью нажмите кнопку live preview:



Live preview позволяет нам взаимодействовать с превью, как если бы приложение работало на симуляторе!

Попробуйте подвигать слайдеры:



Замечательно! Переходим к заключительному этапу. Мы ведь хотим знать, насколько хорошо мы подобрали цвет?

Показываем Alert


После выставления слайдеров в нужные позиции пользователь жмёт кнопу Hit Me, после чего появляется Alert c оценкой.

Сначала добавим в ContentView метод для вычисления оценки. Между переменными '@State' и
body добавим этот метод:

func computeScore() -> Int {
  let rDiff = rGuess - rTarget
  let gDiff = gGuess - gTarget
  let bDiff = bGuess - bTarget
  let diff = sqrt(rDiff * rDiff + gDiff * gDiff + bDiff * bDiff)
  return Int((1.0 - diff) * 100.0 + 0.5)
}

Величина diff — это просто расстояние между двумя точками в трёхмерном пространстве, то есть значение ошибки пользователя. Для получения оценки вычитаем diff из 1, а затем приводим ее значение к диапазону 0 — 100. Чем меньше diff, тем выше оценка.

Затем замените заглушку Text(«Hit me button») этим кодом:

Button(action: {

}) {
  Text("Hit Me!")
}

У Button есть action и label, как у UIButton. Мы хотим, чтобы action вызвал появление Alert view. Но, если мы создадим Alert в action кнопки, то ничего не произойдёт.

Вместо этого мы сделаем Alert частью ContentView, и добавим '@State' переменную типа Bool. Затем мы установим эту переменную в true, в том месте, где мы хотим, чтобы появился наш Alert — в action кнопки. Значение сбросится в false — чтобы спрятать alert — когда пользователь скроет Alert.

Добавим эту '@State' переменную:

@State var showAlert = false

Затем добавим этот код как action кнопки:

self.showAlert = true

self нам необходим, так как showAlert находится внутри замыкания.

Наконец добавим модификатор alert к кнопке, так что наша кнопка полностью выглядит так:

Button(action: {
  self.showAlert = true
}) {
  Text("Hit Me!")
}
.alert(isPresented: $showAlert) {
  Alert(title: Text("Your Score"), message: Text("\(computeScore())"))
}

Мы передаем $showAlert как биндинг, так как значение этой переменной изменится в тот момент, когда пользователь спрячет alert, и это изменение приведет к обновлению view.

У SwiftUI есть простой инициалайзер для Alert view. В нем по умолчанию есть кнопка OK, так что нам даже не потребуется задавать ее в качестве параметра.

Включите live preview, подвигайте слайдеры и нажмите кнопку Hit me. Вуаля!



Теперь с live preview вам особо больше не нужен iOS симулятор. Хотя с ним вы можете протестировать ваше приложение в горизонтальном режиме:



Заключение


Здесь вы можете загрузить готовый проект публикации.

Это руководство только слегка затронуло SwiftUI, но теперь у вас есть впечатление о новых возможностях Xcode для создания UI и получения превью, а также о том, как использовать переменные '@State' для обновления вашего UI.

Для простоты мы не создавали модель данных для RGB. Но большинство приложений создают модели своих данных, используя структуры или классы. Если вы хотите в SwiftUI отслеживать изменения в модели, то она должна соответствовать протоколу BindableObject и реализовывать свойство didChange, которое извещает об изменениях. Изучите примеры Apple и особенно Data Flow Through SwiftUI.

Чтобы облегчить понимание SwiftUI, вы можете добавить SwiftUI view к уже существующему приложению. Смотрите здесь примеры того, как быстро и легко это сделать.

Наконец, изучайте документацию к SwiftUI, а там действительно много полезного!

Комментарии (4)


  1. svanichkin
    23.10.2019 11:54

    После «Сделаем Command-Click на HStack красного слайдера, and и выберем Extract Subview:» пример идет не правильный. Либо конкретно напишите перед каким body надо код вставлять…


    1. infund Автор
      23.10.2019 12:19

      Что-то не совсем понял. Делаем на HStack красного слайдера (который был создан на предыдущем этапе) Command-Click. Всё вроде ясно. Скриншот может слегка по-другому выглядеть, это да, так как оригинал писался еще под бету XCode. Но суть одна — жмём на Extract Subview.

      P.S. А, речь идет о новых переменных. Ок, я уточню.


      1. svanichkin
        23.10.2019 12:20
        +1

        ну вобщем нужно уточнить под каким body нужно добавить строчки
        @Binding var value: Double
        var textColor: Color

        я например вписал их под первым body… пока понял свою ошибку успел два раза расстроиться )


        1. infund Автор
          23.10.2019 12:24

          *Fixed