DrawingIniOS
This page contains details about how to draw custom content on iOS. This involves various technologies / frameworks, such as UIKit, CoreGraphics, CoreAnimation, etc. The foundation for understanding how to
Glossary
- Quartz
- More a (fuzzy) marketing name than a real technology. The following description can be found in the "Drawing and Printing Guide for iOS" [1]: "Quartz is the general name for the native window server and drawing technology in iOS. The Core Graphics framework is at the heart of Quartz, and is the primary interface you use for drawing content."
- OpenGL ES
- OpenGL for Embedded Systems. OpenGL itself stands for "Open Graphics Library".
- Native drawing technologies
- Everything that is not OpenGL ES.
- Core Graphics, CG
- Function-oriented C-based API for drawing. Core Graphics is a framework (CoreGraphics.framework), the main header is
<CoreGraphics/CoreGraphics.h>
. Importing this header is usually not necessary if UIKit is also used, since most UIKit header files already include the CoreGraphics main header. - Quartz 2D
- An API that is used for 2-dimensional drawing. Quartz 2D is a subset of the Core Graphics API. Because of this, Quartz 2D functions use the
CG
prefix. It is not clear which Core Graphics functions and/or data types are not part of Quartz 2D. - UIKit
- Object-oriented Objective-C API for drawing. UIKit provides wrappers for some Core Graphics structures (e.g.
UIColor
instead ofCGColor
), and in all cases uses Core Graphics as the backend for drawing. UIKit is a framework (UIKit.framework), the main header is<UIKit/UIKit.h>
. - Core Animation, CA
- Core Animation is an Objective-C API for creating animations. Core Animation does not provide primitive drawing routines for creating shapes, images, or other types of content. Instead, it is a technology for manipulating and displaying content that were created using other technologies. For this reason, one sometimes encounters Core Animation through other technologies' APIs (e.g. block animation methods in
UIView
). When Core Animation functionality occurs as distinct separate classes or functions, though, it is located in the QuartzCore framework (e.g.CALayer
class). - QuartzCore
- A framework (QuartzCore.framework), the main header is
<QuartzCore/QuartzCore.h
. QuartzCore is a collection of other technologies. On iOS it mostly consists of Core Animation stuff, but on the Mac it also contains Core Image. - Graphics Context
- Drawing operations operate on a graphics context. A graphics context defines the destination for drawing operations. The opaque data type
CGContextRef
is used to represent a graphics context. There are the following types of graphics contexts: Bitmap, PDF, Window, Layer and (in Mac OS X only) PostScript. To obtain a graphics context of the desired type - Coordinate system
- The graphics context defines which coordinate system is used. For instance, the default coordinate system in Core Graphics (which for instance is used for PDF or bitmap graphics contexts) has its origin in the lower-left corner, whereas a window graphics context provided by UIKit (
UIGraphicsGetCurrentContext
) has a coordinate system with the origin set in the upper left corner. UIKit achieves this switch simply by modifying the CTM. - CTM
- Current Transformation Matrix. A mathematical matrix that maps points in a graphics context's coordinate system to points on the graphics context's drawing destination.
- Path
- A path is a description of a 2D geometric scene that uses a sequence of lines and Bézier curves to represent that scene. Both Core Graphics and UIKit provide functions for creating paths.
- Identity transform
- An affine transformation that does not change its operand. The result of applying the identity transform to a coordinate pair is the same identical coordinate pair.
Resources
- Drawing and Printing Guide for iOS [1]
- Quartz 2D Programming Guide [2]
- CGContext Reference [3]
- CGeometry Reference [4]
- Understanding UIKit Rendering (WWDC 2011 video) [5]
- Optimizing 2D Graphics and Animation Performance (WWDC 2012 video) [6]
General Notes
- Drawing must take place in the main thread, UIKit classes generally are not thread-safe
- Regardless of how drawing is done (OpenGL, Quartz, etc.), it always happens inside a UIView
- Drawing operations use points and a logical coordinate system, these are possibly different from the physical pixels displayed on the screen; the pixel-to-point ration is exposed by
- UIView's
contentScaleFactor
property - CALayer's
contentsScale
property - For instance, a scale factor of 2.0 means that 1 point equals 2 pixels
- UIView's
Understanding UIKit Rendering
This section is a summary of the WWDC 2011 video "Understanding UIKit Rendering" [5].
Coordinate systems
UIKit vs. CoreAnimation
- UIKit coordinate systems always have their origin in the top-left corner and their x/y axis pointing to the right and downwards. This is true for all devices.
- CoreAnimation has different coordinate system s that depend on the device (e.g. iPhone devices have their origin in the top-left corner, the original iPad has its origin in the top-right corner, while later iPad devices apparently have their origin in the bottom-left corner)
- Units in a UIKit coordinate systems are points, not pixels
- Units in CoreAnimation coordinate systems are pixels
- The recommendation is to not use CoreAnimation coordinate systems because UIKit coordinate systems are much more consistent
UIScreen
- The UIScreen coordinate system is a UIKit coordinate system
- The UIScreen coordinate system has its origin in the top-left corner of the screen when the device is held in portrait orientation. The x/y axis are pointing to the right and downwards.
- The UIScreen coordinate system is fixed to the physical device screen, so when the device is rotated, the coordinate system origin and the direction of the axis' also rotate! For instance, if the device is rotated to the left into landscape orientation, the origin is now in the bottom-left corner of the screen and the x/y axis are pointing upwards and to the right.
UIView
- Every UIView has its own coordinate system with an origin at 0/0
- UIView coordinate systems are UIKit coordinate systems
- UIView coordinate systems take device orientation into account, i.e. the coordinate system origin is in the top-left corner of the screen as it is currently held by the user! TODO: Is this true?
UIView location and size
Location
- The location of a UIVIew on the screen is determined by its center point
- The center point of a UIVIew is expressed in the coordinate system of its superview
- TODO: Can the center of a UIVIew be at a coordinate with a fractional value (when the view size is an odd number)?
Size
- The size of a UIView is determined by the size of its bounds (see below) plus any transforms
Bounds
- The bounds of a UIView is a rectangle with an origin and a size
- The bounds rectangle is expressed in the UIView's own coordinate system
- The bounds rectangle defines the visible region of the UIVIew's content. This is important only if the UIView's content size is larger than the UIView's bound size.
- The bounds rectangle is expressed before transforms are taken into account to calculate the frame.
Frame
- The frame rectangle is the smallest rectangle that fully encloses the UIView in its superview's coordinate system. This definition is important because of transforms.
- Although UIView and CALayer have a
frame
property, the value of that property is not stored somewhere, it is computed from a number of properties listed in the table further down - Most important to know here is that transforms contribute to the frame computation, so the frame rectangle size and the bounds rectangle size are not necessarily the same!
- The frame rectangle is expressed in the coordinate system of its superview
- This means that changing the frame will change some of the underlying properties that it is computed from. The underlying properties that are changed are
- The center point of the view
- The bounds size
The following table lists the components that are used to compute the frame of a view or layer. The table is presented at 13:20 of the video.
UIView Property | CALayer Property | Coordinate Space | Remarks |
---|---|---|---|
bounds.size | bounds.size | View | The bounds determines the layer's size. Setting the view's bounds also sets the layer's bounds. |
center | position | Superview | Setting the view's center also sets the layer's position. |
transform | affineTransform | Superview | This property has the type CGAffineTransform . Not sure what happens if the view's transform is set
|
- | transform | Superlayer | This property has the type CATransform3D .
|
- | anchorPoint | Layer (Unit Coordinates) | Changes how the other properties are interpreted. Has something to do with the difference between "center" and "position". Is not explained in the WWDC video, they tell you not to worry about this property, or to read the documentation if you do need to know about it. |
Transforms
- Transforms are always applied about the center point of a UIView, not the origin of the frame! This is important to know when applying scale and rotation transforms. It is not important for translate transforms because those change both the frame origin and the center point for the same value.
TODO: How do the various transforms affect the frame? Scale = Makes the frame smaller/larger in relation to the bounds. Rotate = Rotates the frame around the center point. Translate = ?. Other transforms?
Putting everything together
As I currently understand it, this is the rough sequence of how a view's content is drawn on the screen:
- A view's content is put together. For instance
- Subviews in a given layout
- An image
- Something else
- The visible region of the view content is determined by looking at the view's bounds
- Transforms are applied to the visible region, and the content within, resulting in a rectangle that has the size of the frame
- The resulting rectangle is placed within its superview at view's center point. TODO: Is this correct?
Additional notes:
- Setting the UIView property
clipsToBounds
to YES prevents the view contents from "spilling" outside of the UIView's bounds
UIView and CALayer
- The relationship between UIView and CALayer is "has-a", i.e. composition
- The content displayed by a UIVIew is coming from the
view.layer.contents
property- This property should not be set directly - it is set by UIKit through a variety of other means
- For instance, when a UIView's
drawRect:
returns, the bitmap painted inside that method is used to set thecontents
property of the view's layer view.layer.contents
has the typeid
; it may be a CGImageRef, but can also be something else (it's unclear what it is).
Optimizing 2D Graphics and Animation Performance
This section is a summary of the WWDC 2012 video "Optimizing 2D Graphics and Animation Performance" [6].
Retina Displays
- 1 point != 1 pixel
- The "scale factor" expresses how many pixels per point there are on a device
- UIView:
contentScaleFactor
property - CALayer:
contentsScale
property - The propery for UIView objects should be set up correctly
- The property for CALayer objects created by UIKit should be set up correctly
- The property for CALayer objects that the developer creates must be set manually. The canonical way how to do this is this:
layer.contentsScale = [UIScreen mainScreen].scale;
- The property must be set only if the view or layer content is high-resolution. For instance, if an image is still at 1x resolution, the property must not be set. The effect will be that the 1x resolution image is scaled up - although this will look ugly, the image will at least be rendered with the correct dimensions.
- The CGContext provided in the view or layer's drawing method is already set up with the correct scale factor
- Drawing into a CGContextBitmap: The context needs to be set up with pixel, not point dimensions, and the drawing must be scaled up appropriately.
- Alternatively, use
UIGraphicsBeginImageContextWithOptions
to create a bitmap context. Specify 0 as the scale factor and UIKit will figure out the correct scale factor for the device screen. The context can thus be set up with point dimensions.
Core Animation Instrument
- When the Core Animation instrument is running (only when it is running, not when it is stopped), a number of useful debugging options are available
- "Flash Updated Regions" shows if more parts of the screen than necessary were updated
- "Color Misaligned images" shows images that are not on pixel boundaries
- "Color Offscreen-Rendered Yellow" shows if something has been rendered offscreen. Offscreen-rendering is slow and should be avoided if possible. This is explained more in-depth in Understanding UIKit Rendering.
- "Color Hits Green and Misses Red" shows if offscreen-rendered content could be re-used from a cache (green) or not (red)
- "Color Blended Layers" shows areas that are NOT opaque. This is important because opaque views can be rendered much more quickly.
Optimizing drawing
- Instead of invoking
setNeedsDisplay
, you can invokesetNeedsDisplayInRect:
- This invalidates only a particular rectangle instead of the entire view or layer.
- On the next call to the drawing method of a UIView or CALayer, the graphics context will be automatically set up with a clipping rectangle
- The implementation of the drawing method does not need to do anything special, any drawing made outside of the clipping rectangle will simply be discarded
Drawing with CoreGraphics
Drawing basics
- The origin of the CoreGraphics coordinate system is at the bottom-left corner
- In order to implement custom drawing, one needs to override either UIView's
drawRect:
or CALayer'sdrawLayer:inContext:
methods - Before either of these methods is invoked by the system, a graphics context (
CGContextRef
) is created that must be used the subsequent drawing operation- CALayer passes the graphics context to
drawLayer:inContext:
- UIView does not pass the graphics context, one needs to obtain it like this:
- CALayer passes the graphics context to
CGContextRef myContext = UIGraphicsGetCurrentContext();
- The graphics context may already be set up to account for certain view or layer properties (e.g. the scale factor, if one is set)
- The current transformation matrix (CTM) can be used to add scaling, rotation, and translation factors to the actual drawing operations
More on graphics contexts
- Contexts can be saved and restored using the functions
UIGraphicsPushContext()
andUIGraphicsPopContext()
CGBitmapContextCreate
orCGPDFContextCreate
create custom graphics contexts. Important: The coordinate system in these contexts is different from the view's coordinate system - the origin is in the bottom-left corner. This must be taken into account when the content of the context is rendered in a view- A bitmap context draws into a memory buffer; a PDF context records drawing commands and writes them to a file or sends them to a data consumer
- An image can be created from a bitmap context by invoking
CGBitmapContextCreateImage()
. This results in aCGImageRef
that can then be drawn onto the view usingCGContextDrawImage()
; the function must be supplied with the view's graphics context and the bitmap object - See the section Creating a Bitmap Graphics Context in the Quartz 2D Programming Guide.
Types
- Coordinates use floating point values
- Sometimes CGPoint is used to refer to a x/y coordinate
- Sometimes CGRect is used to refer to a rectangle (x/y coordinate of the lower-left corner, together with a width and height)
Creating a path
- This seems to work like Turtle Graphics :-)
- Creating a path does not draw it!
- Some path functions use the current point to form the requested path
CGContextBeginPath()
starts a path; if a previous path exists in the context, it is discardedCGContextMoveToPoint(context, 17.0, 42.0)
sets the current point; after a path is started, this is required to establish the path's initial starting point- The path can now be extended with lines, arcs, curves and rectangles
CGContextClosePath
closes the current sub-path by connecting the current point to the starting point; calling this function is optionalCGContextAddLineToPoint(context, 17.0, 42.0)
draws a line from the current point to the specified end point; the end point becomes the new current pointCGContextAddLines(context, arrayOfCGPoint, count)
draws a series of lines, starting from the current pointCGContextAddArc(context, xCenter, yCenter, radius, startAngle, endAngle, clockWise)
draws an arc that is a segment of a circle; angles are measured in radians.CGContextAddArcToPoint()
is an alternative way to paint an arc, using 2 tangentsCGContextAddCurveToPoint()
draws a cubic Bézier curve from the current point, using specified control points, to the specified end pointCGContextAddQuadCurveToPoint()
draws a quadratic Bézier curve from the current point to the specified end point, using the specified control pointCGContextAddEllipseInRect()
draws an ellipse that is bounded by the specified rectangle; the ellipse is approximated using a sequence of Bézier curvesCGContextAddRect()
draws a rectangle using the specified CGRect; The current point has no influence on the rectangle.- Important: This function uses
CGRectGetMaxX()
andCGRectGetMaxY()
to calculate the points that form the path. As a result, the rectangle that is drawn is 1 pixel wider/higher than the width/height of the specified CGRect (if a line width of 1 is used to stroke the path) - Example: The CGRect specified has an origin x-cordinate of 0, and a width of 12.
CGRectGetMinX()
therefore returns 0, andCGRectGetMaxX()
returns 12. As a result,CGContextAddRect()
will draw horizontal lines that go from a point at x-coordinate 0 to a point at x-coordinate 12 - which means a line that is 13 pixels long!
- Important: This function uses
- When a path is painted, it is flushed from the graphics context.
CGPathRef
andCGMutablePathRef
are used to create reusable paths; the functions above have duplicates that operate on a CGPath instead of a graphics context.CGContextAddPath
is used to apply aCGPath
object to a graphics context - UIBezierPath is an Objective-C wrapper for some of the path-related functions above
Painting a path
- Stroking or filling, or both, is used to paint a path
- Stroking paints the line, filling paints the area inside the path
- Stroke and fill attributes are properties of the graphics context
- Normally a paint function uses the current path
- There are also convenience functions that allow to specify a path to be painted, together with the actual paint operation. For instance,
CGContextFillRect
immediately paints a filled rectangle
Affine Transformations
References
- The section "Transforms" in the "Quartz 2D Programming Guide" [7]
- CGAffineTransform Reference [7]
- CALayer Class Reference [7]
Basics
Affine transformations allow the user to express the following geometric changes that should be applied to drawing operations:
- Translation
- Scaling
- Rotation
Affine transformations can be applied to...
- Core Graphics drawing operations. This is done by changing the CTM (current transformation matrix) of a drawing context.
- UIView and CALayer. This is done by setting the
transform
property of a view or layer.
The mathematical background for affine transformations are matrix multiplications.
Origin
- The origin for the CTM (i.e. when doing Core Graphics drawing) is in the lower-left corner.
- The origin for transforms applied to views and layers by default is the center of the view or layer.
- This can be changed by modifying a CALayer's
anchorPoint
property - Despite its name and type (
CGPoint
), this property does not express a coordinate - Instead it is a kind of multiplication factor
- The default value which represents the center of the view is 0.5 / 0.5
- The anchor point value 0.0 / 0.0 places the origin for transformations in the upper-left corner of the view
- The anchor point is expressed in relation to the view/layer bounds
- This can be changed by modifying a CALayer's
Ordering of multiple transforms
Due to the nature of matrix multiplications (they are not commutative), the order in which transformations are applied is usually important. It's the same as with subtraction or division: If you switch the operands the result of the operation will usually change (e.g. 2/3 is not the same as 3/2).
For instance:
- Translate followed by scale affects the translation distance, while scale followed by translate does not
- Scale followed by rotate results in skewing (aka shearing), while rotate followed by scale does not
The function to combine two transforms into one is called
CGAffineTransformConcat
The natural way how to combine a translation with a scale or rotation transform is to first do the scale or rotation, then do the translation. Doing it this way will not affect the translation distance. The following two functions (source) help with this task because you don't have to think about the order of the transforms, you simply specify the values:
CGAffineTransform CGAffineTransformMakeScaleTranslate(CGFloat sx, CGFloat sy, CGFloat dx, CGFloat dy) { return CGAffineTransformMake(sx, 0.f, 0.f, sy, dx, dy); } CGAffineTransform CGAffineTransformMakeRotateTranslate(CGFloat angle, CGFloat dx, CGFloat dy) { return CGAffineTransformMake(cosf(angle), sinf(angle), -sinf(angle), cosf(angle), dx, dy); }
Identity transform
The so-called "identity transform" is a transformation that does not change its operand. The result of applying the identity transform to a coordinate pair is the same identical coordinate pair. The identity transform is referred to with the constant
CGAffineTransformIdentity
Code snippets
Create a rectangular clipping path:
CGContextSaveGState(context); CGRect clippingRect = CGRectMake(10, 10, 100, 20); CGPathRef clippingPath = CGPathCreateWithRect(clippingRect, NULL); CGContextAddPath(context, clippingPath); CGContextClip(context); // do some drawing that will be clipped, e.g. a gradient CGContextRestoreGState(context); CGPathRelease(clippingPath);
Draw a gradient (more on gradients)
const size_t numberOfLocations = 2; const int numberOfComponentsPerLocation = 4; CGFloat locations[numberOfLocations] = { 0.0, 1.0 }; CGFloat components[numberOfLocations * numberOfComponentsPerLocation] = { 1.0, 0.5, 0.4, 1.0, // Start color 0.8, 0.8, 0.3, 1.0 // End color }; CGColorSpaceRef aColorspace = CGColorSpaceCreateDeviceRGB(); CGGradientRef aGradient = CGGradientCreateWithColorComponents (aColorspace, components, locations, numberOfLocations); CGPoint gradientStartPoint = CGPointMake(0, 0); CGPoint gradientEndPoint = CGPointMake(100, 0); CGContextDrawLinearGradient(context, aGradient, gradientStartPoint, gradientEndPoint, 0); CGColorSpaceRelease(aColorspace); CGGradientRelease(aGradient);
References
- ↑ 1.0 1.1 Drawing and Printing Guide for iOS
- ↑ Quartz 2D Programming Guide
- ↑ CGContext Reference (global functions to draw in a CoreGraphics context)
- ↑ CGeometry Reference (structure for geometric operations and associated functions; e.g. CGPoint, CGSize, CGRect)
- ↑ 5.0 5.1 Understanding UIKit Rendering (WWDC 2011 video)
- ↑ 6.0 6.1 Optimizing 2D Graphics and Animation Performance (WWDC 2012 video)
- ↑ 7.0 7.1 7.2 Section "Transforms" in the "Quartz 2D Programming Guide" Cite error: Invalid
<ref>
tag; name "transforms-quartz-2d" defined multiple times with different content Cite error: Invalid<ref>
tag; name "transforms-quartz-2d" defined multiple times with different content