Playground code: AffineTransform and AnchorPoint don't work as I want - ios

I want to shrink a triangle downward as below:
But the triangle shrink upward as below:
The transform code is below:
layer.setAffineTransform(CGAffineTransformMakeScale(1.0, 0.5))
I tried to use anchorPoint but I can't scceed.
//: Playground - noun: a place where people can play
import UIKit
let view = UIView(frame: CGRectMake(0, 0, 200, 150))
let layer = CAShapeLayer()
let triangle = UIBezierPath()
triangle.moveToPoint (CGPointMake( 50, 150))
triangle.addLineToPoint(CGPointMake(100, 50))
triangle.addLineToPoint(CGPointMake(150, 150))
triangle.closePath()
layer.path = triangle.CGPath
layer.strokeColor = UIColor.blueColor().CGColor
layer.lineWidth = 3.0
view.layer.addSublayer(layer)
view
layer.setAffineTransform(CGAffineTransformMakeScale(1.0, 0.5))
view
Anish gave me a advice but doesn't work well:
layer.setAffineTransform(CGAffineTransformMakeScale(2.0, 0.5))

Transforms operate around the layer's anchorPoint, which by default is at the center of the layer. Thus, you shrink by collapsing inwards, not by collapsing downwards.
If you want to shrink downward, you will need to scale down and change the view's position (or use a translate transform in addition to your scale transform), or change the layer's anchorPoint before performing the transform.
Another serious problem with your code is that your layer has no assigned size. This causes the results of your transform to be very misleading.
I ran this version of your code and got exactly the results you are asking for. I have put a star next to the key lines that I added. (Note that this is Swift 3; you really need to step up to the plate and update here.)
let view = UIView(frame:CGRect(x: 0, y: 0, width: 200, height: 150))
let layer = CAShapeLayer()
layer.frame = view.layer.bounds // *
let triangle = UIBezierPath()
triangle.move(to: CGPoint(x: 50, y: 150))
triangle.addLine(to: CGPoint(x: 100, y: 50))
triangle.addLine(to: CGPoint(x: 150, y: 150))
triangle.close()
layer.path = triangle.cgPath
layer.strokeColor = UIColor.blue.cgColor
layer.lineWidth = 3
view.layer.addSublayer(layer)
view
layer.anchorPoint = CGPoint(x: 0.5, y: 0) // *
layer.setAffineTransform(CGAffineTransform(scaleX: 1, y: 0.5))
view
Before:
After:

Related

Is it possible to show/stroke only a portion of a UIBezierPath?

I have a UIBezierPath shape (comprised of various line segments) already defined, but depending on the scenario, I only want certain portions of it visible. Is this possible?
I was initially thinking along the lines of how when you animate a UIBezierPath, you can animate from a .fromValue to a .toValue. Can I set some sort of "current value" (which I see exists, but only as a getOnly property) to accomplish something similar, outside of an animation context?
Another way I thought of was to just treat my path as a sort of "template" path, and then break it up into various subpaths, but I'm hoping there is an easier/more direct way (unless this method is easier than I anticipate...).
UIBezierPath doesn't have that, but if you add the path to a CAShapeLayer, you can control the strokeStart and strokeEnd.
Try this in a playground:
let path = UIBezierPath()
path.move(to: .zero)
path.addLine(to: CGPoint(x: 10, y: 10))
path.addLine(to: CGPoint(x: 20, y: 0))
path.addLine(to: CGPoint(x: 30, y: 10))
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.strokeColor = UIColor.red.cgColor
layer.fillColor = UIColor.clear.cgColor
// Note these lines
layer.strokeStart = 0.2 // start at 20% of the way
layer.strokeEnd = 0.8 // end at 80% of the way
layer.lineWidth = 2
let view = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
view.layer.addSublayer(layer)
The above produces:
Without the two lines that set the stroke start and end, the view looks like:

Why the edges of UIView are not smooth for huge corner radius?

class AttributedView: UIView {
private let gradient = CAGradientLayer()
var cornerRadius: CGFloat = 3 {
didSet {
layer.cornerRadius = cornerRadius
}
}
}
I simply use it for both: button view (corner radius: 20) and background circle (corner radius: 600).
Why button is smooth, and background is not?
With iOS 13.0 you can simple do, in addition to setting corner radius
yourView.layer.cornerCurve = .continuous
You should use bezzier paths and draw circle. After that you will receive nice, smooth edges.
let context = UIGraphicsGetCurrentContext()!
let gradient = CGGradient(colorsSpace: nil, colors: [UIColor.red.cgColor, UIColor.white.cgColor] as CFArray, locations: [0, 1])!
let ovalPath = UIBezierPath(ovalIn: CGRect(x: 64, y: 9, width: 111, height: 93))
context.saveGState()
ovalPath.addClip()
context.drawLinearGradient(gradient, start: CGPoint(x: 119.5, y: 9), end: CGPoint(x: 119.5, y: 102), options: [])
context.restoreGState()
UIBezierPath is a simple and efficient class for drawing shapes using Swift, which you can then put into CAShapeLayer, SKShapeNode, or other places. It comes with various shapes built in, so you can write code like this to create a rounded rectangle or a circle:
let rect = CGRect(x: 0, y: 0, width: 256, height: 256)
let roundedRect = UIBezierPath(roundedRect: rect, cornerRadius: 50)
let circle = UIBezierPath(ovalIn: rect)
You can also create custom shapes by moving a pen to a starting position then adding lines:
let freeform = UIBezierPath()
freeform.move(to: .zero)
freeform.addLine(to: CGPoint(x: 50, y: 50))
freeform.addLine(to: CGPoint(x: 50, y: 150))
freeform.addLine(to: CGPoint(x: 150, y: 50))
freeform.addLine(to: .zero)
If your end result needs a CGPath, you can get one by accessing the cgPath property of your UIBezierPath.
You probably should clip bounds of this view:
attributedView.clipsToBounds = true

Rotate UIBezierPath rectangle [duplicate]

This question already has answers here:
How to rotate UIBezierPath around center of its own bounds?
(3 answers)
Closed 4 years ago.
I wanted to rotate my ellipse but I don't manage to do it.
I tried to make a CGAffineTransform, but my rectangle disappeared and I also tried a to do a CATransformRotate or something like this on note, but didn't managed to use it properly
let ovalPath = UIBezierPath(ovalIn: CGRect(x: 100, y: 100, width: 10, height: 15))
UIColor.gray.setFill()
ovalPath.fill()
let rotation = CGAffineTransform(rotationAngle: CGFloat(M_PI / 3.0))
ovalPath.apply(rotation)
let note = CAShapeLayer()
note.path = ovalPath.cgPath
note.fillColor = UIColor.black.cgColor
view.layer.addSublayer(note)
You're rotating the path off the screen because of the x/y offset in the cgrect (it's rotating around (0,0)). try this and rotate ccw a bit to see the rotation behavior work correctly. If you want the layer to be at coordinate 100,100 you can use another transform to move it:
let ovalPath = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 10, height: 15))
UIColor.gray.setFill()
ovalPath.fill()
let rotation = CGAffineTransform(rotationAngle: -CGFloat(M_PI / 3.0))
ovalPath.apply(rotation)
let note = CAShapeLayer()
note.path = ovalPath.cgPath
note.fillColor = UIColor.black.cgColor
view.layer.addSublayer(note)

Adding the same layer in a subview changes its visual position

I added a subview (with a black border) in a view and centered it.
Then I generate 2 identical triangles with CAShapeLayer and add one to the subview and the other to the main view.
Here is the visual result in Playground where we can see that the green triangle is totally off and should have been centered.
And here is the code:
let view = UIView()
let borderedView = UIView()
var containedFrame = CGRect(x: 0, y: 0, width: 100, height: 100)
func setupUI() {
view.frame = CGRect(x: 0, y: 0, width: 300, height: 600)
view.backgroundColor = .white
borderedView.frame = containedFrame
borderedView.center = view.center
borderedView.backgroundColor = .clear
borderedView.layer.borderColor = UIColor.black.cgColor
borderedView.layer.borderWidth = 1
view.addSubview(borderedView)
setupTriangles()
}
private func setupTriangles() {
view.layer.addSublayer(createTriangle(color: .red)) // RED triangle
borderedView.layer.addSublayer(createTriangle(color: .green)) // GREEN triangle
}
private func createTriangle(color: UIColor) -> CAShapeLayer {
let layer = CAShapeLayer()
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: -containedFrame.width, y: 0))
bezierPath.addLine(to: CGPoint(x: 0, y: -containedFrame.height))
bezierPath.close()
layer.position = borderedView.center
layer.path = bezierPath.cgPath
layer.fillColor = color.cgColor
return layer
}
Note: All position (of view, the borderedView and both triangles) are the same (150.0, 300.0)
Question: Why is the green layer not in the right position?
#DuncanC is right that each view has its own coordinate system. Your problem is this line:
layer.position = borderedView.center
That sets the layer's position to the center of the frame for the borderedView which is in the coordinate system of view. When you create the green triangle, it needs to use the coordinate system of borderedView.
You can fix this by passing the view to your createTriangle function, and then use the center of the bounds of that view as the layer position:
private func setupTriangles() {
view.layer.addSublayer(createTriangle(color: .red, for: view)) // RED triangle
borderedView.layer.addSublayer(createTriangle(color: .green, for: borderedView)) // GREEN triangle
}
private func createTriangle(color: UIColor, for view: UIView) -> CAShapeLayer {
let layer = CAShapeLayer()
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: -containedFrame.width, y: 0))
bezierPath.addLine(to: CGPoint(x: 0, y: -containedFrame.height))
bezierPath.close()
layer.position = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
layer.path = bezierPath.cgPath
layer.fillColor = color.cgColor
return layer
}
Note: When you do this, the green triangle appears directly below the red one, so it isn't visible.
Every view/layer uses the coordinate system of it's superview/superlayer. If you add a layer to self.view.layer, it will be positioned in self.view.layer's coordinate system. If you add a layer to borderedView.layer, it will be in borderedView.layer's coordinate system.
Think of the view/layer hierarchy as stacks of pieces of graph paper. You place a new piece of paper on the current piece (the superview/layer) in the current piece's coordinates system, but then if you draw on the new view/layer, or add new views/layer inside that one, you use the new view/layer's coordinate system.

Using drawRect to draw & animate two graphs. A background graph, and a foreground graph

I am using drawRect to draw a pretty simple shape (dark blue in the image below).
I'd like this to animate from the left to the right, so that it grows. The caveat here is I need there to be a "max" background in gray, as seen in the top part of the image.
Right now, I'm simulating this animation by overlaying a white view, and then animating the size of it, so that it looks like the blue is animating to the right. While this works... I need the background gray shape to always be there. With my overlayed white view, this just doesn't work.
Here's the code for drawing the "current code" version:
let context = UIGraphicsGetCurrentContext()
CGContextMoveToPoint(context, 0, self.bounds.height - 6)
CGContextAddLineToPoint(context, self.bounds.width, 0)
CGContextAddLineToPoint(context, self.bounds.width, self.bounds.height)
CGContextAddLineToPoint(context, 0, self.bounds.height)
CGContextSetFillColorWithColor(context,UIColor(red: 37/255, green: 88/255, blue: 120/255, alpha: 1.0).CGColor)
CGContextDrawPath(context, CGPathDrawingMode.Fill)
How can I animate the blue part from left to right, while keeping the gray "max" portion of the graph always visible?
drawRect is producing still picture. To get animation you're saying about I'd recommend the following:
Use CoreAnimation to produce animation
Use UIBezierPath to make a shape you need
Use CALayer's mask to animate within required shape
Here is example code for Playground:
import UIKit
import QuartzCore
import XCPlayground
let view = UIView(frame: CGRect(x: 0, y: 0, width: 120, height: 40))
XCPlaygroundPage.currentPage.liveView = view
let maskPath = UIBezierPath()
maskPath.moveToPoint(CGPoint(x: 10, y: 30))
maskPath.addLineToPoint(CGPoint(x: 10, y: 25))
maskPath.addLineToPoint(CGPoint(x: 100, y: 10))
maskPath.addLineToPoint(CGPoint(x: 100, y: 30))
maskPath.closePath()
let maskLayer = CAShapeLayer()
maskLayer.path = maskPath.CGPath
maskLayer.fillColor = UIColor.whiteColor().CGColor
let rectToAnimateFrom = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 97, height: 40))
let rectToAnimateTo = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 0, height: 40))
let layerOne = CAShapeLayer()
layerOne.path = maskPath.CGPath
layerOne.fillColor = UIColor.grayColor().CGColor
let layerTwo = CAShapeLayer()
layerTwo.mask = maskLayer
layerTwo.fillColor = UIColor.greenColor().CGColor
view.layer.addSublayer(layerOne)
view.layer.addSublayer(layerTwo)
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = rectToAnimateFrom.CGPath
animation.toValue = rectToAnimateTo.CGPath
animation.duration = 1
animation.repeatCount = 1000
animation.autoreverses = true
layerTwo.addAnimation(animation, forKey: "Nice animation")
In your code, I only see you draw the graphic once, why not draw gray part first and then draw the blue part.
I don't think it is efficient enough to implement animation in drawRect function.
You can take a look at Facebook's Shimmer Example, it simulate the effect of iPhone unlock animation. It uses a mask layer. The idea could also work in your example.
Also, Facebook's pop framework could simplify your work.

Resources