Кривые Безье и Распознаватели жестов

Если вы нашли опечатку в тексте, выделите ее и нажмите CTRL + ENTER.

В этом уроке мы создадим приложение, которое позволяет играть с фигурами, перемещением, масштабированием и вращением. Мы будем работать с UIGestureRecognizer и UIBezierPaths. Конечный результат будет выглядеть следующим образом:

Распознаватель прикосновений

Для начала создайте новый Single View Application. Убедитесь, что в качестве языка выбран Swift и Universal во вкладке Devices (устройства). Приложение работает лучше всего на iPad, так как у него достаточно большой экран.

Мы хотим добавить различные фигуры в наш view, которые будут появляться, когда пользователь будет прикасаться к экрану. Для этого мы будем использовать UITapGestureRecognizer. Распознаватель жестов является объектом, который определяет, какой выполняется жест. В частности UITapGestureRecognizer определяет, когда происходит прикосновение к экрану. Каждый распознаватель жестов имеет одну или более target-action пару, связанную с ним, и в любое время, когда жест распознается, то вызывается метод для каждого таргета распознавателя жестов. Распознаватель жестов также должен быть присоединен к экземпляру  UIView, и распознаватели жестов будут получать только события, которые происходят внутри этого view или его подвидов.

Давайте добавим UITapGestureRecognizer на наш view контроллер. Метод viewDidLoad является хорошим местом куда можно добавить распознаватель жестов. Экшн, который мы предоставляем для распознавателя жестов, должен соответствовать методу в классе таргета (цели). Название метода передается в строке с двоеточием (:) в конце. Двоеточие означает, что метод принимает 1 параметр, который в этом случае будет распознавателем жестов.

override func viewDidLoad() {
        let tapGR = UITapGestureRecognizer(target: self, action: "didTap:")
        self.view.addGestureRecognizer(tapGR)
    }
    
    func didTap(tapGR: UITapGestureRecognizer) {
        
    }

Добавляем фигуры

Все фигуры в нашем приложении будут экземплярами нашего класса ShapeView, этот класс будет обрабатывать графику и жесты. Изначально мы будем рисовать красный квадрат, но вскоре мы познакомимся с более сложными фигурами!

Для начала создайте новый файл File -> New -> File... -> Cocoa Class. Назовите его ShapeView и сделайте подклассом UIView.

В методе didTap мы можем извлечь локацию прикосновения через метод распознаватель жестов locationInView:. После того, как у нас появилось местоположение прикосновения, мы хотим создать новую фигуру на его месте. Давайте создадим метод init(origin: CGPoint) на нашем ShapeView, что создаст view в данной позиции. Мы также определим константу size инициализированную на 150. В нашем методе инициализации мы вызываем метод init(frame)  на суперкласс и перемещаем только созданный center view  (центр вьюшки) на указанный origin.

class ShapeView: UIView {
    let size: CGFloat = 150.0
    
    init(origin: CGPoint) {
        super.init(frame: CGRectMake(0.0, 0.0, size, size))
        self.center = origin
    }
    
    // We need to implement init(coder) to avoid compilation errors
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Мы нарисуем фигуру в нашем view в методе DrawRect:. Для этого мы будем использовать UIBezierPath. Метод drawRect: не делает ничего по умолчанию, мы переопределяем его для обеспечения логики пользовательского рисунка для нашего view.

Чтобы нарисовать фигуру с UIBezierPath мы сначала должны определить геометрию нашей формы, после этого с помощью метода fill мы заполняем форму. Чтобы установить цвет заливки мы используем метод setFill на UIColor. Мы создадим наш путь Безье с помощью метода инициализации UIBezierPath(roundedRect:, cornerRadius:). Это создаст округлую форму прямоугольника. Впоследствии мы устанавливаем цвет заполнения и заполняем путь. В классе ShapeView напишите следующий метод:

override func drawRect(rect: CGRect) {
        
        let path = UIBezierPath(roundedRect: rect, cornerRadius: 10)
        
        UIColor.redColor().setFill()
        path.fill()
    }

Теперь вернемся к методу didTap в ViewController.swift. Здесь мы создадим ShapeViews и добавим их в иерархию view.

func didTap(tapGR: UITapGestureRecognizer) {
        
        let tapPoint = tapGR.locationInView(self.view)
        
        let shapeView = ShapeView(origin: tapPoint)
        
        self.view.addSubview(shapeView)
    }

Теперь нажатие на экран будет добавлять красные скругленные прямоугольники на месте прикосновения.

Обратите внимание, что у прямоугольников остался черный фон около углов, чтобы решить эту проблему мы установим backgroundColor нашей фигуры в методе init: как UIColor.clearColor().

self.backgroundColor = UIColor.clearColor()

Окантовка прямоугольников

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

Давайте добавим обводку нашим прямоугольникам, для начала давайте определим ширину обводки как константу lineWidth в нашем классе ShapeView. lineWidth может быть установлена на кривой Безье через свойство lineWidth.

let lineWidth: CGFloat = 3

Затем мы должны изменить наш метод drawRect так, чтобы он обвел наш путь. В настоящее время наша фигура совпадает с размером нашего view, что означает, что половина нашей обводки будет снаружи view и следовательно, будет невидимой. Чтобы это исправить, мы вставим используемый прямоугольник для создания нашей кривой Безье в половину ширины нашей линии. Вставка CGRect делается через функцию CGRectInset(rect,dx,dy). Значение происхождения прямоугольника (origin value) смещёно по х-оси на расстояние, указанное в параметре dx и по у-оси на расстояние, заданное параметром dy, и его размер устанавливается (2dx,2dy), исходя из исходного прямоугольника. Если dx и dy положительные значения, то размер прямоугольника уменьшается.

Мы также должны установить цвет для обводки, это делается с помощью метода setStroke на UIColor. Перепишем метод drawRect класса ShapeView:

override func drawRect(rect: CGRect) {
        
        let insetRect = CGRectInset(rect, lineWidth / 2, lineWidth / 2)
        
        let path = UIBezierPath(roundedRect: insetRect, cornerRadius: 10)
        
        
        UIColor.redColor().setFill()
        path.fill()
        
        path.lineWidth = self.lineWidth
        UIColor.blackColor().setStroke()
        path.stroke()
    }

Панорамирование

Панорамирование - постепенный перенос изображения с целью создания зрительного ощущения движения.

Мы будем использовать UIPanGestureRecognizer для того, чтобы двигать наши фигуры. UIPanGestureRecognizer это распознаватель жестов, который распознает панорамирование, т.е. перемещение пальца по view. Вы можете получить перемещение (количество панорамирования) распознавателя жестов с помощью метода translationInView. Мы установим перемещение распознавателя жестов обратно на 0 после панорамирования для упрощения логики.

Есть еще одна вещь, которую стоит отметить, это то, что всякий раз, когда мы взаимодействуем с фигурами, мы хотим переместить ее на вершину иерархии view, так чтобы другие фигуры не закрывали ее. Для этого мы вызываем bringSubviewToFront(view) на superview фигуры.

Мы создадим метод initGestureRecognizers, где мы установим наши распознаватели жестов. Всякий раз, когда метод становится слишком сложным и начинает делать несколько вещей, нужно разделить его на несколько методов. Мы инициализируем UIPanGestureRecognizer так, как мы делали это с UITapGestureRecognizer. В целевом методе мы реализуем логику, описанную выше, эффективно перемещая view, добавляя перемещение в свойство center.

class ShapeView: UIView {
...
    init(origin: CGPoint) {
        ...
        initGestureRecognizers()
    }

    func initGestureRecognizers() {
        let panGR = UIPanGestureRecognizer(target: self, action: "didPan:")
        addGestureRecognizer(panGR)
    }

    func didPan(panGR: UIPanGestureRecognizer) {

        self.superview!.bringSubviewToFront(self)

        var translation = panGR.translationInView(self)

        self.center.x += translation.x
        self.center.y += translation.y

        panGR.setTranslation(CGPointZero, inView: self)
    }

...
}

Теперь вы можете двигать фигуры!

Щипки (pinching gesture)

Мы будем использовать UIPinchGestureRecognizer для масштабирования наших фигуры. UIPinchGestureRecognizer - это распознаватель жестов, который распознает "щипки". Когда пользователь двигает два пальца в направлении друг к другу, то масштаб уменьшается, когда пользователь раздвигает два пальца от друг от друга, масштаб увеличивается. Вы можете получить получить масштаб через свойство scale распознавателя жестов . Мы вернем масштаб распознавателя жестов на 1 после применения этого жеста, чтобы упростить логику.

Для масштабирования view мы изменим свойство transform. Свойство transform является типом CGAffineTransform и может быть использовано для изменения масштаба view, вращения, перемещения и т.п.

Свойство transform изначально равно CGAffineTransformIdentity с трансформацией do nothing. Мы можем использовать различные методы для создания новой трансформации или изменить существующую. В нашем случае лучше всего изменить трансформацию view через функцию CGAffineTransformScale(transform,scaleX,scaleY), которая принимает и увеличивает коэффициент масштаба scaleX в горизонтальном направлении и scaleY в вертикальном направлении. scaleX будет равна scaleY и свойству распознавателя жестов sale. Обновите метод initGestureRecgnizers: и добавьте метод didPinch: в классе ShapeView:

func initGestureRecognizers() {
        let panGR = UIPanGestureRecognizer(target: self, action: "didPan:")
        addGestureRecognizer(panGR)

        let pinchGR = UIPinchGestureRecognizer(target: self, action: "didPinch:")
        addGestureRecognizer(pinchGR)
    }

    func didPinch(pinchGR: UIPinchGestureRecognizer) {

        self.superview!.bringSubviewToFront(self)

        let scale = pinchGR.scale

        self.transform = CGAffineTransformScale(self.transform, scale, scale)

        pinchGR.scale = 1.0
    }

Теперь вы можете масштабировать фигуры!

Вращение

Мы будем использовать UIRotationGestureRecognizer для вращения наших фигур. UIRotationGestureRecognizer - это распознаватель жестов, который обнаруживает вращение. Жест срабатывает, когда пользователь двигает пальцы, расположенные напротив друг друга, по кругу. Вы можете получить вращение распознавателя жестов через свойство property. Мы установим вращение распознавателя жестов обратно на 0 после вращения, чтобы упростить логику. Вращение задается в радианах (radians).

Чтобы обновить вращение нашего view, мы снова изменим свойство transform. Мы будем использовать функцию CGAffineTransformRotate(transform,rotation) для того, чтобы изменить нашу трансформацию view. Функция CGAffineTransformRotate берет на себя трансформацию и создает новую трансформацию, добавляя rotation трансформации фактора rotation. Обновите метод initGestureRecognizers и запишите новый метод didRotate(rotationGR: UIRotationGestureRecognizer) в классе ShapeView:

func initGestureRecognizers() {
        let panGR = UIPanGestureRecognizer(target: self, action: "didPan:")
        addGestureRecognizer(panGR)

        let pinchGR = UIPinchGestureRecognizer(target: self, action: "didPinch:")
        addGestureRecognizer(pinchGR)

        let rotationGR = UIRotationGestureRecognizer(target: self, action: "didRotate:")
        addGestureRecognizer(rotationGR)
    }

    func didRotate(rotationGR: UIRotationGestureRecognizer) {

        self.superview!.bringSubviewToFront(self)

        let rotation = rotationGR.rotation

        self.transform = CGAffineTransformRotate(self.transform, rotation)

        rotationGR.rotation = 0.0
    }

Теперь мы можем вращать наши фигуры!

Вращение и "щипки" фигур работают на отлично, но не передвижение (когда мы пытаемся двигать уменьшенную / повернутую фигуру, движение не работает должным образом). Есть несколько способов это исправить, но проще всего применять трансформацию нашего view для движения, возвращаемое панорамированным распознавателем жестов.

class ShapeView: UIView {
...
    func didPan(panGR: UIPanGestureRecognizer) {

        self.superview!.bringSubviewToFront(self)

        var translation = panGR.translationInView(self)

        translation = CGPointApplyAffineTransform(translation, self.transform)

        self.center.x += translation.x
        self.center.y += translation.y

        panGR.setTranslation(CGPointZero, inView: self)
    }
...
}

Теперь все должно работать правильно.

Случайные (рандомные) цвета.

Давайте раскрасим наши прямоугольники какими-то случайными цветами, вместо красного. Случайные (рандомные) цвета мы получим, используя конструктор initWithHue:saturation:brightness:alpha: класса UIColor.

Мы напишем метод, который возвращает случайные цвета. Во-первых, мы должны будем сгенерировать случайную плавающую точку между 0 и 1, это может быть сделано с помощью CGFloat(Float(arc4random()) / Float(UINT32_MAX)), arc4random() возвращает значение между 0 и UINT32_MAX, так что разделяя это значение на UINT32_MAX, дает нам значение между 0 и 1. Для насыщенности и яркости мы выбираем некоторые произвольные значения. Мы также установим значение альфа (непрозрачность) на 0.8, так что фигуры будут прозрачными немного прозрачными.

class ShapeView: UIView {
...
    func randomColor() -> UIColor {
        let hue:CGFloat = CGFloat(Float(arc4random()) / Float(UINT32_MAX))
        return UIColor(hue: hue, saturation: 0.8, brightness: 1.0, alpha: 0.8)
    }
}

Мы создадим новое свойство fillColor в ShapeView, которое мы будем инициализироваться случайным цветом, также мы должны изменить метод drawRect, чтобы он использовал наш fillColor вместо UIColor.redColor().

class ShapeView {
...
   var fillColor: UIColor!

   init(origin: CGPoint) {

        super.init(frame: CGRectMake(0.0, 0.0, size, size))

        self.fillColor = randomColor()
    ...
    }
    ...

    override func drawRect(rect: CGRect) {
    ... 
        self.fillColor.setFill()
    ...
    }
...
}

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

Добавляем еще фигуры

На этом этапе легко добавить еще фигуры других форм. Давайте добавим круг и прямоугольник. В настоящее время мы создаем наш путь объекта в методе drawRect: . Давайте сделаем path свойством класса ShapeView и инициализируем в методе инициализации. Для этого мы напишем метод randomPath() -> UIBezierPath, который возвращает случайный путь (прямоугольник, круг или треугольник).

Создать круг легко: мы просто вызываем конструктор UIBezierPath(ovalInRect), треугольник создать немного сложнее, как именно рассмотрим ниже. Внесите следующие изменения в ваш код:

class ShapeView {
...
    var path: UIBezierPath!
...
    func randomPath() -> UIBezierPath {

        let insetRect = CGRectInset(self.bounds,lineWidth,lineWidth)

        let shapeType = arc4random() % 3

        if shapeType == 0 {
            return UIBezierPath(roundedRect: insetRect, cornerRadius: 10.0)
        }

        if shapeType == 1 {
            return UIBezierPath(ovalInRect: insetRect)
        }
        return trianglePathInRect(insetRect)
    }

    init(origin: CGPoint) {
    ...  
        self.path = randomPath()
    ...   
    }

    override func drawRect(rect: CGRect) {            
        self.fillColor.setFill()
        self.path.fill()

        self.path.lineWidth = self.lineWidth
        UIColor.blackColor().setStroke()
        self.path.stroke()
    }
}

Треугольник представляет собой многоугольник с 3 вершинами. У UIBezierPath нет конструктора для создания треугольника, но он имеет несколько методов, которые позволяют нам построить произвольные многоугольники. Чтобы добавить многоугольник на кривой Безье, вы должны сначала добавить первую точку через метод moveToPoint(point). Линии затем могут быть добавлены в последующие пункты через метод addLineToPoint(point). После того как все точки добавлены, многоугольник может быть закрыт с помощью метода closePath. Этот метод соединяет последнюю добавленную точку с начальной точкой (указанную в moveToPoint).

3 точки нашего треугольника: середина сверху, нижняя правая точка и нижняя левая точка. Мы создадим метод trianglePathInRect(rect), который возвращает путь треугольника. Этот метод вызывается в randomPath().

    func trianglePathInRect(rect:CGRect) -> UIBezierPath {
        let path = UIBezierPath()

        path.moveToPoint(CGPointMake(rect.width / 2.0, rect.origin.y))
        path.addLineToPoint(CGPointMake(rect.width,rect.height))
        path.addLineToPoint(CGPointMake(rect.origin.x,rect.height))
        path.closePath()


        return path
    }

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

Челленджи

1. Штриховка

Попробуйте добавить штриховку вашим фигурам. Вы можете скачать 2 изображения с штриховкой здесь. Штриховка может быть сделана путем предварительного заполнения пути с FillColor, а затем заполнением пути изображением с штриховкой. Вы можете создать цвет из изображения, используя UIColor (patternImage) конструктор. Выберите рандомно из 2 изображений с штриховкой. Также рандомно нарисуйте фигуру с штриховкой или без.

Решение:

class ShapeView: UIView {
    override func drawRect(rect: CGRect) {

        self.fillColor.setFill()

        self.path.fill()

        var name = "hatch"
        if arc4random() % 2 == 0 {
            name = "cross-hatch"
        }

        let color = UIColor(patternImage: UIImage(named: name)!)

        color.setFill()

        if arc4random() % 2 == 0 {
            path.fill()
        }

        UIColor.blackColor().setStroke()

        path.lineWidth = self.lineWidth

        path.stroke()
    }
}

2. Правильные прямоугольники

Добавим новую фигуру. Правильный многоугольник со случайным числом вершин от 3 до 12. Правильный многоугольник представляет собой равноугольный многоугольник (все углы равны) и равносторонний многоугольник (все стороны имеют одинаковую длину).

 

Подсказка

Вам нужно соединить N точек по кругу. Угловое смещение метлу точками постоянно и равно 2 * PI / N. Создайте метод, который возвращает точку, соответствующую определенному углу, радиусу и смещению.

Решение

func pointFrom(angle: CGFloat, radius: CGFloat, offset: CGPoint) -> CGPoint {
        return CGPointMake(radius * cos(angle) + offset.x, radius * sin(angle) + offset.y)
    }

    func regularPolygonInRect(rect:CGRect) -> UIBezierPath {
        let degree = arc4random() % 10 + 3

        let path = UIBezierPath()

        let center = CGPointMake(rect.width / 2.0, rect.height / 2.0)

        var angle:CGFloat = -CGFloat(M_PI / 2.0)
        let angleIncrement = CGFloat(M_PI * 2.0 / Double(degree))
        let radius = rect.width / 2.0

        path.moveToPoint(pointFrom(angle, radius: radius, offset: center))

        for i in 1...degree - 1 {
            angle += angleIncrement
            path.addLineToPoint(pointFrom(angle, radius: radius, offset: center))
        }

        path.closePath()

        return path
    }

3.Звезды

Добавим новую фигуру, состоящую из пути в форме “звезды”. Весело создавать звезды со случайным числом углов от 5 до 15, например. Звезды, в конечном итоге, должны выглядеть следующим образом:

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

Решение

func starPathInRect(rect: CGRect) -> UIBezierPath {
        let path = UIBezierPath()

        let starExtrusion:CGFloat = 30.0

        let center = CGPointMake(rect.width / 2.0, rect.height / 2.0)

        let pointsOnStar = 5 + arc4random() % 10

        var angle:CGFloat = -CGFloat(M_PI / 2.0)
        let angleIncrement = CGFloat(M_PI * 2.0 / Double(pointsOnStar))
        let radius = rect.width / 2.0

        var firstPoint = true

        for i in 1...pointsOnStar {

            let point = pointFrom(angle, radius: radius, offset: center)
            let nextPoint = pointFrom(angle + angleIncrement, radius: radius, offset: center)
            let midPoint = pointFrom(angle + angleIncrement / 2.0, radius: starExtrusion, offset: center)

            if firstPoint {
                firstPoint = false
                path.moveToPoint(point)
            }

            path.addLineToPoint(midPoint)
            path.addLineToPoint(nextPoint)

            angle += angleIncrement
        }

        path.closePath()

        return path
    }

4. Мы ❤ Фракталы

Добавьте простую фрактальную фигуру в приложение. Фигура создается следующим образом:

Шаг 1: Добавьте круг в какой-то точке

Шаг 2: Добавьте круг на каждую из вершин правильного многоугольника, вписанных в этот круг

Шаг 3: Повторите шаг 2 для каждого круга.

Перебор этого алгоритма 6 раз даст нам форму, которая будет выглядеть похожей на треугольник Серпинского (Sierpinski’s triangle)

Решение

 func addDetailToFractalPath(center: CGPoint, radius: CGFloat, path:UIBezierPath, iterations:Int) {
        if iterations == 0 {
            return
        }

        var angle:CGFloat = -CGFloat(M_PI / 2.0)
        let angleIncrement = CGFloat(M_PI * 2.0 / Double(3))

        path.appendPath(UIBezierPath(ovalInRect: CGRectMake(center.x - radius, center.y - radius, radius * 2, radius * 2)))

        for i in 1...3 {

            let point = pointFrom(angle, radius: radius, offset: center)



            addDetailToFractalPath(point, radius: radius / 2.0, path: path, iterations: iterations - 1)

            angle += angleIncrement
        }

        path.closePath()
    }

    func fractalPathInRect(rect: CGRect) -> UIBezierPath {
        let path: UIBezierPath = UIBezierPath()

        addDetailToFractalPath(CGPointMake(rect.size.width / 2, rect.size.height / 2), radius: 35.0, path: path, iterations: 5)

        return path
    }

Что дальше?

Дальше, вы можете продолжить изучать наши туториалы по мере их появления, а также, параллельно читать перевод официальной книги по языку программирования Swift. И, для более подробного изучения языка, вы можете пройти наши курсы!

Источник урока: https://www.weheartswift.com/bezier-paths-gesture-recognizers/

Урок подготовил: Иван Акулов