I'm creating a subclass of AVPlayerController, that observes the player and handles player's states. If AVPlayerItemStatus is .failed, I add a custom UIView over the player's frame. At present, the important parts of my custom view's code looks like this:
class NoiseView: UIView {
...
var timer: Timer?
func animate() {
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
}
func stopAnimating() {
timer?.invalidate()
}
#objc func timerAction() {
self.setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let context = UIGraphicsGetCurrentContext()!
let h = rect.height
let w = rect.width
var val: CGFloat!
var color: UIColor!
for i in 0...Int(w-1) {
for j in 0...Int(h-1) {
val = .random
color = UIColor(red: val, green: val, blue: val, alpha: 1.0)
context.setFillColor(color.cgColor)
context.fill(CGRect(x: i, y: j, width: 1, height: 1))
}
}
context.flush()
}
}
I'm calling the method animate() from other ViewController that keeps the NoiseView object.
The thing is, it's working as it's supposed to work (view is animating and creating a white noise) but the app starts to work slowly. What should be a proper approach to redraw the view without causing such a performance drop?
By the way, the drop happened with 0.1s interval (10 FPS). What I want to accomplish is having a constant white noise with at least 30 FPS to look like a legit tv noise.
There are a number of strategies you can try to optimize the drawing code, but to me the most obvious one is that you should pre-calculate the CGRects you need outside the drawing code.
The loops you're running require w^h iterations for each frame of animation to calculate all the CGRects you need. That's a lot, and totally unnecessary, because the CGRects will always be the same so long as the width and height are unchanged. Instead, you should do something like this:
var cachedRects = [CGRect]()
override var frame: CGRect {
didSet {
calculateRects()
}
}
func calculateRects() {
cachedRects = []
let w = frame.width
let h = frame.height
for i in 0...Int(w-1) {
for j in 0...Int(h-1) {
let rect = CGRect(x: i, y: j, width: 1, height: 1)
cachedRects += [rect]
}
}
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let context = UIGraphicsGetCurrentContext()!
var val: CGFloat!
var color: UIColor!
for rect in cachedRects {
val = .random
color = UIColor(red: val, green: val, blue: val, alpha: 1.0)
context.setFillColor(color.cgColor)
context.fill(rect)
}
context.flush()
}
By pre-caching the rects, you only have to do (w * h) number of iterations, which is a vast improvement.
If that isn't enough to improve performance, you can further optimize using similar pre-caching strategies, like instead of randomizing the each pixel, pre-calculate tiles of random colors outside the draw code, and then randomly assemble them in drawRect(). If the randomizer is what's the performance problem, this would cut down on the amount of work it would have to.
The key strategy is to try and minimize the amount of work your drawRect() method has to do, because it run on each frame of animation.
Good luck!
Instead of using the timer and calling setNeedsDisplay which in result will keep calling draw method, hence slowing the app.
Use CAAnimation and create CALayer or CAreplicator layer and animate that.
If you need code you can check this link Color Picker or I can post some demo code after sometime, Its fifa time :-p.
CAReplicatorlayer CALayer
Custom draw(rect:) methods can cause performance hits.
I suggest looking into adding a CAAnimation or CAAnimationGroup to your white noise view.
CABasicAnimation Documentation
CAAnimationGroup StackOverflow Post
Related
I have a simple custom CALayer to create an overlaying gradient effect on my UIView. Here is the code:
class GradientLayer: CALayer {
var locations: [CGFloat]?
var origin: CGPoint?
var radius: CGFloat?
var color: CGColor?
convenience init(view: UIView, locations: [CGFloat]?, origin: CGPoint?, radius: CGFloat?, color: UIColor?) {
self.init()
self.locations = locations
self.origin = origin
self.radius = radius
self.color = color?.CGColor
self.frame = view.bounds
}
override func drawInContext(ctx: CGContext) {
super.drawInContext(ctx)
guard let locations = self.locations else { return }
guard let origin = self.origin else { return }
guard let radius = self.radius else { return }
let colorSpace = CGColorGetColorSpace(color)
let colorComponents = CGColorGetComponents(color)
let gradient = CGGradientCreateWithColorComponents(colorSpace, colorComponents, locations, locations.count)
CGContextDrawRadialGradient(ctx, gradient, origin, CGFloat(0), origin, radius, [.DrawsAfterEndLocation])
}
}
I initialize and set these layers here:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let gradient1 = GradientLayer(view: view, locations: [0.0,1.0], origin: CGPoint(x: view.frame.midX, y: view.frame.midY), radius: 100.0, color: UIColor(white: 1.0, alpha: 0.2))
let gradient2 = GradientLayer(view: view, locations: [0.0,1.0], origin: CGPoint(x: view.frame.midX-20, y: view.frame.midY+20), radius: 160.0, color: UIColor(white: 1.0, alpha: 0.2))
let gradient3 = GradientLayer(view: view, locations: [0.0,1.0], origin: CGPoint(x: view.frame.midX+30, y: view.frame.midY-30), radius: 300.0, color: UIColor(white: 1.0, alpha: 0.2))
gradient1.setNeedsDisplay()
gradient2.setNeedsDisplay()
gradient3.setNeedsDisplay()
view.layer.addSublayer(gradient1)
view.layer.addSublayer(gradient2)
view.layer.addSublayer(gradient3)
}
The view seems to display properly most of the time, but (seemingly) randomly I'll get different renderings as you'll see below. Here are some examples (the first one is what I want):
What is causing this malfunction? How do I only load the first one every time?
You have several problems.
First off, you should think of a gradient as an array of stops, where a stop has two parts: a color and a location. You must have an equal number of colors and locations, because every stop has one of each. You can see this if, for example, you check the CGGradientCreateWithColorComponents documentation regarding the components argument:
The number of items in this array should be the product of count and the number of components in the color space.
It's a product (the result of a multiplication) because you have count stops and you need a complete set of color components for each stop.
You're not providing enough color components. Your GradientLayer could have any number of locations (and you're giving it two) but has only one color. You're getting that one color's components and passing that as the components array to CGGradientCreateWithColorComponents, but the array is too short. Swift doesn't catch this error—notice that the type of your colorComponents is UnsafePointer<CGFloat>. The Unsafe part tells you that you're in dangerous territory. (You can see the type of colorComponents by option-clicking it in Xcode.)
Since you're not providing a large enough array for components, iOS is using whatever random values happen to be in memory after the components of your one color. Those may change from run to run and are often not what you want them to be.
In fact, you shouldn't even use CGGradientCreateWithColorComponents. You should use CGGradientCreateWithColors, which takes an array of CGColor so it's not only simpler to use, but safer because it's one less UnsafePointer floating around.
Here's what GradientLayer should look like:
class RadialGradientLayer: CALayer {
struct Stop {
var location: CGFloat
var color: UIColor
}
var stops: [Stop] { didSet { self.setNeedsDisplay() } }
var origin: CGPoint { didSet { self.setNeedsDisplay() } }
var radius: CGFloat { didSet { self.setNeedsDisplay() } }
init(stops: [Stop], origin: CGPoint, radius: CGFloat) {
self.stops = stops
self.origin = origin
self.radius = radius
super.init()
needsDisplayOnBoundsChange = true
}
override init(layer other: AnyObject) {
guard let other = other as? RadialGradientLayer else { fatalError() }
stops = other.stops
origin = other.origin
radius = other.radius
super.init(layer: other)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func drawInContext(ctx: CGContext) {
let locations = stops.map { $0.location }
let colors = stops.map { $0.color.CGColor }
locations.withUnsafeBufferPointer { pointer in
let gradient = CGGradientCreateWithColors(nil, colors, pointer.baseAddress)
CGContextDrawRadialGradient(ctx, gradient, origin, 0, origin, radius, [.DrawsAfterEndLocation])
}
}
}
Next problem. You're adding more gradient layers every time the system calls viewWillLayoutSubviews. It can call that function multiple times! For example, it will call it if your app supports interface rotation, or if a call comes in and iOS makes the status bar taller. (You can test that in the simulator by choosing Hardware > Toggle In-Call Status Bar.)
You need to create the gradient layers once, storing them in a property. If they have already been created, you need to update their frames and not create new layers:
class ViewController: UIViewController {
private var gradientLayers = [RadialGradientLayer]()
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if gradientLayers.isEmpty {
createGradientLayers()
}
for layer in gradientLayers {
layer.frame = view.bounds
}
}
private func createGradientLayers() {
let bounds = view.bounds
let mid = CGPointMake(bounds.midX, bounds.midY)
typealias Stop = RadialGradientLayer.Stop
for (point, radius, color) in [
(mid, 100, UIColor(white:1, alpha:0.2)),
(CGPointMake(mid.x - 20, mid.y + 20), 160, UIColor(white:1, alpha:0.2)),
(CGPointMake(mid.x + 30, mid.y - 30), 300, UIColor(white:1, alpha:0.2))
] as [(CGPoint, CGFloat, UIColor)] {
let stops: [RadialGradientLayer.Stop] = [
Stop(location: 0, color: color),
Stop(location: 1, color: color.colorWithAlphaComponent(0))]
let layer = RadialGradientLayer(stops: stops, origin: point, radius: radius)
view.layer.addSublayer(layer)
gradientLayers.append(layer)
}
}
}
Your problem is the code you have written in viewWillLayoutSubviews function its is called multiple times when views loads just add a check to run it once or better yet add a check in viewdidlayoutsubviews to run it once
I just saw this image and it's interesting to me, how to create such type of animation in Swift:
So, I have many gray teeth in circle and when I set the angle, for example 45degree it will fill these gray teeth into blue within 0..45 degree.
You can just explain me the right way of doing it or you can show different snippets(it would be great). And later I will search or read about it.
Thanks in advance!
If you only need the individual 'teeth' to change color, instead of using the teeth as masks for a solid fill, you can use Core Graphics instead of Core Animation (although Core Animation is generally preferred). So in order to do this, we should be doing the following:
Subclass UIView to insert our drawing code
Create an array of path objects, wrapped in UIBezierPath
Setup a timer to update a progress value and setNeedsDisplay
In drawRect:, draw the paths and assign a fill to each depending on the progress
First of all, lets define the variables we're going to be working with in this UIView subclass.
class TeethLoaderView : UIView {
let numberOfTeeth = UInt(60) // Number of teeth to render
let teethSize = CGSize(width:8, height:45) // The size of each individual tooth
let animationDuration = NSTimeInterval(5.0) // The duration of the animation
let highlightColor = UIColor(red: 29.0/255.0, green: 175.0/255.0, blue: 255.0/255.0, alpha: 1) // The color of a tooth when it's 'highlighted'
let inactiveColor = UIColor(red: 233.0/255.0, green: 235.0/255.0, blue: 236.0/255.0, alpha: 1) // The color of a tooth when it isn't 'hightlighted'
var progress = NSTimeInterval(0.0) // The progress of the loader
var paths = [UIBezierPath]() // The array containing the UIBezier paths
var displayLink = CADisplayLink() // The display link to update the progress
var teethHighlighted = UInt(0) // Number of teeth highlighted
...
Now let's add a function to create our paths.
func getPaths(size:CGSize, teethCount:UInt, teethSize:CGSize, radius:CGFloat) -> [UIBezierPath] {
let halfHeight = size.height*0.5;
let halfWidth = size.width*0.5;
let deltaAngle = CGFloat(2*M_PI)/CGFloat(teethCount); // The change in angle between paths
// Create the template path of a single shape.
let p = CGPathCreateWithRect(CGRectMake(-teethSize.width*0.5, radius, teethSize.width, teethSize.height), nil);
var pathArray = [UIBezierPath]()
for i in 0..<teethCount { // Copy, translate and rotate shapes around
let translate = CGAffineTransformMakeTranslation(halfWidth, halfHeight);
var rotate = CGAffineTransformRotate(translate, deltaAngle*CGFloat(i))
let pathCopy = CGPathCreateCopyByTransformingPath(p, &rotate)!
pathArray.append(UIBezierPath(CGPath: pathCopy)) // Populate the array
}
return pathArray
}
This is fairly simple. We just create a path for a single 'tooth' and then copy this path for how many teeth we need, translating and rotating the path for each one.
Next we want to setup our view. I'm going to a CADisplayLink for the timer so that the animation performs at the same speed on all devices.
override init(frame: CGRect) {
super.init(frame: frame)
commonSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonSetup()
}
private func commonSetup() {
self.backgroundColor = UIColor.whiteColor()
paths = getPaths(frame.size, teethCount: numberOfTeeth, teethSize: teethSize, radius: ((frame.width*0.5)-teethSize.height))
displayLink = CADisplayLink(target: self, selector: #selector(displayLinkDidFire));
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
Here we just set the background color, as well as setup our timer and initialise the paths we're going to be using. Next we want to setup a function to change the progress of the view when the CADisplayLink fires.
func displayLinkDidFire() {
progress += displayLink.duration/animationDuration
if (progress > 1) {
progress -= 1
}
let t = teethHighlighted
teethHighlighted = UInt(round(progress*NSTimeInterval(numberOfTeeth))) // Calculate the number of teeth to highlight
if (t != teethHighlighted) { // Only call setNeedsDisplay if the teethHighlighted changed
setNeedsDisplay()
}
}
Nothing complicated here, we just update the progress and teethHighlighted and call setNeedsDisplay() to redraw the view, if teethHighlighted changed.
Finally, we want to draw the view.
override func drawRect(rect: CGRect) {
let ctx = UIGraphicsGetCurrentContext()
CGContextScaleCTM(ctx, -1, -1) // Flip the context to the correct orientation
CGContextTranslateCTM(ctx, -rect.size.width, -rect.size.height)
for (index, path) in paths.enumerate() { // Draw each 'tooth'
CGContextAddPath(ctx, path.CGPath);
let fillColor = (UInt(index) <= teethHighlighted) ? highlightColor:inactiveColor;
CGContextSetFillColorWithColor(ctx, fillColor.CGColor)
CGContextFillPath(ctx)
}
}
If you wanted to go down the Core Animation path, I adapted this code into a Core Animation layer
Final Result
Full project: https://github.com/hamishknight/Circle-Loader
Well, in the spirit of "go big or go home" (and because I'm actually having some fun doing this), I created a Core Animation version of my Core Graphics answer. It's quite a bit less code and animates smoother, so I'd actually prefer to use this.
First off, let's subclass a UIView again (this isn't strictly necessary, but it's nice to contain everything in a single view) and define our variables:
class TeethLoaderViewCA : UIView {
let numberOfTeeth = UInt(60) // Number of teetch to render
let teethSize = CGSize(width:8, height:45) // The size of each individual tooth
let animationDuration = NSTimeInterval(5.0) // The duration of the animation
let highlightColor = UIColor(red: 29.0/255.0, green: 175.0/255.0, blue: 255.0/255.0, alpha: 1) // The color of a tooth when it's 'highlighted'
let inactiveColor = UIColor(red: 233.0/255.0, green: 235.0/255.0, blue: 236.0/255.0, alpha: 1) // The color of a tooth when it isn't 'hightlighted'
let shapeLayer = CAShapeLayer() // The teeth shape layer
let drawLayer = CAShapeLayer() // The arc fill layer
let anim = CABasicAnimation(keyPath: "strokeEnd") // The stroke animation
...
This is mostly the same as the Core Graphics version, but with a couple of Core Animation objects and without the timing logic. Next, we can pretty much copy the getPaths function we created in the other version, except with a few tweaks.
func getPathMask(size:CGSize, teethCount:UInt, teethSize:CGSize, radius:CGFloat) -> CGPathRef? {
let halfHeight = size.height*0.5
let halfWidth = size.width*0.5
let deltaAngle = CGFloat(2*M_PI)/CGFloat(teethCount); // The change in angle between paths
// Create the template path of a single shape.
let p = CGPathCreateWithRect(CGRectMake(-teethSize.width*0.5, radius, teethSize.width, teethSize.height), nil)
let returnPath = CGPathCreateMutable()
for i in 0..<teethCount { // Copy, translate and rotate shapes around
let translate = CGAffineTransformMakeTranslation(halfWidth, halfHeight)
var rotate = CGAffineTransformRotate(translate, deltaAngle*CGFloat(i))
CGPathAddPath(returnPath, &rotate, p)
}
return CGPathCreateCopy(returnPath)
}
This time, all the paths are grouped into one big path and the function returns that path.
Finally, we just have to create our layer objects & setup the animation.
private func commonSetup() {
// set your background color
self.backgroundColor = UIColor.whiteColor()
// Get the group of paths we created.
shapeLayer.path = getPathMask(frame.size, teethCount: numberOfTeeth, teethSize: teethSize, radius: ((frame.width*0.5)-teethSize.height))
let halfWidth = frame.size.width*0.5
let halfHeight = frame.size.height*0.5
let halfDeltaAngle = CGFloat(M_PI/Double(numberOfTeeth))
// Creates an arc path, with a given offset to allow it to be presented nicely
drawLayer.path = UIBezierPath(arcCenter: CGPointMake(halfWidth, halfHeight), radius: halfWidth, startAngle: CGFloat(-M_PI_2)-halfDeltaAngle, endAngle: CGFloat(M_PI*1.5)+halfDeltaAngle, clockwise: true).CGPath
drawLayer.frame = frame
drawLayer.fillColor = inactiveColor.CGColor
drawLayer.strokeColor = highlightColor.CGColor
drawLayer.strokeEnd = 0
drawLayer.lineWidth = halfWidth
drawLayer.mask = shapeLayer
layer.addSublayer(drawLayer)
// Optional, but looks nice
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
}
All we're doing here is assigning the group of paths to a CAShapeLayer, which we will use as a mask over the drawLayer, which we will be animating around the view (using a stroke on an arch path).
Final Result
Full project: https://github.com/hamishknight/Circle-Loader
I have a spritekit application written in swift and I want to get the color on the pixel that my finger is touching.
I have seen multiple post regarding this and tried them all out but can't seam to get it to work for me. Accourding to other post it should be possible to get the color from a UIView and as a SKScene has a SKIView that inherits from UIView it should be possible to get the color from there.
So to make the question easy and understandable I have an example.
Create a new spritekit application and add a image to it.
In my case I created a png image 200x200 pixels with a lot of different colors in it.
This is the GameScene.swift file, it is the only file I have changes from the auto generated:
import SpriteKit
extension UIView {
func getColorFromPoint(point:CGPoint) -> SKColor {
var pixelData:[UInt8] = [0,0,0,0]
let colorSpace:CGColorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(CGImageAlphaInfo.PremultipliedLast.toRaw())
let context = CGBitmapContextCreate(&pixelData, 1, 1, 8, 4, colorSpace, bitmapInfo)
CGContextTranslateCTM(context, -point.x, -point.y);
self.layer.renderInContext(context)
var red:CGFloat = CGFloat(pixelData[0])/CGFloat(255.0)
var green:CGFloat = CGFloat(pixelData[1])/CGFloat(255.0)
var blue:CGFloat = CGFloat(pixelData[2])/CGFloat(255.0)
var alpha:CGFloat = CGFloat(pixelData[3])/CGFloat(255.0)
var color:SKColor = SKColor(red: red, green: green, blue: blue, alpha: alpha)
return color
}
}
class GameScene: SKScene {
var myColorWheel:SKSpriteNode!
override func didMoveToView(view: SKView) {
let recognizerTap = UITapGestureRecognizer(target: self, action:Selector("handleTap:"))
view.addGestureRecognizer(recognizerTap)
myColorWheel = SKSpriteNode(imageNamed: "ColorWheel.png")
myColorWheel.anchorPoint = CGPoint(x: 0, y: 0)
myColorWheel.position = CGPoint(x: 200, y: 200)
self.addChild(myColorWheel)
}
func handleTap(recognizer : UITapGestureRecognizer)
{
let location : CGPoint = self.convertPointFromView(recognizer.locationInView(self.view))
if(myColorWheel.containsPoint(location))
{
let color = self.view?.getColorFromPoint(location)
println(color)
}
}
}
It don't matter where I press on the image on the display, the result is always:
Optional(UIDeviceRGBColorSpace 0 0 0 0)
Have you tried to take a snapshot first using:
- (UIView *)snapshotViewAfterScreenUpdates:(BOOL)afterUpdates
Then picking the colours from that view?
Not sure how the system renders the .layer in a SKView.
Hope that helps.
Cheers
I'm making an app where I want to draw a lot of shapes - circles, boxes, lines, etc.
Millions of them.
To test the performance of this, I threw together this simple UIView. Note that credit is due - I got inspired by this project.
import UIKit
let qkeyString = "label" as NSString
var QKEY = qkeyString.UTF8String
let qvalString = "com.hanssjunnesson.Draw" as NSString
var QVAL = qvalString.UTF8String
public class RenderImageView: UIView {
var bitmapContext: CGContext?
let drawQueue: dispatch_queue_attr_t = {
let q = dispatch_queue_create(QVAL, nil)
dispatch_queue_set_specific(q, QKEY, &QVAL, nil)
return q
}()
public override init() {
super.init()
render()
}
required public init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
render()
}
override required public init(frame: CGRect) {
super.init(frame: frame)
render()
}
public override func drawRect(rect: CGRect) {
if let bitmapContext = self.bitmapContext {
let context = UIGraphicsGetCurrentContext()
let image = CGBitmapContextCreateImage(bitmapContext)
CGContextDrawImage(context, self.bounds, image)
}
}
private func render() {
dispatch_async(drawQueue) {
let startDate = NSDate()
let bounds = self.bounds
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()
self.bitmapContext = context
CGContextSetFillColorWithColor(context, UIColor.whiteColor().CGColor)
CGContextFillRect(context, bounds)
CGContextSetFillColorWithColor(context, UIColor(red: 0.15, green: 0.4, blue: 0.8, alpha: 1.0).CGColor)
for i in 1...1000000 {
CGContextFillEllipseInRect(context, bounds)
}
UIGraphicsEndImageContext()
self.setNeedsDisplay()
let benchmark = startDate.timeIntervalSinceNow
println("Rendering took: \(-benchmark*1000) Ms")
}
}
}
This works just fine. On my iOS simulator, it takes little over a minute to draw a million circles on top of each other.
I wanted to speed this up, so I tried drawing to the bitmap context from multiple threads.
let group = dispatch_group_create()
for i in 1...100 {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
dispatch_group_enter(group)
CGContextFillEllipseInRect(context, bounds)
dispatch_group_leave(group)
}
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
This did not work, however. I get an EXC_BAD_ACCESS when calling CGContextFillEllipseInRect(context, bounds).
Drawing to a CGContext in a background thread seems fine as long as it's the same thread that created it.
Anyone know of a way of getting this to work?
1) You aren't actually waiting for the group you created to finish -- dispatch_group_wait is going to be called in that code before any of the blocks have been executed, so the enter / leave calls inside them won't have any effect. Use dispatch_group_async instead (see below).
2) You can't draw to a CGContext from two different threads at the same time -- you can see this if you add a println inside your drawing loop. It will work a few times, with varying results, but eventually you'll be end up with an error.
let group = dispatch_group_create()
for i in 1...10 {
dispatch_group_async(group, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
for j in 1...100 {
println("i:\(i), j:\(j)")
CGContextFillEllipseInRect(context, bounds)
}
}
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
Sample output:
iiii::::4123,,,, jjjj::::1111
ii:1, j:2
:5, j:1
i:6, j:1
EXC_BAD_ACCESS
The only solution to this is to jump back on a single thread for drawing, but that defeats what you were trying to do any way. If you have to do lots of calculations to decide what to draw, that could happen on separate threads, but drawing to CGContext itself isn't thread safe.
I have two functions side by side that are looped to constantly draw two UIBezierPaths, the thing is, each of them have a different color so I constantly need to reiterate UIColor.blackColor().setFill() and UIColor(patternImage: UIImage(named: "normalpaper.jpg")).setFill(), the downside is it makes the console impossible to read because it's endlessly spamming the warning message you get.
<Error>: CGContextRestoreGState: invalid context 0x0. This is a serious error.
This application, or a library it uses, is using an invalid context and is thereby
contributing to an overall degradation of system stability and reliability. This
notice is a courtesy: please fix this problem. It will become a fatal error in an
upcoming update.
And thus, here is my question, is there a way of doing this that would not spam my console with this warning message? Perhaps a way of making it so the warning message would not appear? (Couldn't find one with searching) Or maybe a way of omitting the message? Any input is very appreciated, thanks for reading -Zach.
-
If you need the draw functions, there they are below
func drawCircle() {
//Setting the draw color
UIColor.blackColor().setFill()
// Creating the rectangle's size
var drawRect = roundDrawRect(10.0, angle: 7)
//Incrementing the coords
++y
++x
//Drawing the rectangle
drawRect.fill()
}
func eraseCircle() {
//Setting the eraser color
UIColor(patternImage: UIImage(named: "normalpaper.jpg")).setFill()
//Decrementing the coords
eraseX = x - 2
eraseY = y - 2
// Creating the rectangle's size
var eraseRect = roundEraseRect(10.0, angle: 7)
//Drawing the rectangle
eraseRect.fill()
}
Full CircleView class below
(I'm still very new to programming, so it's probably quite inefficient)
//Creating a view capable of painting the circle
class CircleView: UIView {
//Starting X Pos
var x: CGFloat = 100
var eraseX: CGFloat = 100
//Starting Y Pos
var y: CGFloat = 100
var eraseY: CGFloat = 100
//Starting the loop of functions
override func drawRect(rect: CGRect) {
//Creating the looping draw timer
NSTimer.scheduledTimerWithTimeInterval(
0.2,
target: self,
selector: Selector("timerDraw"),
userInfo: nil,
repeats: true)
//Creating the looping erase timer
NSTimer.scheduledTimerWithTimeInterval(
0.3,
target: self,
selector: Selector("timerErase"),
userInfo: nil,
repeats: true)
}
func drawCircle() {
//Setting the draw color
UIColor.blackColor().setFill()
// Creating the rectangle's size
var drawRect = roundDrawRect(10.0, angle: 7)
//Incrementing the coords
++y
++x
//Drawing the rectangle
drawRect.fill()
}
func eraseCircle() {
//Setting the eraser color
UIColor(patternImage: UIImage(named: "normalpaper.jpg")).setFill()
//Decrementing the coords
eraseX = x - 2
eraseY = y - 2
// Creating the rectangle's size
var eraseRect = roundEraseRect(10.0, angle: 7)
//Drawing the rectangle
eraseRect.fill()
}
func timerDraw(){
//DO DRAW LOOP HERE
drawCircle()
}
func timerErase(){
//DO ERASE LOOP HERE
eraseCircle()
}
//Defining the rounded rect erasing (Circle)
func roundEraseRect(radius: CGFloat, angle: CGFloat) -> UIBezierPath {
//Creating the rounded rect (Circle)
var roundedRect = UIBezierPath()
roundedRect.moveToPoint(CGPointMake(eraseX,eraseY))
println(CGPointMake(eraseX,eraseY))
roundedRect.addArcWithCenter(CGPointZero, radius: radius,
startAngle: 0, endAngle: angle ,
clockwise: true)
return roundedRect
}
//Defining the rounded rect drawing (Circle)
func roundDrawRect(radius: CGFloat, angle: CGFloat) -> UIBezierPath {
//Creating the rounded rect (Circle)
var roundedRect = UIBezierPath()
roundedRect.moveToPoint(CGPointMake(x,y))
roundedRect.addArcWithCenter(CGPointZero, radius: radius,
startAngle: 0, endAngle: angle ,
clockwise: true)
return roundedRect
}
}
class ViewController: UIViewController {
Apple's Drawing and Printing Guide for iOS explains all of this, I highly recommend reading it before going further with custom drawing code. At an absolute minimum, read the section on iOS Drawing Concepts.
The problem is that you're creating and firing NSTimers inside your CircleViews drawRect function. Drawing calls can only be made in the right context (which is actually what the error you're seeing it trying to tell you). By doing the drawing in functions invoked from your NSTimer, you're actually doing the drawing outside your drawRect function and there isn't a valid drawing context in that case. Also, with the code the way it is, you'll be starting new timers each time the system needs to redraw your view; that could get out of hand very quickly as the timers start overlapping.
However, with just a bit of rearranging, we can make this work.
Please Note: This isn't necessarily the right way to go about what you're doing with your circle animation, but it will solve the particular problem that you're asking about with with regards to the invalid context error.
Take everything out of drawRect and replace it with calls to eraseCircle and drawCircle.
Take the logic that you have to increment x and y, and eraseX and eraseY out of drawCircle and eraseCircle and put that in timerDraw and timerErase instead.
Instead of calling your drawing code directly in timerDraw and timerErase, tell the view system that you need your view redrawn by calling setNeedsDisplay(). That will flag your view as needing to be redrawn and the view system will call your drawRect function again automatically as soon as it can.
Make your timers work again by overriding didMoveToSuperview and start them there; you should also add logic to stop them if they're already running.
Steps 1 and 3 are the critical bits that makes your error go away.
Something like this:
//Creating a view capable of painting the circle
class CircleView: UIView {
// Timers
var drawTimer: NSTimer?
var eraseTimer: NSTimer?
//Starting X Pos
var x: CGFloat = 100
var eraseX: CGFloat = 100
//Starting Y Pos
var y: CGFloat = 100
var eraseY: CGFloat = 100
override func drawRect(rect: CGRect) {
eraseCircle()
drawCircle()
}
override func didMoveToSuperview() {
// If we have active timers, stop them
if var drawTimer = self.drawTimer {
// This stops the timer
drawTimer.invalidate()
self.drawTimer = nil
}
if var eraseTimer = self.eraseTimer {
// This stops the timer
eraseTimer.invalidate()
self.eraseTimer = nil
}
// If we're actually part of the view hierarchy, start the timers
if self.superview != nil {
//Creating the looping draw timer
self.drawTimer = NSTimer.scheduledTimerWithTimeInterval(
0.2,
target: self,
selector: Selector("timerDraw"),
userInfo: nil,
repeats: true)
//Creating the looping erase timer
self.eraseTimer = NSTimer.scheduledTimerWithTimeInterval(
0.3,
target: self,
selector: Selector("timerErase"),
userInfo: nil,
repeats: true)
}
}
func drawCircle() {
//Setting the draw color
UIColor.blackColor().setFill()
// Creating the rectangle's size
var drawRect = roundDrawRect(10.0, angle: 7)
//Drawing the rectangle
drawRect.fill()
}
func eraseCircle() {
//Setting the eraser color
UIColor(patternImage: UIImage(named: "normalpaper.jpg")).setFill()
// Creating the rectangle's size
var eraseRect = roundEraseRect(10.0, angle: 7)
//Drawing the rectangle
eraseRect.fill()
}
func timerDraw(){
//Incrementing the coords
++y
++x
self.setNeedsDisplay()
}
func timerErase(){
//Decrementing the coords
eraseX = x - 2
eraseY = y - 2
self.setNeedsDisplay()
}
//Defining the rounded rect erasing (Circle)
func roundEraseRect(radius: CGFloat, angle: CGFloat) -> UIBezierPath {
//Creating the rounded rect (Circle)
var roundedRect = UIBezierPath()
roundedRect.moveToPoint(CGPointMake(eraseX,eraseY))
println(CGPointMake(eraseX,eraseY))
roundedRect.addArcWithCenter(CGPointZero, radius: radius,
startAngle: 0, endAngle: angle ,
clockwise: true)
return roundedRect
}
//Defining the rounded rect drawing (Circle)
func roundDrawRect(radius: CGFloat, angle: CGFloat) -> UIBezierPath {
//Creating the rounded rect (Circle)
var roundedRect = UIBezierPath()
roundedRect.moveToPoint(CGPointMake(x,y))
roundedRect.addArcWithCenter(CGPointZero, radius: radius,
startAngle: 0, endAngle: angle ,
clockwise: true)
return roundedRect
}
}
As to the best way to achieve the animation you're attempting, you could look at just drawing the circle once and then, in your UIViewController moving the entire view on a timer. Or, possibly better, using Core Animation. Or, if your final product is going to be a game (or even game like), maybe take a look at Sprite Kit.