Как сделать простое приложение для рисования с UIKit и Swift

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

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

Пока я рос, для меня рисование было связано с ручкой и бумагой, но сейчас это время прошло: и ручка, и бумага заменены компьютерами, и мобильными устройствами! Рисование может стать особенно увлекательным занятием на устройствах с системой распознавания прикосновений (touch-based devices), подтверждением чего является большое количество приложений по рисованию в App Store.

Хотите узнать, как сделать приложение для рисования самому? Хорошей новостью является то, что это довольно просто, благодаря некоторым крутым API для рисования, доступным в iOS.

В этом туториале вы создадите приложение очень похожее на Color Pad для iPhone. В процессе вы узнаете, как:

  1. рисовать линии и штрихи, используя Quartz2D;
  2. использовать несколько цветов;
  3. установить ширину мазка и непрозрачность;
  4. создать ластик;
  5. создать собственный селектор цвета RGB, и поделиться со всеми своим рисунком!

Хватайте карандаши и начнем!

Начало

Начните с загрузки проекта (ссылка с архивом).

Запустите Xcode, откройте проект и посмотрите на файлы внутри. Их не слишком много. Я добавил все необходимые изображения в предметный каталог и создал основной вид приложения со всеми необходимыми ограничениями. Весь проект основан на шаблоне Single View Application.

Теперь откройте Main.storyboard и посмотрите на интерфейс. Сверху у View Controller Scene три кнопки. Уже по названиям понятно, что они будут использоваться для очистки холста (reset), перехода на экран настроек (settings) и возможности поделиться своим рисунком (save). Внизу вы можете увидеть несколько кнопок с изображением карандашей и ластика. Они будут использоваться для выбора цвета.

И наконец есть два изображения называемые mainImageView и tempImageView - вы поймете позже, почему вам нужно два изображения, чтобы пользователь мог рисовать кистью с различными уровнями непрозрачности.

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

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

Рисуем быстро

Ваше приложение начнет работу с простой возможности рисования (Drawing Feature) при которой вы можете, проводя пальцем по экрану, рисовать простые черные линии. (Эй, даже Пикассо начал с азов).

Откройте ViewController.swift и добавьте следующие свойства классу:

var lastPoint = CGPoint.zero var red: CGFloat = 0.0 var green: CGFloat = 0.0 var blue: CGFloat = 0.0 var brushWidth: CGFloat = 10.0 var opacity: CGFloat = 1.0 var swiped = false Вот краткое описание переменных, используемых выше:

  1. lastPointзапоминает последнюю нарисованную точку на холсте. Используется, когда рисуется непрерывный мазок;
  2. red, green и blue - текущие значения RGB для выбранного цвета;
  3. brushWidth и opacity- ширина мазка и непрозрачность;
  4. swiped используется, когда мазок кисти непрерывен.

Все значения RGB по умолчанию 0, это означает, что рисунок будет пока черным. Непрозрачность по умолчанию установлена на 1.0, а ширина линии на 10,0.

Теперь часть, посвященная рисованию! Все методы, регистрирующие прикосновения взяты из родительского класса UIResponder. Они срабатывают в ответ на прикосновения начатого (began), перемещаемого (moved) или законченного (ended) события. Вы будете использовать все эти три метода для реализации вашей идеи рисования.

Начните с добавления следующего метода:

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
  swiped = false
  if let touch = touches.first{
    lastPoint = touch.locationInView(self.view)
  }
}

touchesBegan вызывается, когда пользователь ставит палец на экран. Это начало события рисования, поэтому сначала сбрасывается swiped на false, так как пока нет никакого движения. У вас также сохраняется локация прикосновения в lastPoint. Поэтому, когда пользователь начинает рисовать, двигая по экрану пальцем, вы можете отследить траекторию движения. Это тот самый момент, когда кисть соприкасается с бумагой! :)

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

func drawLineFrom(fromPoint: CGPoint, toPoint: CGPoint) {
 
  // 1
  UIGraphicsBeginImageContext(view.frame.size)
  let context = UIGraphicsGetCurrentContext()
  tempImageView.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height))
 
  // 2
  CGContextMoveToPoint(context, fromPoint.x, fromPoint.y)
  CGContextAddLineToPoint(context, toPoint.x, toPoint.y)
 
  // 3
  CGContextSetLineCap(context, CGLineCapRound)
  CGContextSetLineWidth(context, brushWidth)
  CGContextSetRGBStrokeColor(context, red, green, blue, 1.0)
  CGContextSetBlendMode(context, CGBlendModeNormal)
 
  // 4
  CGContextStrokePath(context)
 
  // 5
  tempImageView.image = UIGraphicsGetImageFromCurrentImageContext()
  tempImageView.alpha = opacity
  UIGraphicsEndImageContext()
 
}
 
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent) {
  // 6
  swiped = true
  if let touch = touches.first {
    let currentPoint = touch.locationInView(view)
    drawLineFrom(lastPoint, toPoint: currentPoint)
 
    // 7
    lastPoint = currentPoint
  }
}

Вот что происходит в этом методе:

  1. Первый метод отвечает за рисование линий между двумя точками. Помните, что это приложение имеет два изображения- mainImageView (который содержит "рисунок до сих пор") и tempImageView (который содержит "линию, которую вы в настоящее время рисуете"). Вы хотите нарисовать в tempImageView, поэтому вам нужно установить контекст рисования с изображением в настоящее время в tempImageView (которое должно быть пустой при запуске).
  2. Далее вы получите текущую точку касания, а затем рисуйте линию с CGContextAddLineToPoint от lastPoint к currentPoint. Вы можете подумать, что при этом подходе будет создан ряд прямых линий и результат будет выглядеть как набор зазубрен? Прямая будет создана, но регистрация прикосновений с экраном срабатывает так часто, что линии получатся достаточно короткими и в результате, будут выглядеть красивой гладкой кривой.
  3. Вот все параметры рисования для размера кисти и непрозрачности, и цвета мазка.
  4. Это то место, где происходит волшебство, и где вы фактически рисуете контур!
  5. Далее вам нужно свернуть контекст рисования, чтобы отобразить новую линию в tempImageView.
  6. В touchesMoved вы устанавливаете swiped как true и можете отслеживать, происходит ли в текущем времени движение свайп. Так как это touchesMoved, то да - свайп происходит! Затем вы вызываете вспомогательный метод, который вы только что написали, для того, чтобы нарисовать линию.
  7. Наконец, вы обновляете lastPoint, поэтому следующее событие начнется там, где вы остановились.

Затем добавьте финальный обработчик прикосновений:

override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
 
  if !swiped {
    // draw a single point
    drawLineFrom(lastPoint, toPoint: lastPoint)
  }
 
  // Merge tempImageView into mainImageView
  UIGraphicsBeginImageContext(mainImageView.frame.size)
  mainImageView.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height), blendMode: CGBlendModeNormal, alpha: 1.0)
  tempImageView.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height), blendMode: CGBlendModeNormal, alpha: opacity)
  mainImageView.image = UIGraphicsGetImageFromCurrentImageContext()
  UIGraphicsEndImageContext()
 
  tempImageView.image = nil
}

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

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

Заключительный шаг - это объединение tempImageView с mainImageView. Вы нарисовали мазок в tempImageView, а не в mainImageView. В чем смысл дополнительного UIImageView, когда вы можете просто нарисовать прямо на mainImageView? Да, вы можете, но двойные изображения используются чтобы сохранить прозрачность. Когда вы рисуете на tempImageView, непрозрачность установлена на 1.0 (полностью непрозрачный). Тем не менее, при слиянии tempImageView с mainImageView, вы можете установить непрозрачность tempImageView на выбранное значение, таким образом, устанавливая непрозрачность мазка кисти такой, какой вы хотите. Если вы рисовали бы прямо на mainImageView, то было бы невероятно трудно сделать мазки с разными значениями непрозрачности.

Итак, время начать рисовать! Создайте и запустите приложение. Вы увидите, что теперь вы можете рисовать черные линии на холсте!

Это отличный старт! С этими методами обработки прикосновений у вас появляется огромное количество функциональных возможностей. Теперь пришло время ознакомится с еще несколькими опциями, начиная с цвета.

Разноцветное приложение

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

let colors: [(CGFloat, CGFloat, CGFloat)] = [
  (0, 0, 0),
  (105.0 / 255.0, 105.0 / 255.0, 105.0 / 255.0),
  (1.0, 0, 0),
  (0, 0, 1.0),
  (51.0 / 255.0, 204.0 / 255.0, 1.0),
  (102.0 / 255.0, 204.0 / 255.0, 0),
  (102.0 / 255.0, 1.0, 0),
  (160.0 / 255.0, 82.0 / 255.0, 45.0 / 255.0),
  (1.0, 102.0 / 255.0, 0),
  (1.0, 1.0, 0),
  (1.0, 1.0, 1.0),
] 

Мы создаем массив значений RGB, где каждый элемент массива является кортежем из трех CGFloat. Цвета здесь совпадают с порядком цветов в интерфейсе, а также с тегом каждой кнопки.

Далее, найдем pencilPressed и добавим следующую реализацию:

// 1
var index = sender.tag ?? 0
if index < 0 || index >= colors.count {
  index = 0
}
 
// 2
(red, green, blue) = colors[index]
 
// 3
if index == colors.count - 1 {
  opacity = 1.0
}

Это короткий метод, но давайте посмотрим на него шаг за шагом:

  1. Во-первых вы должны знать, какой индекс цвета выберет пользователь. Существует много мест, где что-то может пойти не так - неправильный тег, тег не установлен, не хватает цвета в массиве - таким образом, здесь происходит несколько проверок. По умолчанию, если значение находится вне диапазона, то используется просто черный цвет, так как он первый.
  2. Далее установите свойства red, green и blue. Вы еще не в курсе, что могли установить несколько переменных с помощью кортежа? Значит это будет главным открытием дня по Swift! :]
  3. Последний цвет - это ластик, но он специфичен. Кнопка «ластик» устанавливает цвет на белый и непрозрачность на 1.0. Так как цвет фона тоже белый, то это даст вам очень удобный эффект ластика!

Итак, пробуем рисовать дальше? Давайте! - запускаем и готовимся к взрыву цвета! Теперь, нажав кнопку, вы меняете цвет мазка кисти, соответствующий цвету кнопки. Вот Малевич бы обрадовался!

«С чистого листа»

У всех великих художников бывают моменты, когда они, сделав шаг назад и встряхнув головой, бормочут: «Нет! Нет! Из этого никогда ничего не выйдет!» Вам нужна возможность очистить холст и начать заново. У Вас уже есть кнопка «Сброс» в вашем приложении для этого.

Найдите reset() и заполните реализацию метода следующим образом:

mainImageView.image = nil

И все! Хотите верьте, хотите нет! Код, который вы видите выше, устанавливает изображение на nil в mainImageView, и - вуаля - ваш холст очищается! Помните, что вы рисовали линии в image view’s image context и это значит, что если вы все обнулите, то это приведет к полному сбросу.

Запустите еще раз свой код. Нарисуйте что-то, а затем нажмите кнопку «Сброс», чтобы очистить ваш рисунок. Вот! Теперь нет необходимости идти и в отчаянии мять или рвать полотна.

Завершающие штрихи — настройки

Хорошо! Теперь у вас есть функциональное приложение для рисования, но ведь есть еще и второй экран настроек!

Для начала откройте SettingsViewController.swift и добавьте два следующих свойства классу:

var brush: CGFloat = 10.0
var opacity: CGFloat = 1.0

Это позволит вам отслеживать размер кисти и непрозрачность, которую выберет пользователь.

Затем добавьте следующую реализацию для sliderChanged():

if sender == sliderBrush {
  brush = CGFloat(sender.value)
  labelBrush.text = NSString(format: "%.2f", brush.native) as String
} else {
  opacity = CGFloat(sender.value)
  labelOpacity.text = NSString(format: "%.2f", opacity.native) as String
}
 
drawPreview()

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

Добавьте реализацию для drawPreview:

func drawPreview() {
  UIGraphicsBeginImageContext(imageViewBrush.frame.size)
  var context = UIGraphicsGetCurrentContext()
 
  CGContextSetLineCap(context, CGLineCapRound)
  CGContextSetLineWidth(context, brush)
 
  CGContextSetRGBStrokeColor(context, 0.0, 0.0, 0.0, 1.0)
  CGContextMoveToPoint(context, 45.0, 45.0)
  CGContextAddLineToPoint(context, 45.0, 45.0)
  CGContextStrokePath(context)
  imageViewBrush.image = UIGraphicsGetImageFromCurrentImageContext()
  UIGraphicsEndImageContext()
 
  UIGraphicsBeginImageContext(imageViewBrush.frame.size)
  context = UIGraphicsGetCurrentContext()
 
  CGContextSetLineCap(context, CGLineCapRound)
  CGContextSetLineWidth(context, 20)
  CGContextMoveToPoint(context, 45.0, 45.0)
  CGContextAddLineToPoint(context, 45.0, 45.0)
 
  CGContextSetRGBStrokeColor(context, 0.0, 0.0, 0.0, opacity)
  CGContextStrokePath(context)
  imageViewOpacity.image = UIGraphicsGetImageFromCurrentImageContext()
 
  UIGraphicsEndImageContext()
}

Этот метод использует те же приемы для создания предварительного просмотра настроек, какие использует и ViewController в методах обработки прикосновений. В обоих случаях метод рисует одну точку, а не линии: с соответствующей шириной и непрозрачностью, получаемой из значений ползунка.

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

Интеграция настроек

Есть еще одна важная деталь, которую мы упустили. Заметили какую?

Обновленные значения непрозрачности и ширина до сих пор никак не применялись в рисунке холста ViewController! Это потому, что вы еще не приобщали значения, указанные на экране настроек, с ViewController. Это подходящая работа для протокола делегирования.

Откройте файл SettingsViewController.swift и добавьте следующий код:

protocol SettingsViewControllerDelegate: class {
  func settingsViewControllerFinished(settingsViewController: SettingsViewController)
}

Это будет определять протокол класса с одним требуемым методом. Так мы создаем путь для экрана настроек, по которому он будет соотноситься с любой интересующей его областью настроек.

Кроме того, добавьте свойство классу SettingsViewController:

weak var delegate: SettingsViewControllerDelegate?

Это делается для ссылки на делегата. Если есть делегат, то вам нужно уведомить его, когда пользователь нажимает кнопку «Закрыть». Найдите close() и добавьте следующую строку в конец метода:

self.delegate?.settingsViewControllerFinished(self)

Это вызовет метод делегата, так что теперь он может обновлять себя новыми значениями.

Теперь откройте ViewController.swift и добавьте новое расширение класса для протокола в нижней части файла:

extension ViewController: SettingsViewControllerDelegate {
  func settingsViewControllerFinished(settingsViewController: SettingsViewController) {
    self.brushWidth = settingsViewController.brush
    self.opacity = settingsViewController.opacity
  }
}

Это объявляет класс, как соответствующий протоколу SettingsViewControllerDelegate и реализует свой единственный метод. В реализации все что нужно сделать, это установить текущую ширину кисти и непрозрачность, исходя из значений, взятых из настроек контрольной панели ползунков.

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

Добавьте следующий метод коррекции класса:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
  let settingsViewController = segue.destinationViewController as! SettingsViewController
  settingsViewController.delegate = self
  settingsViewController.brush = brushWidth
  settingsViewController.opacity = opacity
}

Когда пользователь использует переключатели перехода, нажимая кнопку Settings (настройки), этот метод настраивает новый SettingsViewController, установив себя в качестве делегата и перенося туда текущую кисть, и настройки непрозрачности.

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

Последние штрихи — выбор цвета пользователем

В настоящее время у вас есть 10 цветных кнопок на экране холста рисунка. Тем не менее, с пользовательским выбором цвета RGB художники, использующие ваше приложение, будут иметь возможность выбора любого цвета из доступных в диапазоне RGB.

Есть множество цветовых ползунков RGB в настройках экрана, которые будут реализованы вами в дальнейшем.

Поскольку вы уже ввели предварительный просмотр размера кисти и непрозрачности, вы могли бы сделать возможным и просмотр нового цвета кисти! :] Предварительный просмотр будет доступен в обоих изображениях, а также непрозрачность и предварительный просмотр кисти будут показаны в RGB цветах. Нет необходимости создавать дополнительное изображение, вы будете повторно использовать то, что у вас уже есть!

Откройте SettingsViewController.swift и добавьте следующие свойства:

var red: CGFloat = 0.0
var green: CGFloat = 0.0
var blue: CGFloat = 0.0

Вы будете использовать их для сохранения текущих значений RGB.

Теперь добавьте реализацию colorChanged:

red = CGFloat(sliderRed.value / 255.0)
labelRed.text = NSString(format: "%d", Int(sliderRed.value)) as String
green = CGFloat(sliderGreen.value / 255.0)
labelGreen.text = NSString(format: "%d", Int(sliderGreen.value)) as String
blue = CGFloat(sliderBlue.value / 255.0)
labelBlue.text = NSString(format: "%d", Int(sliderBlue.value)) as String
 
drawPreview()

Этот метод будет вызываться, когда вы перемещаете любой из RGB ползунков. Обратите внимание, как в выше написанном коде, все что вы делаете, обновляет значения свойств и обновляет ярлыки.

Если вы сейчас запустите свой проект, то заметите, что ваши изменения цвета не будут отображаться в превью. Чтобы они отображались, вам нужно внести небольшое изменение в drawPreview(). Найдите строку с CGContextSetRGBStrokeColor и замените все 0.0 значения с переменными красного, зеленого и синего цветов.

В первой половине этого метода замените вызов CGContextSetRGBStrokeColor следующим:

CGContextSetRGBStrokeColor(context, red, green, blue, 1.0)

А во второй части замените вызов CGContextSetRGBStrokeColor на следующее:

CGContextSetRGBStrokeColor(context, red, green, blue, opacity)

Теперь у вас есть образцы кистей и непрозрачности с правильными настройками, если вы хотите, чтобы они появлялись в тот момент, когда появляется экран настроек, то добавьте следующую реализацию viewWillAppear классу:

override func viewWillAppear(animated: Bool) {
  super.viewWillAppear(animated)
 
  sliderBrush.value = Float(brush)
  labelBrush.text = NSString(format: "%.1f", brush.native) as String
  sliderOpacity.value = Float(opacity)
  labelOpacity.text = NSString(format: "%.1f", opacity.native) as String
  sliderRed.value = Float(red * 255.0)
  labelRed.text = NSString(format: "%d", Int(sliderRed.value)) as String
  sliderGreen.value = Float(green * 255.0)
  labelGreen.text = NSString(format: "%d", Int(sliderGreen.value)) as String
  sliderBlue.value = Float(blue * 255.0)
  labelBlue.text = NSString(format: "%d", Int(sliderBlue.value)) as String
 
  drawPreview()
}

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

Запускаем наконец ViewController.swift. Как и прежде, вы должны убедиться, что текущий цвет соотносится с настройками экрана, для этого добавьте следующие строки в конце prepareForSegue:

settingsViewController.red = red
settingsViewController.green = green
settingsViewController.blue = blue

Это воздействует на текущий красный, зеленый и синий цвета, так что RGB ползунки установлены правильно.

Наконец, найдите settingsViewControllerFinished в расширении класса и добавьте следующие строки в этом методе:

self.red = settingsViewController.red
self.green = settingsViewController.green
self.blue = settingsViewController.blue

При закрытии SettingsViewController, мы получаем обновленные значения RGB.

Самое время перезапустить наше приложение! Установите значения своего Color Picker. Теперь RGB цвет, который отображается в превью, является дефолтным цветом для вашего холста!

Но что хорошего в том, что вы не можете поделиться своим искусством с кем-то другим? Хоть вы и не сможете прикрепить свой рисунок на холодильник, но вы сможете поделиться им со всем миром!

Последний этап. Поделись искусством!

Этот этап мы разделим на две части. Первая часть — это то, что мы должны получить изображение в качестве объекта UIImage, а затем мы просто передадим его в UIActivityViewController для того, чтобы решить, какие из опций по распространению нам подходят больше, исходя из того, какие аккаунты у нас есть.

В ViewController.swift запишите следующую реализацию метода share():

UIGraphicsBeginImageContext(mainImageView.bounds.size)
mainImageView.image?.drawInRect(CGRect(x: 0, y: 0, 
  width: mainImageView.frame.size.width, height: mainImageView.frame.size.height))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
 
let activity = UIActivityViewController(activityItems: [image], applicationActivities: nil)
presentViewController(activity, animated: true, completion: nil)

Этот метод достаточно простой: сначала он рендерит рисунок из mainImageView в новый UIImage. Затем UIActivityViewController выполняет остальную тяжелую работу за вас! А все что остается вам, так это передать массив того, чем вы хотите поделиться — в нашем случае всего одно изображение.

Второй параметр инициализатора applicationActivities позволяет вам ставить ограничения. Таким образом, передавая nil, это будет означать, что iOS предоставит столько вариантов для опции «share» (поделиться), сколько вообще возможно. Мы уверены, что ваши рисунки этого заслуживают!

Запустите приложение и создайте свой шедевр! Когда вы нажмете «Share», у вас появится возможность рассказать миру о своем таланте!

Источник http://www.raywenderlich.com/87899/make-simple-drawing-app-uikit-swift