Pull to refresh

Введение в Core Graphics на iOS

Reading time 6 min
Views 41K
Недавно обнаружил, что на хабре нет статей по работе с Core Graphics в iOS, также как не нашел подобных статей на русском языке. На сайте Apple для разработчиков есть документация по работе с 2D графикой в iOS — Core Graphics Framework Reference, включающее 400 страниц документации, полезной в качестве справки, но не дающей начального представления о работе с видами и рисованием. Поэтому я решил написать вводную статью по работе с 2D графикой в iOS.

Введение


Известно, что Cocoa Touch не включает AppKit, предоставляющий классы и методы для работы интерфейсом в Mac OS X. Вместо AppKit на платформе iOS для работы с 2D используется низкоуровневый Cи-фреймворк Core Graphics, который также является частью Mac OS X SDK, но используется реже, так как Си-функции и работа с памятью не настолько удобные и гибкие, как классы Objective-C в AppKit. Кроме того, очевидно, что при работе с Core Graphics приходится писать больше кода, который сложнее расширять и поддерживать. В этой статье мы создадим Custom View и познакомимся с возможностями фреймворка.

Начало работы


Для начала нам нужно создать приложение и добавить в него Custom View, на котором далее мы будем рисовать наши объекты.
Создадим в Xcode View-based Application и назовем его Graphics.
image

Далее добавим наш View (Cmd+N) и назовем его MyCanvas,
image
который будет классом Objective-C и сабклассом UIView.

Всё «рисование» происходит в методе:

- (void)drawRect:(CGRect)rect
{
// Рисуем тут
}


Следует помнить, что мы никогда не вызываем напрямую метод -drawRect:. Мы только реализуем данный метод, который выполняется при инициализации View.
Для обновления содержимого, когда нам нужно обновить данный View или какую-то его часть (aRect), мы используем методы UIView
-(void)setNeedsDisplay;
-(void)setNeedsDisplayInRect:(CGRect)aRect;

соответственно.

Нарисуем простой квадрат с помощью метода drawRect:. Все рисование происходит в контексте (context), который определяет, где происходит рисование. Чтобы получить context используется Си-функция UIGraphicsGetCurrentContext. В iOS центр координат при рисовании находится в левом верхнем углу, а не в левом нижнем, как у Mac OS X.

- (void)drawRect:(CGRect)rect
{
// Получим context
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextClearRect(context, rect); // Очистим context

CGContextSetRGBFillColor(context, 255, 0, 0, 1);
CGContextFillRect(context, CGRectMake(20, 20, 100, 100));
}


Очищать контекст необходимости в данном случае нет, так как мы не будем перерисовывать нашу графику, однако это хорошая практика очищать графические элементы перед рисованием. Следует учесть, что после очистки View станет чёрным.

Функция CGContextSetRGBFillColor устанавливает цвет заливки для данного контекста, а функция CGContextFillRect рисует квадрат.

Теперь запустим наше приложение в симуляторе (Cmd+R), и как мы видим, ничего не произошло. Дело в том, что мы не создали наш View и не добавили его в иерархию.
Существует два способа создания View, в том числе и Custom View — Interface Builder и непосредственно с помощью кода. Что касается IB, всё просто, добавляем элемент UIView перетаскиванием и устанавливаем ему Custom Class: MyView в Identity Inspector.
Для того, чтобы создать наш View вручную создадим его в GraphicsViewController.m:

- (void)viewDidLoad
{
[super viewDidLoad];
MyCanvas *myView = [[MyCanvas alloc] initWithFrame:self.view.bounds];
[self.view addSubview:myView];
}


Для добавления View в иерархию используется addSubview:, для удаления из иерархии removeFromSuperview:. При этом первый метод отправяется супервиду, а второй виду, который нужно удалить.

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

Теперь, если мы запустим наше приложение, то увидим красный квадрат с отступами в 20 поинтов.
При рисовании мы используем поинты (pt), а не пиксели (px), для того, чтобы размеры были одинаковы для Retina и обычного дисплея. Работа с пикселями также возможна. Для определения типа экрана у конкретного устройства используется свойство @property CGFloat contentScaleFactor; которое возвращает «масштаб» (сколько пикселей в поинте для данного view на экране). Значение может быть 2.0 для retina или 1.0 для обычного экрана.
image

Рассмотрим работу с другими примитивами:
1) Окружность c синей заливкой, создается методом CGContextFillEllipseInRect, и вписывается в прямоугольник, таким образом можно создавать эллипсы.

CGContextSetRGBFillColor(context, 0, 0, 255, 1);
CGContextFillEllipseInRect(context, CGRectMake(30, 140, 80, 80));


2) Окружность без заливки с зеленый контуром и толщиной линии 3, толщина линии задается методом CGContextSetLineWidth, а окружность без заливки рисуется методом CGContextStrokeEllipseInRect.

CGContextSetRGBStrokeColor(context, 0, 255, 0, 1);
CGContextSetLineWidth(context, 3.0);
CGRect circleRect = CGRectMake(140, 20, 100, 100);
CGContextStrokeEllipseInRect(context, circleRect);


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

CGContextSetRGBStrokeColor(context, 255, 0, 255, 1);
CGPoint points[6] = {CGPointMake(70, 20), CGPointMake(120, 120),
CGPointMake(120, 120), CGPointMake(20, 120),
CGPointMake(20, 120), CGPointMake(70, 20)};
CGContextStrokeLineSegments(context, points, 6);


4) Безье сплайн. В данном случае нужно задать начальную и конечную точки, а также координаты точек, задающих кривизну. Для тех, кто не знаком с кривыми Безье, краткий краш-курс:
image
Зададим точки отдельными CGPoint’ами, чтобы не путаться:

CGPoint bezierStart = {20, 260};
CGPoint bezierEnd = {300, 260};
CGPoint bezierHelper1 = {80, 320};
CGPoint bezierHelper2 = {240, 320};
CGContextBeginPath(context);
CGContextMoveToPoint(context, bezierStart.x, bezierStart.y);
CGContextAddCurveToPoint(context,
bezierHelper1.x, bezierHelper1.y,
bezierHelper2.x, bezierHelper2.y,
bezierEnd.x, bezierEnd.y);
CGContextStrokePath(context);


Нарисуем более сложный элемент, например, синусойду:

CGContextSetRGBStrokeColor(context, 255, 255, 255, 1);
int y;
for(int x=rect.origin.x; x < rect.size.width; x++)
{
y = ((rect.size.height/6) * sin(((x*4) % 360) * M_PI/180)) + 380;
if (x == 0) CGContextMoveToPoint(context, x, y);
else CGContextAddLineToPoint(context, x, y);
}
CGContextStrokePath(context);


В 5й строке мы добавили 380 поинтов, чтобы сместить фукнцию по вертикали вниз.

В итоге экран нашего айфона после запуска приложения будет выглядеть так:
image

Работа с текстом


Наиболее удобный способ, для того, чтобы вывести текст на экран — использовать объект UILabel, который имеет множество методов преобразования текста. Однако, встречаются ситуации, когда нужно нарисовать текст в методе -drawRect, например, если мы хотим повернуть текст.

Рисование с помощью Core Graphics:

char *txt = "My CG text"; // создаем символьный массив, который выведем на экран
CGContextSelectFont(context, "Helvetica", 18.0, kCGEncodingMacRoman); // выбираем шрифт
CGContextSetTextDrawingMode(context, kCGTextFill); // выбираем вариант отображения текста: kCGTextFill (заливка) или kCGTextStroke (контур)
CGContextShowTextAtPoint(context, 20, 280, txt, strlen(txt)); // выводим текст на экран


Для выбора шрифта, размера, стиля отображения, метрики можно использовать объект UIFont. А с помощью метода [UIFont familyNames] можно получить массив доступных шрифтов.

Если мы хотим использовать UILabel и при этом создать его программно, а не в Interface Builder, добавим следующий код в наш GraphicsViewController.m:

UILabel *scaleNumber = [[UILabel alloc] initWithFrame:CGRectMake(160, 140, 140, 21)]; //создаем объект, который будет являться нашим View
scaleNumber.textColor = [UIColor yellowColor]; //задаем цвет текста
scaleNumber.backgroundColor = [UIColor colorWithWhite:0 alpha:0]; //создаем прозрачный фон, чтобы наш текст не был в белом (или другого цвета) прямоугольнике
scaleNumber.text = @"Vitaly Ishkulov"; //какой-нибудь текст
scaleNumber.adjustsFontSizeToFitWidth = YES; //можно сделать, чтобы текст автоматически уменьшался, если не помещается, при этом увеличиваться больше заданного размера (или системного, если размер не задан) текст не будет
[myView addSubview:scaleNumber]; //добавляем наш текст в иерархию View
[scaleNumber release]; //освобождаем из памяти, так как мы передали управление/владение текстом нашему виду myView


Заключение и немного слов о работе с памятью


Для освобождения из памяти views, не используют [myView release]; Вместо этого создадим метод releaseOutlets:
- (void)releaseOutlets {
self.myView = nil;
}

И вызовем данный метод из viewDidUnload и dealloc:
- (void)viewDidUnload
{
[super viewDidUnload];
[self releaseOutlets];
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}

- (void)dealloc
{
[self releaseOutlets];
[super dealloc];
}

Немного странное выражение self.myView = nil; на самом деле легко объясняется, если взглянуть на сеттер, который синтезирует для нас комманда synthesize myView:
- (void)setMyView:(MyCanvas *)anObject
{
if (anObject != myView) {
[myView release];
myView = [anObject retain];
}
}

Если anObject = nil, то метод последняя строка в методе присваивает myView nil, а предыдущая освобождает из памяти текущий View — то, что мы и хотели сделать.

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

Ссылки для дальнейшего изучения:
View Programming Guide for iOS
Core Graphics Framework Reference
Tags:
Hubs:
+32
Comments 19
Comments Comments 19

Articles