Ранее в книге мы уже создавали несколько простейших приложений на Swift, используя для этого среду разработки Xcode. Как вы уже знаете, playground не предназначен для создания полноценных приложений, но набросать небольшой проект и протестировать его, не выходя из среды playground-проекта, вполне возможно. В этой главе мы рассмотрим пример создания приложения уже в среде Xcode Playground. Также вы познакомитесь с понятием API, разграничением доступа к различным объектам, а также я покажу вам важность работы с документацией.
Каждая программа или отдельный проект в Xcode — это модуль.
Модуль — это единый функциональный блок, выполняющий определенные задачи. Модули могут взаимодействовать между собой. Каждый внешний модуль, который вы используете в своей программе, для вас «черный ящик». Вам недоступна его внутренняя реализация — вы знаете лишь то, какие функции данный модуль позволяет выполнить (то есть что дать ему на вход и что получите на выходе). Модули состоят из исходных файлов и ресурсов.
Исходный файл — отдельный файл, содержащий программный код и разрабатываемый в пределах одного модуля.
Для того чтобы из набора файлов получить модуль, необходимо провести компиляцию, то есть из кода, понятного разработчику и среде программирования, получается файл (модуль), понятный компьютеру. Модуль может быть собран как из одного, так и из множества исходных файлов и ресурсов. В качестве модуля может выступать, например, целая программа или фреймворк.
С фреймворками (или библиотеками) мы встречались ранее: вспомните про Foundation и UIKit, которые мы подгружали в программу с помощью директивы import. Данная директива служит для обеспечения доступа к функционалу внешней библиотеки. Для доступа к ее функциям чаще всего существует специальный набор правил, то есть интерфейс, называемый API.
API (application programming interface) — это набор механизмов (обычно функций и типов данных), включенных в состав некоторой библиотеки (модуля). API доступны при условии, что содержащая их библиотека подключена к проекту (импортирована в проект).
Если вернуться к фреймворку Foundation, то функция arc4random_uniform(), которую мы использовали ранее, является одной из большого перечня доступных API-функций. Пример подключения фреймворка к библиотеке приведен в листинге 34.1.
Листинг 34.1
import Foundation
При разработке приложений вы будете использовать большое количество различных библиотек, которые, в том числе, поставляются вместе с Xcode. Самое интересное, что Xcode содержит просто гигантское количество возможностей: работа с 2D и 3D, различные визуальные элементы, физические законы и многое-многое другое. И все это реализуется благодаря дополнительным библиотекам. В этой главе вы познакомитесь с некоторыми их возможностями.
ПРИМЕЧАНИЕ Запомните, что одни библиотеки могут подключать другие. Так, например, UIKit подгружает в проект Foundation, и дополнительное подключение Foundation не требуется.
База системы разграничения доступа при разработке приложений строится на основе понятия «модуль».
Всего Swift предлагает пять различных уровней доступа для объектов вашего кода:
open и public
Открытый и публичный. Данные уровни доступа очень похожи, они открывают полную свободу использования объекта. Вы можете импортировать модуль и свободно использовать его public-объекты в своем коде. Разница между open и public описывается далее.
internal
Внутренний. Данный уровень используется в случаях, когда необходимо ограничить использование объекта самим модулем. Таким образом, объект будет доступен во всех исходных файлах модуля, исключая его использование за пределами модуля.
fileprivate
Частный в пределах файла. Данный уровень позволяет использовать объект только в пределах данного исходного файла.
private
Частный. Данный уровень позволяет использовать объект только в пределах конструкции, в которой он объявлен. Например, объявленный в классе параметр не будет доступен в его расширениях.
Открытый уровень доступа является самым высоким и наименее ограничивающим, а частный — самым низким и максимально ограничивающим.
Открытый уровень применяется только к классам и членам класса (свойствам, методам и т.д.) и отличается от публичного следующими характеристиками:
• Класс, имеющий уровень доступа public (или более строгий), может иметь подклассы только в том модуле, где он был объявлен.
• Члены класса, имеющие уровень доступа public (или более строгий), могут быть переопределены (с помощью оператора override) в подклассе только в том модуле, где он объявлен.
• Класс, имеющий уровень доступа open, может иметь подклассы внутри модуля, где он определен, и в модулях, импортированных в данном модуле.
• Члены класса, имеющие уровень доступа open, могут быть переопределены (с помощью оператора override) в подклассе в том модуле, где он объявлен, а также в модулях, импортируемых в данном модуле.
Главное правило определения уровня доступа в Swift звучит следующим образом: объект с более низким уровнем доступа не может определить объект с более высоким уровнем доступа (в контексте модуля).
По умолчанию все объекты вашего кода имеют уровень доступа internal. Для того чтобы изменить его, необходимо явно указать уровень. При этом если вы разрабатываете фреймворк, то для того, чтобы сделать некоторый объект частью доступного API, вам необходимо изменить его уровень доступа на public.
Чтобы определить уровень доступа к некоторому объекту, необходимо указать соответствующее ключевое слово (open, public, internal, private) перед определением объекта (func, property, class, struct и т.д.). Пример приведен в листинге 34.2.
Листинг 34.2
open class SomePublicClass {}
internal class SomeInternalClass {}
fileprivate class SomePrivateClass {}
public var somePublicVar = 0
private var somePrivatelet = 0
internal func someInternalFunc() {}
ПРИМЕЧАНИЕ Если уровень доступа к вашему объекту предполагается internal, то можно его не указывать, так как по умолчанию для любого объекта назначен именно этот уровень.
Следует подробнее остановиться на определении уровня доступа различных типов объектов.
Как мы уже неоднократно говорили, Swift позволяет определять собственные типы данных. Если вам требуется указать уровень доступа к типу данных или его членам, то это необходимо сделать в момент определения типа. Новый тип данных может быть использован там, где это позволяет его уровень доступа.
Если ваш объект имеет вложенные объекты (например, класс со свойствами и методами), то уровень доступа родителя определяет уровни доступа к его членам. Таким образом, если вы укажете уровень доступа private, то все его члены по умолчанию будут иметь уровень доступа private. Для уровней доступа public и internal уровень доступа членов — internal.
Рассмотрим пример из листинга 34.3.
Листинг 34.3
public class SomePublicClass { // public класс
public var somePublicProperty = 0 // public свойство
var someInternalProperty = 0 // internal свойство
fileprivate func somePrivateMethod() {} // fileprivate метод
}
class SomeInternalClass { // internal класс
var someInternalProperty = 0 // internal свойство
private func somePrivatemethod() {} // private метод
}
private class SomePrivateClass { // private класс
var somePrivateProperty = 0 // private свойство
func somePrivateMethod() {} // private метод
}
Всего было определено три класса с различными уровнями доступа. Из примера видно, как влияет определение уровня доступа к классу на доступ к его членам.
Обратите внимание, что при наследовании уровень доступа подкласса не может быть выше уровня родительского класса.
Уровень доступа к кортежу типа данных определяется наиболее строгим типом данных, включенным в кортеж. Так, например, если вы скомпонуете кортеж типа из двух разных типов, один из которых будет иметь уровень доступа internal, а другой — private, то результирующим уровнем доступа будет private, то есть самый строгий.
ПРИМЕЧАНИЕ Запомните, что Swift не позволяет явно указать тип данных кортежа. Он вычисляется автоматически.
Уровень доступа к функции определяется самым строгим уровнем типов аргументов функции и типа возвращаемого значения. Рассмотрим пример из листинга 34.4.
Листинг 34.4
func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// тело функции
}
Можно было ожидать, что уровень доступа функции будет равен internal, так как не указан явно. На самом деле эта функция вообще не будет скомпилирована. Это связано с тем, что тип возвращаемого значения — это кортеж с уровнем доступа private. При этом тип этого кортежа определяется автоматически на основе типов данных, входящих в него.
В связи с тем, что уровень доступа функции — private, его необходимо указать явно (листинг 34.5).
Листинг 34.5
private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// тело функции
}
Что касается перечислений, стоит обратить внимание на то, что каждый член перечисления получает тот же уровень доступа, что установлен для самого перечисления.
Что может быть интереснее, чем интерактивное приложение, в котором можно двигать, щелкать, толкать и т.д.? Разработкой именно такого приложения мы и займемся.
Для реализации возьмем следующую задачу: в квадратном ящике расположены несколько цветных шариков. Пользователь может перемещать шарики и сталкивать их друг с другом.
Задание
Сейчас вам необходимо создать новый playground-проект типа Blank. Назовите его "Balls" и удалите весь программный код, присутствующий в нем.
Xcode обладает потрясающим механизмом, который называется Interactive playgrounds. С его помощью вы можете нажимать, перемещать и совершать другие действия с объектами прямо в окне playground-проекта. Interactive playgrounds помогут вам быстро создать прототип приложения, тем самым уменьшив вероятность неприятных ошибок в релизной версии программы.
Interactive playgrounds наделяют вас практически теми же возможностями по построению интерфейсов, что и при полноценной разработке приложений. Именно этот механизм мы будем использовать при разработке вашего первого приложения.
В Xcode присутствует модуль, предназначенный для работы с Interactive Playground. Он называется PlaygroundSupport.
ПРИМЕЧАНИЕ PlaygroundSupport — модуль, обеспечивающий взаимодействие пользователя с Xcode при работе в playground.
Наиболее важным механизмом данной библиотеки является класс PlaygroundPage, предназначенный для создания интерактивного содержимого страниц в playground-проекте.
PlaygroundSupport позволяет взаимодействовать с некоторыми объектами. Поэтому нам потребуется функционал, обеспечивающий графическое построение этих объектов. И таким функционалом обладает модуль UIKit.
UIKit — это библиотека, обеспечивающая ключевую инфраструктуру, необходимую для построения iOS-приложений. UIKit содержит огромное количество классов, позволяющих строить визуальные элементы, анимировать их, обрабатывать физические законы, управлять механизмами печати и обработки текста и многое-многое другое! Это важнейшая и совершенно незаменимая библиотека функций, которую вы будете использовать при разработке каждого приложения.
Импортируем в проект Balls библиотеки PlaygroundSupport и UIKit (листинг 34.6).
Листинг 34.6
import PlaygroundSupport
import UIKit
Разделим функциональную нагрузку разрабатываемой программы между исходными файлами. В панели Project Navigator в папке Source создайте новый файл с именем Balls.swift. Для этого щелкните по названию папки правой кнопкой мыши и выберите пункт New File. В результате новый файл отобразится в составе папки Sources. Вам останется лишь указать его имя (рис. 34.1). Всю функциональную начинку мы разместим в этом файле, в то время как главный файл Balls будет использовать реализованный функционал.
Рис. 34.1. Новый файл в Project Navigator
В только что созданном файле также импортируйте библиотеку UIKit.
Весь функционал будет заключен в одном-единственном классе, который мы назовем Balls и расположим в файле Balls.swift. Определите новый класс Balls, как это сделано в листинге 34.7.
Листинг 34.7
import UIKit
public class Balls: UIView {}
В качестве уровня доступа класса Balls указан public. Это связано с тем, что некоторые свойства и методы класса UIView, которые будут переопределены в Balls, также соответствуют public, а как вы знаете, уровень доступа свойств и методов не может быть выше, чем уровень самого типа.
В состав подключенной библиотеки UIKit входит тип данных UIView, предназначенный для визуального отображения графических элементов и взаимодействия с ними. Например, мы можем отобразить прямоугольную рабочую область типа UIView, наполнив ее другими графическими элементами, каждый из которых также будет представлять собой отдельный экземпляр типа UIView. В этом и будет заключаться решение поставленной нами задачи с шариками (рис. 34.2).
В связи с этим реализуемый класс Balls является подклассом для UIView.
У разрабатываемой системы можно выделить следующие свойства.
1. Существует прямоугольная область.
2. Внутри данной области расположены несколько шариков.
3. Каждый шарик имеет свой цвет.
4. Шарики могут перемещаться.
5. Шарики могут взаимодействовать друг с другом (ударяться).
6. Шарики могут взаимодействовать с границами прямоугольной области.
Благодаря пункту 3 вы получите очень важный опыт взаимодействия с типом данных UIColor, который позволяет работать с цветовой палитрой.
Мы будем определять количество шариков, передавая массив цветов. Каждый из переданных цветов будет указывать на один шарик. Объявим два свойства, содержащих информацию о цветах и шариках (листинг 34.8).
Рис. 34.2. Структура отображений
Листинг 34.8
public class Balls: UIView {
// список цветов для шариков
private var colors: [UIColor]
// шарики
private var balls: [UIView] = []
}
Свойство colors — это массив значений типа UIColor. Сами шарики, как мы говорили выше, представляют собой экземпляры типа UIView, наложенные на отображение экземпляра типа Balls, который является наследником типа UIView. Мы не реализуем дополнительный тип данных для шариков, потому что функционала UIView вполне достаточно для решения поставленной задачи.
Для использования разрабатываемого класса нам потребуется реализовать инициализатор (листинг 34.9).
Листинг 34.9
//инициализатор класса
public init(colors: [UIColor]){
self.colors = colors
super.init(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
backgroundColor = UIColor.gray
}
Инициализатор init(colors:) в качестве входного параметра получает массив значений типа UIColor. В результате количество и порядок шариков будут зависеть именно от массива colors.
Класс UIView имеет встроенный инициализатор init(frame:), который позволяет определить характеристики создаваемого графического элемента. При его вызове задаются значения для построения прямоугольной основы, на которую будут накладываться шарики. Для создания прямоугольников служит специальный класс CGRect, которому в качестве параметров передаются координаты левого верхнего угла и значения длин сторон.
Свойство backgroundColor определяет цвет создаваемого объекта, оно наследуется от класса UIView. Класс UIColor позволяет нам создать практически любой требуемый цвет. Для этого существует большое количество методов. В данном случае мы используем свойство gray, которое определяет серый цвет. При необходимости создания произвольного цвета вы можете использовать соответствующие методы (в этом вам поможет окно автодополнения).
Обратите внимание, что при попытке запустить проект Xcode отобразит ошибку, сообщающую об отсутствии инициализатора init(coder:) (рис. 34.3).
Рис. 34.3. Ошибка, сообщающая об отсутствии инициализатора init(coder:)
Вы можете написать код инициализатора самостоятельно (листинг 34.10) или позволить Xcode исправить ошибку.
Листинг 34.10
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Теперь перейдем к основному файлу проекта Balls в Project Navigator и создадим экземпляр класса Balls (листинг 34.11).
Листинг 34.11
let balls = Balls(colors: [UIColor.white])
В качестве входного параметра инициализатора мы передаем массив объектов типа UIColor, содержащий всего один элемент, который определяет белый цвет. В результате в константу balls будет записан экземпляр класса Balls, описывающий всю разрабатываемую систему целиком.
ПРИМЕЧАНИЕ Xcode позволяет преобразовать некоторые ресурсы из текстового вида в графический. Так, например, описание цвета может быть представлено в виде визуальной палитры цветов прямо в редакторе кода!
Для этого требуется переписать объявление в виде специального литерала:
#colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)
После его написания данный код будет преобразован к графическому квадрату черного цвета. Вы можете вставить как элемент массива colors вместо UIColor.white и скопировать необходимое количество раз (рис. 34.4).
Рис. 34.4. Визуальное отображение ресурсов
Для выбора нового цвета вам необходимо щелкнуть по квадрату и выбрать подходящий из появившейся палитры. Это еще одна из поразительных возможностей среды разработки Xcode!
Обратите внимание на то, что такой подход возможен только в файлах, описывающих страницы playground. В Balls.swift вся информация отображается исключительно в текстовом виде.
Добавьте еще три произвольных цвета в массив colors, при этом не забудьте разделить их запятыми.
Теперь наш проект готов для предварительного отображения написанного кода. Для этого добавьте следующую строку в файл Balls (листинг 34.12).
Листинг 34.12
PlaygroundPage.current.liveView = balls
В данном листинге используется функционал подключенной ранее библиотеки PlaygroundSupport — класс PlaygroundPage, который позволяет вывести графические элементы на странице playground.
Теперь нажмите кнопку Assistant Editor, и в правой части Xcode отобразится вывод экземпляра класса Balls (рис. 34.5). В данный момент отображается только прямоугольник — он будет подложкой для отображения набора шариков.
Рис. 34.5. Работа в режиме Assistant Editor
Вернемся к файлу Balls.swift.
Xcode имеет довольно внушительное количество типов данных, — как говорится, на все случаи жизни. Для указания размера шариков мы будем использовать тип данных CGSize. Создадим свойство класса Balls, описывающее высоту и ширину шарика (листинг 34.13).
Листинг 34.13
// размер шариков
private var ballSize: CGSize = CGSize(width: 40, height: 40)
В качестве аргументов инициализатор класса CGSize получает параметры width и height, определяющие ширину и высоту.
Теперь напишем метод, отвечающий за отображение шариков (листинг 34.14).
Листинг 34.14
func ballsView () {
/* производим перебор переданных цветов
именно они определяют количество шариков */
for (index, color) in colors.enumerated() {
/* шарик представляет собой
экземпляр класса UIView */
let ball = UIView(frame: CGRect.zero)
/* указываем цвет шарика
он соответствует переданному цвету */
ball.backgroundColor = color
// накладываем отображение шарика на отображение подложки
addSubview(ball)
// добавляем экземпляр шарика в массив шариков
balls.append(ball)
/* вычисляем отступ шарика по осям X и Y, каждый
последующий шарик должен быть правее и ниже
предыдущего */
let origin = 40*index + 100
// отображение шарика в виде прямоугольника
ball.frame = CGRect(x: origin, y: origin,
width: Int(ballSize.width), height: Int(ballSize.height))
// с закругленными углами
ball.layer.cornerRadius = ball.bounds.width / 2.0
}
}
Для обработки шариков мы используем свойство colors, которое хранит в себе массив переданных цветов. Метод enumerated() уже знаком нам — он позволяет перебрать все элементы массива, получая индекс и значение каждого элемента.
Как уже неоднократно говорилось, шарики, как и вся система целиком, это отображения. Но если подложка — это экземпляр потомка класса UIView, то каждый шарик — экземпляр самого UIView. Изначально в качестве отображения мы используем конструкцию CGRect.zero, которая соответствует CGRect(x: 0, y: 0, width: 0, height:0), то есть прямоугольнику с нулевыми размерами.
Для того чтобы подложка и шарики выводились совместно, необходимо использовать функцию addSubview(_:), которая накладывает одно отображение на другое.
Для того чтобы шарики обрисовывались так же, как и их подложка, необходимо добавить вызов метода ballsVew() в инициализатор init(colors:) (листинг 34.15).
Листинг 34.15
//инициализатор класса
public init(colors: [UIColor]){
self.colors = colors
super.init(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
backgroundColor = UIColor.gray
// вызов функции отрисовки шариков
ballsView()
}
Вернитесь к файлу Balls. В режиме Assistant Editor можно увидеть, что кроме серой подложки отрисовываются четыре разноцветных шарика. В данный момент шарики статичны: вы не сможете с ними взаимодействовать.
Теперь займемся интерактивностью. Нам необходимо реализовать возможность перемещения шариков указателем мыши, их столкновения между собой и с бортами подложки.
Для анимации движения используется класс-аниматор UIDynamicAnimator. Он позволяет отображать обрабатываемые другими классами события, например перемещения.
Создадим новое свойство класса Balls (листинг 34.16).
Листинг 34.16
// аниматор графических объектов
private var animator: UIDynamicAnimator?
Обратите внимание, что аниматор — это опционал. Это связано с тем, что данное свойство не будет иметь какого-либо значения к моменту вызова родительского инициализатора super.init(frame:).
Теперь необходимо подключить аниматор к отображению. Для этого добавим в инициализатор соответствующий код (листинг 34.17).
Листинг 34.17
public init(colors: [UIColor]){
self.colors = colors
super.init(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
backgroundColor = UIColor.blueColor()
// подключаем аниматор с указанием на сам класс
animator = UIDynamicAnimator(referenceView: self)
ballsView()
}
Сам по себе аниматор не производит каких-либо действий. Для того чтобы он отображал некоторое изменение состояния объектов, необходимо использовать ряд дополнительных классов и связать каждый из этих классов с аниматором.
Для взаимодействия пользователя с графическими элементами UIKit предоставляет нам специальный класс UISnapBehavior.
ПРИМЕЧАНИЕ Каждый тип данных, в названии которого присутствует Behavior, предназначен для обработки некоторого поведения. Так, UISnapBehavior обрабатывает поведение при перемещении объектов от точки к точке.
Определим новое свойство типа UISnapBehavior (листинг 34.18).
Листинг 34.18
// обработчик перемещений объектов
private var snapBehavior: UISnapBehavior?
Класс UISnapBehavior позволяет обрабатывать касания экрана устройства (или щелчки мышки). Для этого в состав UISnapBehavior включены три метода:
touchesBegan(_:with:)
Метод вызывается в момент касания экрана.
touchesMoved(_:with:)
Метод срабатывает при каждом перемещении пальца, уже коснувшегося экрана.
touchesEnded(_:with:)
Метод вызывается по окончании взаимодействия с экраном (когда палец убран).
Все методы уже определены в UISnapBehavior, поэтому при создании собственной реализации этих методов необходимо их переопределять, то есть использовать ключевое слово override.
Реализуем метод touchesBegan(_:with:) (листинг 34.19).
Листинг 34.19
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let touchLocation = touch.location( in: self )
for ball in balls {
if (ball.frame.contains( touchLocation )) {
snapBehavior = UISnapBehavior(item: ball,snapTo: touchLocation)
snapBehavior?.damping = 0.5
animator?.addBehavior(snapBehavior!)
}
}
}
}
Коллекция touches содержит данные обо всех касаниях. Это связано с тем, что тач-панель современных устройств поддерживает мультитач, то есть одновременное касание несколькими пальцами. В начале метода извлекаются данные о первом элементе набора touches и помещаются в константу touch.
Константа touchLocation содержит координаты касания относительно всего отображения.
С помощью метода ball.frame.contains() мы определяем, входят ли координаты касания в какой-либо из шариков. Если находится соответствие, то в свойство snapBehavior записываются данные об объекте, с которым происходит взаимодействие, и о координатах, куда данный объект должен быть перемещен.
Свойство damping определяет плавность и затухание при движении шарика.
Далее, используя метод addBehavior(_:) аниматора, указываем, что обрабатываемое классом UISnapBehavior поведение объекта должно быть анимировано. Таким образом, все изменения состояния объекта, производимые в свойстве snapBehavior, будут анимированы.
После обработки касания необходимо обработать перемещение пальца. Для этого реализуем метод touchesMoved(_:with:) (листинг 34.20).
Листинг 34.20
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let touchLocation = touch.location( in: self )
if let snapBehavior = snapBehavior {
snapBehavior.snapPoint = touchLocation
}
}
}
Так как в свойстве snapBehavior уже содержится указание на определенный шарик, с которым происходит взаимодействие, нет необходимости проходить по всему массиву шариков снова. Единственной задачей данного метода является изменение свойства snapPoint, которое указывает на координаты объекта.
Для завершения обработки перемещения объектов касанием необходимо переопределить метод touchesEnded(_:with:) (листинг 34.21).
Листинг 34.21
public override func touchesEnded(_ touches: Set<UITouch>,
with event: UIEvent?) {
if let snapBehavior = snapBehavior {
animator?.removeBehavior(snapBehavior)
}
snapBehavior = nil
}
Данный метод служит для решения одной очень важной задачи — очистки используемых ресурсов. После того как взаимодействие с шариком окончено, нет необходимости хранить информацию об обработчике поведения в snapBehavior.
ПРИМЕЧАНИЕ Возьмите в привычку удалять ресурсы, пользоваться которыми вы уже не будете. Это сэкономит изрядное количество памяти и уменьшит вероятность возникновения ошибок.
Перейдите в файл Balls, и в окне Assistant Editor вы увидите изображение четырех шариков, но, в отличие от предыдущего раза, сейчас вы можете указателем мыши перемещать любой из шариков (рис. 34.6)!
Рис. 34.6. Шарики, расположенные внутри подложки
Хотя шарики и могут перемещаться, они не взаимодействуют между собой и с границами подложки. Для обработки поведения при столкновениях служит класс UICollisionBehavior. Создадим новое свойство (листинг 34.22).
Листинг 34.22
// обработчик столкновений
private var collisionBehavior: UICollisionBehavior
Следующим шагом будет редактирование инициализатора (листинг 34.23).
Листинг 34.23
public init(colors: [UIColor]){
self.colors = colors
// создание значения свойства
collisionBehavior = UICollisionBehavior(items: [])
/* указание на то, что границы отображения
также являются объектами взаимодействия */
collisionBehavior.setTranslatesReferenceBoundsIntoBoundary( with:
UIEdgeInsets(top: 1, left: 1, bottom: 1, right: 1))
super.init(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
backgroundColor = UIColor.gray
// подключаем аниматор с указанием на сам класс
animator = UIDynamicAnimator(referenceView: self)
// вызов функции отрисовки шариков
ballsView()
}
Одним из свойств класса UICollisionBehavior является items. Оно указывает на набор объектов, которые могут взаимодействовать между собой. В момент работы инициализатора шарики еще не созданы, поэтому в качестве входного параметра при создании объекта типа UICollisionBehavior мы указываем пустой массив.
Для того чтобы обеспечить взаимодействие шариков не только друг с другом, но и с границами подложки, мы используем метод setTranslatesReferenceBoundsIntoBoundary(with:). Он устанавливает границы коллизий с внутренним отступом в 1 пиксел с каждой стороны подложки.
В самом конце необходимо добавить обработчик поведения при коллизиях аниматору animator. Делается это с помощью уже известного нам метода addBehavior(_:).
Теперь вам потребуется добавить каждый шарик в обработчик коллизий. Для этого существует метод addItem(_:). Добавим соответствующую строку в метод ballsView() (листинг 34.24).
Листинг 34.24
func ballsView () {
/* производим перебор переданных цветов
именно они определяют количество шариков */
for (index, color) in colors.enumerated() {
/* шарик представляет собой
экземпляр класса UIView */
let ball = UIView(frame: CGRect.zero)
/* указываем цвет шарика
он соответствует переданному цвету */
ball.backgroundColor = color
// накладываем отображение шарика на отображение подложки
addSubview(ball)
// добавляем экземпляр шарика в массив шариков
balls.append(ball)
/* вычисляем отступ шарика по осям X и Y, каждый
последующий шарик должен быть правее и ниже
предыдущего */
let origin = 40*index + 100
// отображение шарика в виде прямоугольника
ball.frame = CGRect(x: origin, y: origin,
width: Int(ballSize.width), height:
Int(ballSize.height))
// с закругленными углами
ball.layer.cornerRadius = ball.bounds.width / 2.0
// добавим шарик в обработчик столкновений
collisionBehavior.addItem(ball)
}
}
Поздравляю вас! Это была последняя строчка кода, которую необходимо было написать для решения поставленной задачи. Сейчас вы можете перейти к файлу Balls и в Assistant Editor протестировать созданный прототип.
Как вы можете видеть, шарики невозможно переместить за границы, а при попытке столкнуть их они разлетаются.
UIKit и Foundation предоставляет еще огромное количество возможностей помимо рассмотренных. Так, например, можно создать гравитационное, магнитное и любое другое взаимодействие элементов, смоделировать силу тяжести или турбулентность, установить параметры скорости и ускорения. Все это и многое-многое другое позволяет вам создавать поистине функциональные приложения, которые обязательно найдут отклик у пользователей.