Статьи

Гладкий рисунок от руки на iOS

Из этого туториала вы узнаете, как реализовать продвинутый алгоритм рисования для плавного рисования от руки на устройствах iOS. Читай дальше!

Touch — это основной способ взаимодействия пользователя с устройствами iOS. Одна из наиболее естественных и очевидных функций, которые эти устройства должны обеспечивать, — это возможность рисовать пальцем на экране. В настоящее время в App Store имеется множество приложений для рисования от руки и ведения заметок, и многие компании даже просят клиентов подписать iDevice при совершении покупок. Как на самом деле работают эти приложения? Давайте на минуту остановимся и подумаем о том, что происходит «под капотом».

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

Цель этого руководства — показать, как рисование от руки может быть реализовано в iOS, начиная с базового алгоритма, который выполняет интерполяцию по прямой линии, и переходя к более сложному алгоритму, который приближается к качеству, обеспечиваемому известными приложениями, такими как Penultimate . Как будто создание алгоритма, который работает не достаточно сложно, мы также должны убедиться, что алгоритм работает хорошо. Как мы увидим, наивная реализация чертежей может привести к появлению приложения со значительными проблемами производительности, что сделает рисование громоздким и в конечном итоге непригодным для использования.


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

Запустите новый проект Xcode iPad на основе шаблона « Single View Application » и назовите его « FreehandDrawingTut ». Обязательно включите автоматический подсчет ссылок (ARC), но отмените выбор раскадровок и модульных тестов. Вы можете сделать этот проект либо iPhone, либо универсальным приложением, в зависимости от того, какие устройства у вас есть для тестирования.

Новый проект

Далее, выберите «FreeHandDrawingTut» проект в Xcode Navigator и убедитесь, что поддерживается только книжная ориентация:

Только портрет поддержки

Если вы собираетесь развернуть iOS 5.x или более раннюю версию, вы можете изменить поддержку ориентации следующим образом:

1
2
3
4
— (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

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

Я хочу разрабатывать наш код итеративно, улучшая его поэтапно — как вы бы реально сделали, если бы начинали с нуля — вместо того, чтобы бросать на вас окончательную версию за один раз. Я надеюсь, что этот подход поможет вам лучше разобраться в различных проблемах. Имея это в виду и избавляя от необходимости многократно удалять, изменять и добавлять код в один и тот же файл, который может стать грязным и подверженным ошибкам, я выберу следующий подход:

  • Для каждой итерации мы будем создавать новый подкласс UIView. Я опубликую весь необходимый код, чтобы вы могли просто скопировать и вставить в файл .m нового созданного вами подкласса UIView. Не будет открытого интерфейса для функциональности подкласса представления, а это значит, что вам не нужно трогать файл .h.
  • Чтобы протестировать каждую новую версию, нам нужно будет назначить созданный нами подкласс UIView, чтобы он в настоящее время занимал весь экран. Я покажу вам, как это сделать в Интерфейсном Разработчике в первый раз, подробно пройдя все этапы, а затем напомню вам об этом шаге каждый раз, когда мы кодируем новую версию.

В XCode выберите File> New> File … , выберите класс Objective C в качестве шаблона и на следующем экране назовите файл LinearInterpView и сделайте его подклассом UIView . Сохрани это. Название «LinearInterp» здесь сокращенно от «линейная интерполяция». Ради учебника я назову каждый подкласс UIView, который мы создаем, чтобы подчеркнуть некоторую концепцию или подход, введенный в коде класса.

Как я уже упоминал ранее, вы можете оставить заголовочный файл без изменений. Удалите весь код, присутствующий в файле LinearInterpView.m, и замените его следующим:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#import «LinearInterpView.h»
 
@implementation LinearInterpView
{
    UIBezierPath *path;
}
 
— (id)initWithCoder:(NSCoder *)aDecoder // (1)
{
    if (self = [super initWithCoder:aDecoder])
    {
        [self setMultipleTouchEnabled:NO];
        [self setBackgroundColor:[UIColor whiteColor]];
        path = [UIBezierPath bezierPath];
        [path setLineWidth:2.0];
    }
    return self;
}
 
— (void)drawRect:(CGRect)rect // (5)
{
    [[UIColor blackColor] setStroke];
    [path stroke];
}
 
 
— (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint p = [touch locationInView:self];
    [path moveToPoint:p];
}
 
— (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint p = [touch locationInView:self];
    [path addLineToPoint:p];
    [self setNeedsDisplay];
}
 
— (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self touchesMoved:touches withEvent:event];
}
 
— (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self touchesEnded:touches withEvent:event];
}
@end

В этом коде мы работаем непосредственно с событиями касания, о которых приложение сообщает нам каждый раз, когда у нас есть последовательность касаний. то есть пользователь кладет палец на экранное изображение, перемещает палец по нему и, наконец, убирает палец с экрана. Для каждого события в этой последовательности приложение отправляет нам соответствующее сообщение (в терминологии iOS сообщения отправляются «первому ответчику»; вы можете обратиться к документации для получения подробной информации).

Чтобы справиться с этими сообщениями, мы реализуем методы -touchesBegan:WithEvent: и company, которые объявлены в классе UIResponder, от которого наследуется UIView. Мы можем написать код для обработки сенсорных событий так, как нам нравится. В нашем приложении мы хотим запросить расположение штрихов на экране, выполнить некоторую обработку, а затем нарисовать линии на экране.

Точки относятся к соответствующим закомментированным номерам из кода выше:

  1. Мы переопределяем -initWithCoder: потому что представление рождается из XIB, как мы вскоре настроим.
  2. Мы отключили несколько касаний: мы будем иметь дело только с одной последовательностью касаний, что означает, что пользователь может рисовать только одним пальцем за раз; любой другой палец, помещенный на экран в течение этого времени, будет игнорироваться. Это упрощение, но не обязательно неразумное — люди обычно тоже не пишут на бумаге двумя ручками одновременно! В любом случае, это не позволит нам отвлечься слишком далеко, поскольку у нас уже достаточно работы.
  3. UIBezierPath — это класс UIKit, который позволяет нам рисовать на экране фигуры, состоящие из прямых линий или определенных типов кривых.
  4. Поскольку мы делаем пользовательское рисование, нам нужно переопределить представление -drawRect: метод. Мы делаем это, обводя путь каждый раз, когда добавляется новый отрезок.
  5. Также обратите внимание, что, хотя ширина линии является свойством пути, цвет самой линии является свойством контекста рисования. Если вы не знакомы с графическим контекстом, вы можете прочитать о нем в документации Apple. А пока представьте, что графический контекст — это «холст», который вы рисуете при переопределении -drawRect: метод, и в результате вы видите представление на экране. Вскоре мы встретим другой вид контекста рисования.

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

  1. В панели навигатора нажмите ViewController.xib (если вы создали универсальное приложение, просто выполните этот шаг для файлов ViewController ~ iPhone.xib и ViewController ~ iPad.xib ).
  2. Когда представление появится на холсте конструктора интерфейса, щелкните по нему, чтобы выбрать его. На панели утилит нажмите «Инспектор удостоверений» (третья кнопка справа вверху панели). В самом верхнем разделе написано «Пользовательский класс», здесь вы установите класс представления, по которому щелкнули.
  3. Прямо сейчас должно появиться «UIView», но нам нужно изменить его на (как вы уже догадались) LinearInterpView . Введите имя класса (простой ввод «L» должен вызывать уверенность в автозаполнении).
  4. Опять же, если вы собираетесь тестировать это как универсальное приложение, повторите этот точный шаг для обоих файлов XIB, созданных для вас шаблоном.
Изменение класса представления контроллера представления на наш пользовательский подкласс UIView

Теперь создайте приложение. Вы должны получить блестящий белый вид, который вы можете нарисовать пальцем. Учитывая несколько строк кода, которые мы написали, результаты не слишком плохие! Конечно, они тоже не впечатляющие. Внешний вид с подключением точек довольно заметен (и да, мой почерк тоже отстой).

Наша первая попытка рисования от руки

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

Если вы некоторое время играете с приложением на своем устройстве, вы обязательно заметите что-то: в итоге отклик интерфейса пользователя начинает отставать, и вместо ~ 60 точек касания, которые были получены в секунду, по какой-то причине количество точек Пользовательский интерфейс может отбирать капли все дальше и дальше. Поскольку точки становятся все дальше друг от друга, интерполяция по прямой линии делает рисунок еще более «блочным», чем раньше. Это, конечно, нежелательно. Так что же происходит?


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

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

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

Создайте новый подкласс UIView, как вы делали ранее, назвав его CachedLIView (LI должен напомнить нам, что мы все еще делаем L inear Iterpolation). Удалите все содержимое CachedLIView.m и замените его следующим:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#import «CachedLIView.h»
 
@implementation CachedLIView
{
    UIBezierPath *path;
    UIImage *incrementalImage;
}
 
— (id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super initWithCoder:aDecoder])
    {
        [self setMultipleTouchEnabled:NO];
        [self setBackgroundColor:[UIColor whiteColor]];
        path = [UIBezierPath bezierPath];
        [path setLineWidth:2.0];
    }
    return self;
}
 
— (void)drawRect:(CGRect)rect
{
    [incrementalImage drawInRect:rect];
    [path stroke];
}
 
 
— (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint p = [touch locationInView:self];
    [path moveToPoint:p];
}
 
— (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint p = [touch locationInView:self];
    [path addLineToPoint:p];
    [self setNeedsDisplay];
}
 
— (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event // (2)
{
    UITouch *touch = [touches anyObject];
    CGPoint p = [touch locationInView:self];
    [path addLineToPoint:p];
    [self drawBitmap];
    [self setNeedsDisplay];
    [path removeAllPoints];
}
 
— (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self touchesEnded:touches withEvent:event];
}
 
— (void)drawBitmap // (3)
{
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, YES, 0.0);
    [[UIColor blackColor] setStroke];
    if (!incrementalImage) // first draw;
    {
        UIBezierPath *rectpath = [UIBezierPath bezierPathWithRect:self.bounds];
        [[UIColor whiteColor] setFill];
        [rectpath fill];
    }
    [incrementalImage drawAtPoint:CGPointZero];
    [path stroke];
    incrementalImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
}
@end

После сохранения не забудьте изменить класс объекта представления в ваших XIB на CachedLIView!

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

Опять же, ссылаясь на цифры в комментариях:

  1. Кроме того, мы сохраняем в памяти (закадровое) растровое изображение того же размера, что и наш холст (то есть на виде экрана), в котором мы можем сохранить то, что нарисовали до сих пор.
  2. Мы рисуем содержимое на экране в этот буфер каждый раз, когда пользователь поднимает палец (сигнализируется с помощью -touchesEnded: WithEvent).
  3. Метод drawBitmap создает растровый контекст — методам UIKit требуется «текущий контекст» (холст) для рисования. Когда мы внутри -drawRect: этот контекст автоматически становится доступным для нас и отражает то, что мы рисуем в нашем экранном представлении. Напротив, контекст растрового изображения должен быть создан и уничтожен явно, а нарисованное содержимое находится в памяти.
  4. Кэшируя предыдущий рисунок таким образом, мы можем избавиться от предыдущего содержимого пути и таким образом не допустить, чтобы путь становился слишком длинным.
  5. Теперь каждый раз, когда вызывается drawRect: мы сначала рисуем содержимое буфера памяти в нашем представлении, которое (по замыслу) имеет точно такой же размер, и поэтому для пользователя мы поддерживаем иллюзию непрерывного рисования, только иначе, чем перед.

Хотя это не идеально (что, если наш пользователь продолжит рисовать, не поднимая палец, когда-нибудь?), Этого будет достаточно для целей этого урока. Вам предлагается поэкспериментировать самостоятельно, чтобы найти лучший метод. Например, вы можете пытаться кэшировать чертеж периодически, а не только тогда, когда пользователь поднимает палец. Как это происходит, эта процедура кэширования вне экрана дает нам возможность фоновой обработки, если мы решим ее реализовать. Но мы не собираемся делать это в этом уроке. Тем не менее, вы можете попробовать сами!


Теперь давайте обратим наше внимание на то, чтобы рисунок выглядел лучше. До сих пор мы соединяли соседние точки касания прямыми отрезками. Но обычно, когда мы рисуем от руки, наш естественный штрих имеет свободный и извилистый (а не блочный и жесткий) вид. Имеет смысл, что мы пытаемся интерполировать наши точки кривыми, а не отрезками. К счастью, класс UIBezierPath позволяет нам нарисовать его тезку: кривые Безье.

Что такое кривые Безье? Без вызова математического определения кривая Безье определяется четырьмя точками: двумя конечными точками, через которые проходит кривая, и двумя «контрольными точками», которые помогают определить касательные, которые кривая должна касаться в своих конечных точках (это технически кубическая кривая Безье, но для простоты я буду называть это просто «кривой Безье»).

Кубическая кривая Безье

Кривые Безье позволяют нам рисовать всевозможные интересные фигуры.

Интересные формы, которые способны создать кубические Безье

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

Вы уже знаете тренировку. Создайте новый подкласс UIView и назовите его BezierInterpView . Вставьте следующий код в файл .m:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#import «BezierInterpView.h»
 
@implementation BezierInterpView
{
    UIBezierPath *path;
    UIImage *incrementalImage;
    CGPoint pts[4];
    uint ctr;
}
 
— (id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super initWithCoder:aDecoder])
    {
        [self setMultipleTouchEnabled:NO];
        [self setBackgroundColor:[UIColor whiteColor]];
        path = [UIBezierPath bezierPath];
        [path setLineWidth:2.0];
    }
return self;
     
}
 
— (void)drawRect:(CGRect)rect
{
    [incrementalImage drawInRect:rect];
    [path stroke];
}
 
 
— (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    ctr = 0;
    UITouch *touch = [touches anyObject];
    pts[0] = [touch locationInView:self];
}
 
— (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint p = [touch locationInView:self];
    ctr++;
    pts[ctr] = p;
    if (ctr == 3) // 4th point
    {
        [path moveToPoint:pts[0]];
        [path addCurveToPoint:pts[3] controlPoint1:pts[1] controlPoint2:pts[2]];
        [self setNeedsDisplay];
        pts[0] = [path currentPoint];
        ctr = 0;
    }
}
 
— (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
     
    [self drawBitmap];
    [self setNeedsDisplay];
    pts[0] = [path currentPoint];
    [path removeAllPoints];
    ctr = 0;
     
}
 
— (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self touchesEnded:touches withEvent:event];
}
 
— (void)drawBitmap
{
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, YES, 0.0);
    [[UIColor blackColor] setStroke];
    if (!incrementalImage) // first time;
    {
        UIBezierPath *rectpath = [UIBezierPath bezierPathWithRect:self.bounds];
        [[UIColor whiteColor] setFill];
        [rectpath fill];
    }
    [incrementalImage drawAtPoint:CGPointZero];
    [path stroke];
    incrementalImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
}
@end

Как указывают встроенные комментарии, основным изменением является введение пары новых переменных для отслеживания точек в наших сегментах Безье и модификация метода -(void)touchesMoved:withEvent: для рисования сегмента Безье для каждого четыре балла (фактически, каждые три балла, с точки зрения касаний, о которых нам сообщает приложение, потому что мы разделяем одну конечную точку для каждой пары смежных сегментов Безье).

Здесь вы можете указать, что мы пренебрегли случаем, когда пользователь поднимает палец и заканчивает последовательность касаний, прежде чем у нас будет достаточно очков, чтобы завершить наш последний сегмент Безье. Если это так, вы были бы правы! Хотя визуально это не имеет большого значения, в некоторых важных случаях это имеет значение. Например, попробуйте нарисовать маленький круг. Возможно, он не закроется полностью, и в реальном приложении вы захотите соответствующим образом обработать это в -touchesEnded:WithEvent . Пока мы на этом, мы также не уделяем особого внимания случаю отмены касания. Метод touchesCancelled:WithEvent обрабатывает это. Взгляните на официальную документацию и посмотрите, есть ли какие-либо особые случаи, которые вам, возможно, придется здесь решать.

Итак, как же выглядят результаты? Еще раз напоминаю вам установить правильный класс в XIB перед сборкой.

Незначительное улучшение, если есть

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


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

Итак, что мы можем с этим поделать? Если мы собираемся придерживаться подхода, который мы начали в последней версии (то есть, используя кривые Безье), нам нужно позаботиться о непрерывности и гладкости в «точке соединения» двух соседних сегментов Безье. Две касательные в конечной точке с соответствующими контрольными точками (вторая контрольная точка первого сегмента и первая контрольная точка второго сегмента) кажутся ключевыми; если бы обе эти касательные имели одинаковое направление, то кривая была бы более гладкой на стыке.

Касательные являются соединением двух сегментов Безье

Что если мы переместим общую конечную точку где-нибудь на линии, соединяющей две контрольные точки? Без использования дополнительных данных о точках соприкосновения наилучшей точкой, по-видимому, будет средняя точка линии, соединяющей две рассматриваемые контрольные точки, и наше навязанное требование относительно направления двух касательных будет выполнено. Давайте попробуем это!

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

Создайте подкласс UIView (еще раз) и назовите его SmoothedBIView. Замените весь код в файле .m следующим текстом:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
#import «SmoothedBIView.h»
 
@implementation SmoothedBIView
{
    UIBezierPath *path;
    UIImage *incrementalImage;
    CGPoint pts[5];
    uint ctr;
}
 
— (id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super initWithCoder:aDecoder])
    {
        [self setMultipleTouchEnabled:NO];
        [self setBackgroundColor:[UIColor whiteColor]];
        path = [UIBezierPath bezierPath];
        [path setLineWidth:2.0];
    }
    return self;
     
}
— (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self setMultipleTouchEnabled:NO];
        path = [UIBezierPath bezierPath];
        [path setLineWidth:2.0];
    }
    return self;
}
 
 
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
— (void)drawRect:(CGRect)rect
{
    [incrementalImage drawInRect:rect];
    [path stroke];
}
 
 
— (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    ctr = 0;
    UITouch *touch = [touches anyObject];
    pts[0] = [touch locationInView:self];
}
 
— (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint p = [touch locationInView:self];
    ctr++;
    pts[ctr] = p;
    if (ctr == 4)
    {
        pts[3] = CGPointMake((pts[2].x + pts[4].x)/2.0, (pts[2].y + pts[4].y)/2.0);
         
        [path moveToPoint:pts[0]];
        [path addCurveToPoint:pts[3] controlPoint1:pts[1] controlPoint2:pts[2]];
 
        [self setNeedsDisplay];
        // replace points and get ready to handle the next segment
        pts[0] = pts[3];
        pts[1] = pts[4];
        ctr = 1;
    }
     
}
 
— (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self drawBitmap];
    [self setNeedsDisplay];
    [path removeAllPoints];
    ctr = 0;
}
 
— (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self touchesEnded:touches withEvent:event];
}
 
— (void)drawBitmap
{
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, YES, 0.0);
     
    if (!incrementalImage) // first time;
    {
        UIBezierPath *rectpath = [UIBezierPath bezierPathWithRect:self.bounds];
        [[UIColor whiteColor] setFill];
        [rectpath fill];
    }
    [incrementalImage drawAtPoint:CGPointZero];
    [[UIColor blackColor] setStroke];
    [path stroke];
    incrementalImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
}
 
@end

Суть алгоритма, который мы обсуждали выше, реализована в методе -touchesMoved: WithEvent : . Встроенные комментарии должны помочь вам связать обсуждение с кодом.

Итак, каковы результаты, визуально говоря? Не забудьте сделать это с XIB.

Совсем неплохо!

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


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

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

Если ваше приложение выполняет больше работы, чем то, что мы делали здесь (и в приложении для рисования, которое стоит отгрузить, то было бы!), Его разработка таким образом, чтобы код не-пользовательского интерфейса (в частности, кэширование вне экрана) выполнялся в фоновом потоке, мог существенно изменить ситуацию на многоядерном устройстве (iPad 2 и выше). Даже на однопроцессорном устройстве, таком как iPhone 4, производительность должна улучшиться, так как я ожидаю, что процессор разделит работу кэширования, которая, в конце концов, происходит только один раз в несколько циклов основного цикла.

Я призываю вас напрячь свои навыки кодирования и поиграть с UIKit API, чтобы развить и улучшить некоторые идеи, реализованные в этом руководстве. Веселитесь, и спасибо за чтение!