Omitting the UIColor.setFill() error message? - ios

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.

Related

How can I get the coordinates of a Label inside a view?

What I am trying to do is to get the position of my label (timerLabel) in order to pass those coordinates to UIBezierPath (so that the center of the shape and the center of the label coincide).
Here's my code so far, inside the viewDidLoad method, using Xcode 13.2.1:
// getting the center of the label
let center = CGPoint.init(x: timerLabel.frame.midX , y: timerLabel.frame.midY)
// drawing the shape
let trackLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
and this is what I have when I run my app:
link
What I don't understand is why I get (0,0) as coordinates even though I access the label's property (timerLabel.frame.midX).
The coordinates of your label may vary depending on current layout. You need to track all changes and reposition your circle when changes occur. In view controller that uses constraints you would override
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// recreate your circle here
}
this alone does not explain why your circle is so far out. First of all, looking at your image you do not get (0, 0) but some other value which may be relative position of your label within the blue bubble. The frame is always relative to its superview so you need to convert that into your own coordinate system:
let targetView = self.view!
let sourceView = timerLabel!
let centerOfSourceViewInTargetView: CGPoint = targetView.convert(CGPoint(x: sourceView.bounds.midX, y: sourceView.bounds.midY), to: targetView)
// Use centerOfSourceViewInTargetView as center
but I suggest using neither of the two. If you are using constraints (which you should) then rather create more views than adding layers to your existing views.
For instance you could try something like this:
#IBDesignable class CircleView: UIView {
#IBInspectable var lineWidth: CGFloat = 10 { didSet { refresh() } }
#IBInspectable var strokeColor: UIColor = .lightGray { didSet { refresh() } }
override var frame: CGRect { didSet { refresh() } }
override func layoutSubviews() {
super.layoutSubviews()
refresh()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let fillRadius: CGFloat = min(bounds.width, bounds.height)*0.5
let strokeRadius: CGFloat = fillRadius - lineWidth*0.5
let path = UIBezierPath(ovalIn: .init(x: bounds.midX-strokeRadius, y: bounds.midY-strokeRadius, width: strokeRadius*2.0, height: strokeRadius*2.0))
path.lineWidth = lineWidth
strokeColor.setStroke()
UIColor.clear.setFill() // Probably not needed
path.stroke()
}
private func refresh() {
setNeedsDisplay() // This is to force redraw
}
}
this view should draw your circle within itself by overriding draw rect method. You can easily use it in your storyboard (first time it might not draw in storyboard because Xcode. Simply close your project and reopen it and you should see the circle even in storyboard).
Also in storyboard you can directly modify both line width and stroke color which is very convenient.
About the code:
Using #IBDesignable to see drawing in storyboard
Using #IBInspectable to be able to set values in storyboard
Refreshing on any value change to force redraw (sometimes needed)
When frame changes forcing a redraw (Needed when setting frame from code)
A method layoutSubviews is called when resized from constraints. Again redrawing.
Path is computed so that it fits within the size of view.

Constantly animated view in Swift

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

iOS: Animating a circle slice into a wider one

Core-Animation treats angles as described in this image:
(image from http://btk.tillnagel.com/tutorials/rotation-translation-matrix.html)
EDIT: Adding an animated gif to explain better what I'm needing:
I need to animate a slice to grow wider, starting at 300:315 degrees, and ending 300:060.
To create each slice I'm using this function:
extension CGFloat {
func toRadians() -> CGFloat {
return self * CGFloat(Double.pi) / 180.0
}
}
func createSlice(angle1:CGFloat, angle2:CGFloat) -> UIBezierPath! {
let path: UIBezierPath = UIBezierPath()
let width: CGFloat = self.frame.size.width/2
let height: CGFloat = self.frame.size.height/2
let centerToOrigin: CGFloat = sqrt((height)*(height)+(width)*(width));
let ctr: CGPoint = CGPoint(x: width, y: height)
path.move(to: ctr)
path.addArc( withCenter: ctr,
radius: centerToOrigin,
startAngle: CGFloat(angle1).toRadians(),
endAngle: CGFloat(angle2).toRadians(),
clockwise: true
)
path.close()
return path
}
I can now create the two slices and a sublayer with the smaller one, but I can't find how to proceed from this point:
func doStuff() {
path1 = self.createSlice(angle1: 300,angle2: 315)
path2 = self.createSlice(angle1: 300,angle2: 60)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path1.cgPath
shapeLayer.fillColor = UIColor.cyan.cgColor
self.layer.addSublayer(shapeLayer)
I would highly appreciate any help here!
Only a single color
If you want to animate the angle of a solid color filled pie segment like the one in your question, then you can do it by animating the strokeEnd of a CAShapeLayer.
The "trick" here is to make a very wide line. More specifically, you can create a path that is just an arc (the dashed line in the animation below) at half of the intended radius and then giving it the full radius as its line width. When you animate stroking that line it looks like the orange segment below:
Depending on your use case, you can either:
create a path from one angle to the other angle and animate stroke end from 0 to 1
create a path for a full circle, set stroke start and stroke end to some fraction of the circle, and animate stroke end from the start fraction to the end fraction.
If your drawing is just a single color like this, then this will be the smallest solution to your problem.
However, if your drawing is more complex (e.g. also stroking the pie segment) then this solutions simply won't work and you'll have to do something more complex.
Custom drawing / Custom animations
If your drawing of the pie segment is any more complex, then you'll quickly find yourself having to create a layer subclass with custom animatable properties. Doing so is a bit more code - some of which might look a bit unusual1 - but not as scary as it might sound.
This might be one of those things that is still more convenient to do in Objective-C.
Dynamic properties
First, create a layer subclass with the properties you're going to need. In Objective-C parlance these properties should be #dynamic, i.e. not synthesized. This isn't the same as dynamic in Swift. Instead we have to use #NSManaged.
class PieSegmentLayer : CALayer {
#NSManaged var startAngle, endAngle, strokeWidth: CGFloat
#NSManaged var fillColor, strokeColor: UIColor?
// More to come here ...
}
This allows Core Animation to handle these properties dynamically allowing it to track changes and integrate them into the animation system.
Note: a good rule of thumb is that these properties should all be related to drawing / visual presentation of the layer. If they aren't then it's quite likely that they don't belong on the layer. Instead they could be added to a view that in turn uses the layer for its drawing.
Copying layers
During the custom animation, Core Animation is going to want to create and render different layer configurations for different frames. Unlike most of Apple's other frameworks, this happens using the copy constructor init(layer:). For the above five properties to be copied along, we need to override init(layer:) and copy over their values.
In Swift we also have to override the plain init() and init?(coder).
override init(layer: Any) {
super.init(layer: layer)
guard let other = layer as? PieSegmentLayer else { return }
fillColor = other.fillColor
strokeColor = other.strokeColor
startAngle = other.startAngle
endAngle = other.endAngle
strokeWidth = other.strokeWidth
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
return nil
}
Reacting to change
Core Animation is in many ways built for performance. One of the ways it achieves this is by avoiding unnecessary work. By default, a layer won't redraw itself when a property changes. But these properties is used for drawing, and we want the layer to redraw when any of them changes. To do that, we need to override needsDisplay(forKey:) and return true if the key was one of these properties.
override class func needsDisplay(forKey key: String) -> Bool {
switch key {
case #keyPath(startAngle), #keyPath(endAngle),
#keyPath(strokeWidth),
#keyPath(fillColor), #keyPath(strokeColor):
return true
default:
return super.needsDisplay(forKey: key)
}
}
Additionally, If we want the layers default implicit animations for these properties, we need to override action(forKey:) to return a partially configured animation object. If we only want some properties (e.g. the angles) to implicitly animate, then we only need to return an animation for those properties. Unless we need something very custom, it's good to just return a basic animation with the fromValue set to the current presentation value:
override func action(forKey key: String) -> CAAction? {
switch key {
case #keyPath(startAngle), #keyPath(endAngle):
let anim = CABasicAnimation(keyPath: key)
anim.fromValue = presentation()?.value(forKeyPath: key)
return anim
default:
return super.action(forKey: key)
}
}
Drawing
The last piece of a custom animation is the custom drawing. This is done by overriding draw(in:) and using the supplied context to draw the layer:
override func draw(in ctx: CGContext) {
let center = CGPoint(x: bounds.midX, y: bounds.midY)
// subtract half the stroke width to avoid clipping the stroke
let radius = min(center.x, center.y) - strokeWidth / 2
// The two angle properties are in degrees but CG wants them in radians.
let start = startAngle * .pi / 180
let end = endAngle * .pi / 180
ctx.beginPath()
ctx.move(to: center)
ctx.addLine(to: CGPoint(x: center.x + radius * cos(start),
y: center.y + radius * sin(start)))
ctx.addArc(center: center, radius: radius,
startAngle: start, endAngle: end,
clockwise: start > end)
ctx.closePath()
// Configure the graphics context
if let fillCGColor = fillColor?.cgColor {
ctx.setFillColor(fillCGColor)
}
if let strokeCGColor = strokeColor?.cgColor {
ctx.setStrokeColor(strokeCGColor)
}
ctx.setLineWidth(strokeWidth)
ctx.setLineCap(.round)
ctx.setLineJoin(.round)
// Draw
ctx.drawPath(using: .fillStroke)
}
Here I've filled and stroked a pie segment that extends from the center of the layer to the nearest edge. You should replace this with your custom drawing.
A custom animation in action
With all that code in place, we now have a custom layer subclass whose properties can be animated both implicitly (just by changing them) and explicitly (by adding a CAAnimation for their key). The results looks something like this:
Final words
It might not be obvious with the frame rate of those animations but one strong benefit from leveraging Core Animation (in different ways) in both these solutions is that it decouples the drawing of a single state from the timing of an animations.
That means that the layer doesn't know and doesn't have to know about the duration, delays, timing curves, etc. These can all be configured and controlled externally.
So at last I have found a solution. It took me time to understand that there is indeed no way to animate the fill of the shape, but we can trick CA engine by creating a filled circle by making the stroke (i.e. the border of the arc) extremely wide, so that it fills the whole circle!
extension CGFloat {
func toRadians() -> CGFloat {
return self * CGFloat(Double.pi) / 180.0
}
}
import UIKit
class SliceView: UIView {
let circleLayer = CAShapeLayer()
var fromAngle:CGFloat = 30
var toAngle:CGFloat = 150
var color:UIColor = UIColor.magenta
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
convenience init(frame:CGRect, fromAngle:CGFloat, toAngle:CGFloat, color:UIColor) {
self.init(frame:frame)
self.fromAngle = fromAngle
self.toAngle = toAngle
self.color = color
}
func setup() {
circleLayer.strokeColor = color.cgColor
circleLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(circleLayer)
layer.backgroundColor = UIColor.brown.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
let startAngle:CGFloat = (fromAngle-90).toRadians()
let endAngle:CGFloat = (toAngle-90).toRadians()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height) / 4
let path = UIBezierPath(arcCenter: CGPoint(x: 0,y :0), radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
circleLayer.position = center
circleLayer.lineWidth = radius*2
circleLayer.path = path.cgPath
}
public func animate() {
let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 3.0;
pathAnimation.fromValue = 0.0;
pathAnimation.toValue = 1.0;
circleLayer.add(pathAnimation, forKey: "strokeEndAnimation")
}
}
So, now we can add it into our view controller and run the animation. In my case - I'm bridging it into Objecive-C but you can easily adapt it to swift.
I simply can't believe that in 2017 it was still not possible to find a ready solution for this simple task. It took me days to have that done. I really hope it will help others!
Here is how I'm using my class:
#implementation ViewController
{
SliceView *sv_;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.grayColor;
CGFloat width = 240.0;
CGFloat height = 160.0;
CGRect r = CGRectMake(
self.view.frame.size.width/2 - width/2,
self.view.frame.size.height/2 - height/2,
width, height);
sv_ = [[SliceView alloc] initWithFrame:r fromAngle:150 toAngle:30 color:[UIColor yellowColor] ];
[self.view addSubview:sv_];
}
- (IBAction)pressedGo:(id)sender {
[sv_ animate];
}
I'm adding a slight improvement for David's class. (David - you are welcome to copy into your book-quality answer!)
You can add the following init function:
convenience init(frame:CGRect, startAngle:CGFloat, endAngle:CGFloat, fillColor:UIColor,
strokeColor:UIColor, strokeWidth:CGFloat) {
self.init()
self.frame = frame
self.startAngle = startAngle
self.endAngle = endAngle
self.fillColor = fillColor
self.strokeColor = strokeColor
self.strokeWidth = strokeWidth
}
and then call it like this (Objective-C in my case):
PieSegmentLayer *sliceLayer = [[PieSegmentLayer alloc] initWithFrame:r startAngle:30 endAngle:180 fillColor:[UIColor cyanColor] strokeColor:[UIColor redColor] strokeWidth:4];
[self.view.layer addSublayer:sliceLayer];

How to constraint a view/layer's move path to a Bezier curve path?

Before asking the question, i have searched the SO:
iPhone-Move UI Image view along a Bezier curve path
But it did not give a explicit answer.
My requirement is like this, I have a bezier path, and a view(or layer if OK), I want to add pan gesture to the view, and the view(or layer)'s move path must constraint to the bezier path.
My code is below:
The MainDrawView.swift:
import UIKit
class MainDrawView: UIView {
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
drawArc()
}
func drawArc() {
let context = UIGraphicsGetCurrentContext()
// set start point
context?.move(to: CGPoint.init(x: 0, y: 400))
//draw curve
context?.addQuadCurve(to: CGPoint.init(x: 500, y: 250), control: CGPoint.init(x: UIScreen.main.bounds.size.width, y: 200))
context?.strokePath()
}
}
The ViewController.swift:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var clickButton: UIButton!
lazy var view1:UIView = {
let view: UIView = UIView.init(frame: CGRect.init(origin: CGPoint.init(x: 0, y: 0), size: CGSize.init(width: 10, height: 10)))
view.center = CGPoint.init(x: UIScreen.main.bounds.size.width / 2.0, y: UIScreen.main.bounds.size.height / 2.0)
view.backgroundColor = UIColor.red
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
initData()
initUI()
}
func initData() {
}
func initUI() {
self.clickButton.isHidden = true
// init the view
self.view.addSubview(self.view1)
}
}
Edit -1
My deep requirment is when I use pan guester move the view/layer, the view will move on the bezier path.
If bezier path is off lines then u can find the slope of the line and for every change in x or y you can calculate the position of the bezier path
var y :Float = (slope * (xPos-previousCoord.x))+previousCoord.y; xPos is continuously changing. Similarly, u can find for x. For any closed shape with line segments, you can use this.
But if u need it for circle, then u need to convert cartesian to polar. i.e.., from coordinates u can find the angle, then from that angle, you have the radius so by using that angle u need to find cartesian coordinates from that.
θ = tan-1 ( 5 / 12 )
U need to use mainly 3 coordinates one is centre of circle, the second one is your touch point, and the last one is (touchpoint.x, centreofcircle.y). from centre of circle calculate distance between two coordinates
You have θ and radius of circle then find points using
x = r × cos( θ )
y = r × sin( θ )
Don't mistake r in the image for the radius of the circle, r in the image is the hypotenuse of three coordinates. You should calculate for every change of x in touch point.
Hope it works. But for irregular shapes I am not sure how to find.
It sounds like you'll probably have to do some calculations. When you set your method to handle the UIPanGestureRecognizer, you can get a vector back for the pan, something like this:
func panGestureRecognized(_ sender: UIPanGestureRecognizer) {
let vector:CGPoint = sender.translation(in: self.view) // Or use whatever view the UIPanGestureRecognizer was added to
}
You could then use that to extrapolate the movement along the x and y axis. I would recommend starting by getting an algebraic equation for the path you're moving the view on. To move the view along that line, you're going to have to be able to calculate a point along that line relative to the movement of the UIPanGestureRecognizer, and then update the position of the view using that calculated point along the line.
I don't know if it'll work for what you're trying to do, but if you want to try animating your view along the path, it's actually pretty easy:
let path = UIBezierPath() // This would be your custom path
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = path.cgPath
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false animation.repeatCount = 0
animation.duration = 5.0 // However long you want
animation.speed = 2 // However fast you want
animation.calculationMode = kCAAnimationPaced
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animatingView.layer.add(animation, forKey: "moveAlongPath")

Failing to stroke() the UIBezierPath

I wish to create a perfectly rounded rect (Circle) and paint it on the screen. I have tested my code in playground, and it successfully paints the UIBezierPath. It doesn't, however, successfully paint it in the iOS simulator. Here's the code I've been working on:
class Circles {
//Defining the rounded rect (Circle)
func roundRect(radius: CGFloat, angle: CGFloat) -> UIBezierPath {
//Creating the rounded the rectangle (Circle)
var roundedRect = UIBezierPath()
roundedRect.addArcWithCenter(CGPointZero, radius: radius,
startAngle: 0, endAngle: angle ,
clockwise: true)
return roundedRect
}
//Drawing the Bezier Path (Circle)
func drawRect(rect: UIBezierPath){
rect.moveToPoint(self.point)
UIColor.blackColor().setStroke()
rect.stroke()
}
//Giving the rounded rect (Circle) it's position
var point = CGPointMake(500, 500)
}
//Giving the rounded rect (Circle) it's size
var newRect = Circles().roundRect(200.0, angle: 7)
//Drawing the rounded rect (Circle)
Circles().drawRect(newRect)
I have seen some other posts with similar problems from a few years back, however they were in Objective-C, I tried translating but it was not of any use. I've also tried several other methods of painting the path on the screen but, again sadly, it was of no use. I tested it to make sure the functions are working with println statements, the issue is I don't know why the stroke is not activating. Thanks for reading, -Zach.
Here's the updated version using what Mike said:
class CircleView: UIView {
override func drawRect(rect: CGRect) {
// Creating the rectangle's size
var newRect = Circles().roundRect(200.0, angle: 7)
//Drawing the rectangle
Circles().drawRect(newRect)
}
//Holding all to do with the circle
class Circles {
//Defining the rounded rect (Circle)
func roundRect(radius: CGFloat, angle: CGFloat) -> UIBezierPath {
//Creating the rounded rect (Circle)
var roundedRect = UIBezierPath()
roundedRect.addArcWithCenter(CGPointZero, radius: radius,
startAngle: 0, endAngle: angle ,
clockwise: true)
return roundedRect
}
//Drawing the Bezier Path (Circle)
func drawRect(rect: UIBezierPath){
rect.moveToPoint(self.point)
UIColor.blackColor().setStroke()
UIColor.blackColor().setFill()
rect.stroke()
rect.fill()
}
//Giving the rounded rect (Circle) it's position
var point = CGPointMake(500, 500)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//Generating the background
self.view.backgroundColor = UIColor(patternImage: UIImage(named: "normalpaper.jpg"))
let circleView = CircleView(frame: self.view.bounds)
self.view.addSubview(circleView)
}
As you mentioned in the comments, you're calling drawRect from a UIViewController's viewDidLoad function. You don't have a valid drawing context there, so that's not going to work.
The easiest way to make this work is to create a UIView subclass with its own drawRect function which calls Circle().drawRect(...) and add that as a subview of your UIViewController's view.
Here's an example of this:
Note: I've made the custom view transparent by setting circleView.opaque = false so that the background you mentioned in the comments shows through.
class CircleView: UIView {
override func drawRect(rect: CGRect) {
// I just copied this directly from the original question
var newRect = Circles().roundRect(200.0, angle: 7)
Circles().drawRect(newRect)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Create a new CircleView that's the same size of this
// view controller's bounds and add it to the view hierarchy
let circleView = CircleView(frame: self.view.bounds)
circleView.opaque = false
self.view.addSubview(circleView)
}
}
Note: If you're going to be doing custom drawing, I highly recommend you read Drawing and Printing Guide for iOS. It'll teach you all the basics you need to make it work.

Resources