UIKit 1 / 1.6. Ещё несколько вариантов управления


− Ну и куда мы пойдем? – невысокая Аня едва успевала за быстрыми шагами Коли.
− В закат. А если честно, то ты мне только мешать будешь. Лучше бы оставалась оплакивать свою подругу. Она походу скоро умрет.
− Почему это? Рана неглубокая. Еще никто от царапин не умирал.
− Ты видела эту зеленую фигню? Мы даже не знаем, что это. А вдруг, это кровь окисляется на этом странном воздухе или туда инфекция попала?

Вокруг была только пустыня с этим странным зеленым песком. Солнце казалось синим то ли от того, что ребята устали, и от жары все меняло свои черты, то ли оттого, что оно действительно было синим. Спустя несколько часов на горизонте замаячили какие−то глыбы, как если бы на Земле это была свалка огромных коричневых валунов.

− Бинго, там наверняка должны быть растения, − обрадовался Коля.

Приблизившись к камням, ребята застыли в изумлении. Огромные коричневые глыбы были одинаковы по форме. Некоторые были большие, другие поменьше, лежали они в идеальной системе, образуя нечто наподобие Стоун Хенджа.


Ваша работа забирает очень много времени и единственный способ оставаться удовлетворенным – это делать то, что по-настоящему нравится. Вы должны любить свою работу. Если такую работу вы еще не нашли, не останавливайте поиски. Не сдавайтесь.

Стив Джобс

В этом уроке


От винта!

  1. Создайте новое приложение ControlsBasic
  2. Добавьте в центр верхней половины экрана (примерно) на раскадровку кнопку
  3. Создайте для неё действие buttonTapped(_:), как мы делали это раньше (включите редактор помощник, протяните с зажатым ctrl от кнопки в код, выберите Action и введите указанное имя)
  4. Добавьте аутлет для кнопки с названием button


Переключатели

Переключатель представляет собой объект, который может находится только в двух состояниях - включено и выключено. Думайте о нём как о рычажке. Если Вы уже работали с iOS, то видели переключатели постоянно. Добавим переключатель - специальный объект класса UISwitch.

Расположите его чуть ниже кнопки и создайте метод switchChanged в коде, как мы это делали раньше. Пусть он явно принимает объект переключателя.

Теперь сделаем так, чтобы наша кнопка работала, только если включен переключатель. Добавьте следующий код в метод switchChanged - он будет вызван при изменении состояния переключателя:

self.button.isEnabled = sender.isOn

Что же происходит в методе:

Запустите программу и нажмите по переключателю. Вы заметите, что кнопка стала неактивной. Нажмите ещё раз и кнопка включится.


Ипотека на всю жизнь

Создадим простой кредитный калькулятор:

  1. Измените название кнопки на Рассчитать (Потрясающая логика поставить кнопку выше данных - мы на этом примере научим Вас, как не делать пользовательские интерфейсы!)
  2. Добавьте ещё один лейбл с надписью Согласен на вечную кабалу перед переключателем, а его сместите вправо.
  3. Добавьте ещё два лейбла - с текстом _percent и Сумма: так, чтобы получилось, как на рисунке ниже:


Слайдер

Вы наверняка видели слайдеры уже не раз - они применимы, когда пользователь задаёт какую-то величину из большого диапазона, но не за его пределами.
В Cocoa Touch объектом слайдера выступает UISlider. Добавьте один из них на полотно напротив лейбла _percent:

  1. Теперь создайте действие слайдера sliderChanged с явным указанием сендера и аутлет slider.
  2. И создайте ещё один аутлет от лейбла _percent с названием percentLabel.
  3. Добавьте код в метод sliderChanged:
    self.percentLabel.text = "\(sender.value)%"

Он использует новое свойство value у не менее нового объекта типа UISlider, чтобы добавить значение слайдера в лейбл процентов.

В инспекторе атрибутов в разделе Slider, в графе Maximum пропишите максимальное значение - 100 (хотя мы знаем, что 100% для ипотеки это даже мало), а текущее (Value) в 13%. Почему? Потому что.



Запустите код и проверьте, что всё работает:

Мы уже заметили несколько проблем - изначально виден шаблонный лейбл _percent, а также очень некрасиво отображается значения процентов (оно округляется до дробного числа, а мы хотим до красивого целого).

Первую проблему Вы можете решить сами. Ответ: добавьте в viewDidLoad-метод код:

self.percentLabel.text = "\(self.slider.value.rounded())%"

При первой загрузке контроллера в память значение лейбла станет равным строке со значением слайдера. Зачем же мы сделали число c методом rounded(), а не просто значение слайдера? Мы хотим округлить его. Использование метода возвращает целое число, полученное из заданного на основе правила школьного округления: если дробная часть больше или равна 0.5, то округление идёт вверх, иначе вниз. Кроме того важно отметить, что данный метод называется rounded, то есть является причастием, что в методологии наименования методов означает, что метод не меняет свой объект, а возвращает новый, полученный из данного.

Внесите те же изменения в метод sliderChanged, чтобы его тело имело вид:

self.percentLabel.text = "\(sender.value.rounded())%"

Запустите код. Вы увидите, что стало так, как Вы хотели:


Текстовые поля

Текстовые поля представляют собой области для ввода текстовых данных.

  1. Добавьте текстовое поле UITextField на полотно чуть напротив лейбла Сумма:.
  2. И добавьте два лейбла: Долг: и _amount на холст ниже строчки с текстовым полем. Как Вы уже заметили, все заменяемые значения мы обозначаем через нижнее подчеркивание: Вы абсолютно не обязаны делать также. Каких-то особых требований по этому поводу в руководствах по дизайну нет.
  3. Сместите текст в лейбле Долг: по правому краю. Для этого воспользуйтесь простыми переключателями в инспекторе атрибутов в строке Alignment:

    В итоге у Вас получится что-то вроде:

Создайте метод textChanged от текстового поля, причем сделайте так, чтобы событием было Editing Changed (наступает при изменении текста), что очень мило. Наш калькулятор в реальном времени скажет, сколько Вы будете должны. Явно задайте сендер как UITextField. Добавим в метод код, который заставит изменяться размер долга. Примем здесь, что кредит выдаётся на 10 лет.

if let summ = Double(sender.text!) {
    self.amountLabel.text = "\((summ * Double(pow(self.slider.value/100 + 1, 10))).rounded())$"
} else {
    self.amountLabel.text = "Не могу рассчитать долг"
}

Что же делает код? С помощью if let он проверяет, можно ли извлечь текст и превратить в число. Для этого используется специальный проваливающийся конструктор Double, принимающий строку. Если это невозможно, то будет выведена соответствующая надпись. В противном случае будет присвоено значение долга по формуле Сумма изначальная * (Процентная ставка в форме десятичной дроби + 1)в_степени_(10).

Чтобы заранее предотвратить какие-либо ошибки, добавьте в viewDidLoad строки:

self.amountLabel.text = "0$"

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


Магические числа и несколько методов

Число 10 указано в нашем коде явно. Такие числа называются магическими - нигде не описано, что оно значит. По-хорошему такие числа объявляются как специальные констант. Находясь внутри кода в явном виде, они ухудшают его понимание. Кроме того, такие числа часто ведут к неожиданным проблемам.

По нашим правилам кредита мы не платим ничего до срока в 10 лет и проценты начисляются на текущее тело долга, которое состоит из изначального долга и процентов.

Функция pow(_:_:) возводит число (первый аргумент) в степень (второй аргумент). Она является частью Foundation, который включен в UIKit. Отметим, что она возвращает Float, а не Double в данном случае и нужно явное преобразование в число типа Double.


Создание алерта

Алерт - это всплывающее уведомление. В данном случае мы рассмотрим его вариант в виде модального окна, то есть окна, которое появляется в центре экрана и предлагает несколько действий на выбор.

Мы хотим дать пользователю возможность подписать договор с Люцифером по нажатию кнопки рассчитать.
Для этого последовательно будем добавлять код в метод buttonTapped:

let alert = UIAlertController(title: "Договор с Люцифером", message: "Вы хотите подписать договор на продажу собственной души?", preferredStyle: .alert)

Для вывода алерта нужно создать специальный контроллер типа UIAlertController. В данном случае используется конструктор init(title:message:preferredStyle:), где первым аргументом выступает заголовок окна, заметьте, что Вы можете не передавать ни заголовок, ни следующее сообщение message, тогда будут отображаться только действия. Третий аргумент принимает в себя предпочитаемый тип контроллера - объект типа UIAlertControllerStyle, который на момент написания данной книги имеет два кейса - .alert и .actionSheet. Кейс .alert представляет собой модальное окно, которое нам и нужно по сути. Обратите внимание на два аспекта:

  1. Только что созданный алерт-контроллер ничего не делает и не отображается
  2. Алерт-контроллер нельзя унаследовать и модифицировать в подклассах - это создано в соответствии с гайдлайнами Apple по дизайну уведомлений

Теперь создадим действие на подписание договора:

let action = UIAlertAction(title: "Подписать", style: .default) {(alertAction) in self.view.backgroundColor = #colorLiteral(red: 1, green: 0, blue: 0.009946824187, alpha: 1)}

Для этого мы создали объект класса UIAlertAction через конструктор:

init(title: String?, style: UIAlertActionStyle, handler: ((UIAlertAction) -> Void)? = nil)

Тут первый аргумент - заголовок действия, второй - вид действия, который задаётся перечислением UIAlertActionStyle, которое содержит три кейса:

Последним аргументом конструктора выступает обработчик (handler), вызываемый при нажатии на действие. Оно представляет собой необязательное замыкание, мы же использовали его в форме волочащегося замыкания (которое пишется после закрывающих скобок функции с аргументами). В нашем случае оно меняет цвет фона на противно красный.

Теперь создадим действие, которое предложить пользователю отменить вызов алерта без каких-либо последствий:

let cancelAction = UIAlertAction(title: "Отменить", style: .cancel, handler: nil)

Для этого мы использовали стиль .cancel и обработчик задали в nil. Однако мы могли не указывать явно, так как это аргумент-по-умолчанию, однако мы сделали это для наглядности. Впредь так делать не будем.

Теперь надо добавить действия в контроллер:

alert.addAction(action)
alert.addAction(cancelAction)

Для этого используется метод addAction(_:) у алерт-контроллера. Действия будут отображаться в том порядке, в котором они были переданы в контроллер (справа налево).

Напоследок выведем контроллер:

self.present(alert, animated: true, completion: nil)

Мы использовали следующий метод нашего контроллера представления:

present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil)
  1. Первым аргументом выступает контроллер представления (так как контроллер алертов есть наследник данного типа, то мы можем передать его в качестве аргумента).
  2. Вторым - флаг того, анимировано ли появление контроллера (под флагом часто называется булева величина).
  3. Последним необязательное замыкание, вызываемое по завершении выполнения появления контроллера.

Запустите приложение и нажмите кнопку рассчитать - Вам предложат продать свою душу. Если Вы согласитесь, то задний фон контроллера будет окрашен в красный.


Жесты и их распознавание

Каждый элемент управления взаимодействует с пользователем через жесты: кнопки и переключатели работают с касаниями (тапами или taps), а слайдеры со свайпами (swipes, движение по перемещению чего-либо без отрыва пальца от поверхности). Жест связывается с одним видом, но вид может иметь несколько жестов. Например, переключатель отвечает на тапы, но пользователь также может использовать свайпы. Вы можете добавить любой собственный жест на любой вид.

Найдите жест касания Tap Gesture Recognizer (из библиотеки объектов) и добавьте его на полотно. Он появится в Document Outline:

Теперь перейдите в его свойства - Вы можете изменить число нажатий (tap) и касаний touch, нужных для его использования.

Создайте действие для жеста с названием, например, respondToTapGesture. В типе отправителя явно задайте UITapGestureRecognizer - класс распознания этого типа жестов. Этот класс является наследником класса UIGestureRecognizer, отвечающего за распознавание жестов.

Давайте убедимся, что жест может понимать точку касания. Добавим для этого в метод следующий код:

let location = sender.location(in: self.view)
print(location)

Первый метод возвращает позицию касания в переданном представлении, причём каждое представление имеет свою подсистему координат. Возвращаемое значение имеет тип CGPoint - структура для точки из Core Graphics.
Затем мы просто выводим её в консоль.
Запустите программу и проверьте это.


Задаем действия программно

Помимо задания действий через IB, Вы можете привязать действия через код.
Сперва отвяжем действие от кнопки Рассчитать - для этого зайдите в её инспектор соединений и отвяжите действие, нажав по крестику у соответствующего события.



Теперь добавим в метод viewDidLoad следующий код:

self.button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)

Глаза разбегаются, как мы много сделали! У кнопок есть метод, добавляющий ей цель:

addTarget(_ target: Any?, action: Selector, for controlEvents: UIControlEvents)

Первый аргумент принимает что угодно. Собственно это и есть целевое назначение кнопки. Метод именно этого объекта будет вызван селектором.
action это нечто для нас новое - Selector - специальная конструкция, которая преобразует метод или функцию в объект, который могут принимать специальные категории методов.
Третий аргумент имеет тип UIControlEvents, то есть событие элемента управления. Он представляет собой перечисление. Мы взяли в качестве селектора уже готовый метод -заметьте, что селектор принимает полную сигнатуру метода, а не просто указатель на функцию.

Запустите программу и Вы увидите, что всё работает как и раньше.