Flipping CGContext inside drawrect method draws in wrong posotion - ios

I tried to draw a circle flipped vertically by its center. (Flipping a circle by its center looks same so that I can check visually). But the drawn position is entirely wrong. I expect that the fill has to be occurred entirely inside the stroked region
circleRect = CGRect(x:100, y:100, width:100, height: 100)
override func draw(_ dirtyRect: CGRect) {
let ctx = NSGraphicsContext.current()!.cgContext
if(drawCircle) {
let path = getOvalPath(circleRect) //returns a oval cgpath for the rect, here the rect is a square so it gives a circle
//stroked without flipping
ctx.addPath(path)
ctx.setStrokeColor(CGColor.black)
ctx.strokePath()
//filled with flipping
ctx.saveGState
ctx.translateBy(x: 0, y: circleRect.size.height)
ctx.scaleBy(x: 1, y: -1)
ctx.addPath(path)
ctx.setFillColor(fillColor)
ctx.fillPath()
ctx.restoreGState()
}
drawCircle = true
setNeedsDisplay(circleRect)

Your circle's center is at (100, 100). Consider the effects of your transforms on that point:
var point = CGPoint(x: 100, y: 100)
// point = (100, 100)
point = point.applying(CGAffineTransform(scaleX: 1, y: -1))
// point = (100, -100)
point = point.applying(CGAffineTransform(translationX: 0, y: 100))
// point = (100, 0)
So your transforms move the center of the circle.
You're thinking about “flipping” the y axis, moving it from the bottom left corner of some “canvas” to the top left corner. Your circle will stay put if it is centered vertically on that canvas. Your canvas's height is implicitly defined by the y translation you apply, which is 100. So your canvas has height 100, which means the circle's center would need to be at y = 100/2 = 50 to stay put.
Since your circle's center is at y = 100, you need to use a canvas height of 2*100 = 200 to make the circle stay put.

Related

Trying to scale a view from it's top left corner

I have a child view controller whose view has been added as a subview on the parent view. I want to scale this by 1.1x from its top left corner. I tried doing this:
override func viewDidLoad() {
self.view.layer.anchorPoint = CGPoint(x: 0, y: 0)
}
func scaleMyView() {
self.view.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
}
But that seems to re-centre the view over it's top left corner and then scale.
After reading some answers on here, I then tried setting the position of the view's layer like this:
let center = self.view.frame.origin
self.view.layer.anchorPoint = CGPoint(x: 0, y: 0)
self.view.layer.position = center
But that moves the view somewhere to the center of the screen. How can I just make it scale from its top left corner without it moving in the parent view??
You're exactly right and extremely close. To change where a transform is anchored, you need to change the layer's anchor point. But when you do that, you need to change the layer's position to compensate, or the layer will move.
Let self.v be the view we want to transform:
let lay = self.v.layer
let p = lay.frame.origin
lay.anchorPoint = CGPoint(x: 0, y: 0)
lay.position = p
lay.setAffineTransform(CGAffineTransform(scaleX: 1.1, y: 1.1))

How to rotate a CGRect about its centre?

Background:
I want to show a label that is rotated 90 degrees clockwise. The rotated label should be in a certain frame that I specify. Let's say (10, 100, 50, 100).
I know that I can rotate a view by using CGAffineTransform. I also know that I should not set the frame property after a transformation is done, according to the docs.
When the value of this property is anything other than the identity transform, the value in the frame property is undefined and should be ignored.
I tried to set the frame after the transform and although it worked, I don't want to do something that the docs told me not to.
label2.transform = CGAffineTransform(rotationAngle: -.pi / 2)
label2.frame = CGRect(x: 10, y: 100, width: 50, height: 100)
Then I thought, I could do this:
create a CGRect of the frame that I want
rotate that rect 90 degrees anticlockwise
set that rect as the frame of the label
rotate the label 90 degrees clockwise
And then the label would be in the desired frame.
So I tried something like this:
let desiredFrame = CGRect(x: 10, y: 100, width: 50, height: 100) // step 1
// I am using UIViews here because I am just looking at their frames
// whether it is a UIView or UILabel does not matter
// label1 is a view that is in the desired frame but not rotated
// If label2 has the correct frame, it should completely cover label1
let label1 = UIView(frame: desiredFrame)
let label2Frame = rotateRect(label1.frame) // step 2
let label2 = UIView(frame: label2Frame) // step 3
view.addSubview(label1)
view.addSubview(label2)
label1.backgroundColor = .red
label2.backgroundColor = .blue
label2.transform = CGAffineTransform(rotationAngle: -.pi / 2) // step 4
Where rotateRect is declared like this:
func rotateRect(_ rect: CGRect) -> CGRect {
return rect.applying(CGAffineTransform(rotationAngle: .pi / 2))
}
This didn't work. label2 does not overlap label1 at all. I can't even see label2 at all on the screen.
I suspect that this is because the applying method in CGRect rotates the rect about the origin, instead of the centre of the rect. So I tried to first transform the rect to the origin, rotate it, then transform it back, as this post on math.SE said:
func rotateRect(_ rect: CGRect) -> CGRect {
let x = rect.x
let y = rect.y
let transform = CGAffineTransform(translationX: -x, y: -y)
.rotated(by: .pi / 2)
.translatedBy(x: x, y: y)
return rect.applying(transform)
}
However, I still cannot see label2 on the screen.
I think that the order of your transformations are incorrect. If you do it so, that,
Translate(x, y) * Rotate(θ) * Translate(-x, -y)
And using that with your rotateRect seem to work correctly. Since, you are rotating the view by 90 degrees, the blue view completely block red view. Try it out with some other angle and you shall see effect more prominently.
func rotateRect(_ rect: CGRect) -> CGRect {
let x = rect.midX
let y = rect.midY
let transform = CGAffineTransform(translationX: x, y: y)
.rotated(by: .pi / 2)
.translatedBy(x: -x, y: -y)
return rect.applying(transform)
}
Since I only want to rotate the view by 90 degrees, the width and height of the initial frame will be the reverse of the width and height of the rotated frame.
The centers of the initial frame and rotated are the same, so we can set the center of the label to the centre of the desired frame before the transformation.
let desiredFrame = CGRect(x: 10, y: 100, width: 50, height: 100)
let label1 = UIView(frame: desiredFrame)
// get the centre of the desired frame
// this is also equal to label1.center
let center = CGPoint(x: desiredFrame.midX, y: desiredFrame.midY)
let label2Frame = CGRect(x: 0, y: 0, width: desiredFrame.height, height: desiredFrame.width)
let label2 = UIView(frame: label2Frame)
view.addSubview(label1)
view.addSubview(label2)
label1.backgroundColor = .red
label2.backgroundColor = .blue
label2.center = center // set the centre of label2 before the transform
label2.transform = CGAffineTransform(rotationAngle: -.pi / 2)

With strokeColor, a line is drawn from the center to the circumference

I am using swift 4.
I wrote the code below and tried to draw a circle with stroke.
However, with the code below I could not draw my desired circle.
The problem is that the line is drawn from the center of the circle to the outer periphery (strictly speaking, toward the coordinate point of the 'startAngle' property).
I want to erase the line, what should I do?
(I prepared an image.)
class Something{
var line:[CAShapeLayer] = []
var path:[UIBezierPath] = []
func drawingNow(){
let layer = CAShapeLayer()
self.layer.addSublayer(layer)
self.line.append(layer)
let addPath: UIBezierPath = UIBezierPath()
addPath.move(to: CGPoint(x: 100, y: 100))
addPath.addArc(
withCenter: CGPoint(x: 100, y: 100),
radius: CGFloat(50),
startAngle: CGFloat(//someangle),
endAngle: CGFloat(//someangle),
clockwise: true
)
self.path.append(addPath)
//self.line.last!.strokeColor = etc... (If don't use "override func draw()")
self.line.last!.fillColor = UIColor.clear.cgColor
self.line.last!.path = addPath.cgPath
self.setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
if self.path.count != 0{
UIColor.orange.setStroke()
self.path.last!.stroke()
}
}
}
Image
After calling addPath.move(to: CGPoint(x: 100, y: 100)), the UIBezierPath moved to the specified coordinate. Now you are telling it to add an arc from there with a centre of (100, 100). To draw an arc with a centre (100, 100), it first need to move to the circumference. But at this point it already starts drawing! That's just how addArc works. The same goes for addLine. Look at the docs:
This method adds the specified arc beginning at the current point.
So the arc always starts at the current point, which is (100, 100).
Instead of telling it to move to the centre first, just tell it to draw an arc by removing the move(to:)` line.

Creating circle on the every edge of rectangle

I want to create on the edge of every side of the rectangle a small circle instead of only corner.
I make a label that set :
exampleLabel.layer.borderWidth = 2.0
exampleLabel.layer.borderColor = UIColor.black.cgColor
So to get something like this shape :
You can also draw in each corner a circle.
let circle = UIView(frame: CGRect(x: -2.5, y: -2.5, width: 5.0, height: 5.0))
circle.layer.cornerRadius = 2.5
circle.backgroundColor = UIColor.black
self.exampleLabel.addSubview(circle)
this is a circle in the left top corner. You can do it also for left bottom/right top/ right bottom.
You can play with the position by the x and y value
Let me know if this is what you want

How do I use this circle drawing code in a UIView?

I am making an app including some breathing techniques for a client. What he wants is to have a circle in the middle. For breathing in it becomes bigger, for breathing out tinier. The thing is, that he would like to have a cool animated circle in the middle, not just a standard one. I showed him this picture from YouTube:
The code used in the video looks like this:
func drawRotatedSquares() {
UIGraphicsBeginImageContextWithOptions(CGSize(width: 512, height: 512), false, 0)
let context = UIGraphicsGetCurrentContext()
context!.translateBy(x: 256, y: 256)
let rotations = 16
let amount = M_PI_2 / Double(rotations)
for i in 0 ..< rotations {
context!.rotate(by: CGFloat(amount))
//context!.addRect(context, CGRect(x: -128, y: -128, width: 256, height: 256))
context!.addRect(CGRect(x: -128, y: -128, width: 256, height: 256))
}
context!.setStrokeColor(UIColor.black as! CGColor)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
imageView.image = img
}
But if I run it, my simulator shows just a white screen. How do I get this circle into my Swift 3 app and how would the code look like? And is it possible not to show it in an ImageView but simply in a view?
Thank you very much!
Here is an implementation as a UIView subclass.
To set up:
Add this class to your Swift project.
Add a UIView to your Storyboard and change the class to Circle.
Add an outlet to your viewController
#IBOutlet var circle: Circle!
Change the value of multiplier to change the size of the circle.
circle.multiplier = 0.5 // 50% of size of view
class Circle: UIView {
var multiplier: CGFloat = 1.0 {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
// Calculate size of square edge that fits into the view
let size = min(bounds.width, bounds.height) * multiplier / CGFloat(sqrt(2)) / 2
// Move origin to center of the view
context.translateBy(x: center.x, y: center.y)
// Create a path to draw a square
let path = UIBezierPath()
path.move(to: CGPoint(x: -size, y: -size))
path.addLine(to: CGPoint(x: -size, y: size))
path.addLine(to: CGPoint(x: size, y: size))
path.addLine(to: CGPoint(x: size, y: -size))
path.close()
UIColor.black.setStroke()
let rotations = 16
let amount = .pi / 2 / Double(rotations)
for _ in 0 ..< rotations {
// Rotate the context
context.rotate(by: CGFloat(amount))
// Draw a square
path.stroke()
}
}
}
Here it is running in a Playground:
You posted a singe method that generates a UIImage and installs it in an image view. If you don't have the image view on-screen then it won't show up.
If you create an image view in your view controller and connect an outlet to the image view then the above code should install the image view into your image and draw it on-screen.
You could rewrite the code you posted as the draw(_:) method of a custom subclass of UIView, in which case you'd get rid of the context setup and UIImage stuff, and simply draw in the current context. I suggest you search on UIView custom draw(_:) methods for more guidance.

Resources