Unexpected behavior in scaling CAShapeLayer - ios

I am using the following code to scale a CAShapelayer instance (i.e., self.view.layer.sublayers![0]):
class ViewController: UIViewController {
override func viewDidLoad()
{
super.viewDidLoad()
self.view = Map(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height))
let pinchGR = UIPinchGestureRecognizer(target: self, action: #selector(self.didPinch(_:)))
self.view.addGestureRecognizer(pinchGR)
}
#objc func didPinch(_ pinchGR: UIPinchGestureRecognizer)
{
let transformation = CGAffineTransform(a: pinchGR.scale, b: 0, c: 0, d: pinchGR.scale, tx: 0, ty: 0)
if pinchGR.state == .began || pinchGR.state == .changed
{
self.view.layer.sublayers![0].setAffineTransform(transformation)
}
}}
class Map: UIView{
override init(frame: CGRect)
{
super.init(frame: frame)
self.backgroundColor = UIColor.blue
drawTestShape()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
}
func drawTestShape()
{
let shapeLayer = CAShapeLayer()
shapeLayer.frame = self.frame
let path = UIBezierPath()
shapeLayer.lineWidth = 5.0
shapeLayer.fillColor = nil
shapeLayer.strokeColor = UIColor.white.cgColor
shapeLayer.backgroundColor = UIColor.red.cgColor
path.move(to: CGPoint(x:200,y:200))
path.addLine(to: CGPoint(x:300,y:300))
path.addLine(to: CGPoint(x:100,y:300))
path.close()
shapeLayer.path = path.cgPath
self.layer.addSublayer(shapeLayer)
}}
The scaling is performed correctly in the first pinch gesture. However, when a second pinch gesture is applied, the CAShapelayer instance starts again from its initial size, and not from the size it had after the first gesture.

I can't really get behind the idea of scaling a layer, but if you must do it, try something more like this; this is the standard pattern for implementing a pinch gesture recognizer, so it's really best to stick with it:
#IBAction func didPinch(_ pinchGR: UIPinchGestureRecognizer) {
self.view.layer.sublayers![0].setAffineTransform(
self.view.layer.sublayers![0].affineTransform().scaledBy(
x:pinchGR.scale, y:pinchGR.scale))
pinchGR.scale = 1.0;
}

Related

Mask a gradient layer with the intersection of two shape layers in Swift

I have two shapes of type CAShapeLayer (e.g. one box and a circle) and a gradient layer of type CAGradientLayer. How can I mask the gradient layer with the intersection of the two shapes like this picture in Swift?
Not exactly clear what you mean by "intersection of the two shapes" ... but maybe this is what you're going for:
To get that, we can create a CAShapeLayer with an oval (round) path, and use it as a mask on the gradient layer.
Here's some example code:
class GradientMaskingViewController: UIViewController {
let gradView = MaskedGradView()
override func viewDidLoad() {
super.viewDidLoad()
gradView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(gradView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
gradView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
gradView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
gradView.heightAnchor.constraint(equalTo: gradView.widthAnchor),
gradView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
gradView.colorArray = [
.blue, .orange, .purple, .yellow
]
}
}
class MaskedGradView: UIView {
enum Direction {
case horizontal, vertical, diagnal
}
public var colorArray: [UIColor] = [] {
didSet {
setNeedsLayout()
}
}
public var locationsArray: [NSNumber] = [] {
didSet {
setNeedsLayout()
}
}
public var direction: Direction = .vertical {
didSet {
setNeedsLayout()
}
}
private let gLayer = CAGradientLayer()
private let maskLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
// add gradient layer as a sublayer
layer.addSublayer(gLayer)
// mask it
gLayer.mask = maskLayer
// we'll use a 120-point diameter circle for the mask
maskLayer.path = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: 120.0, height: 120.0)).cgPath
// so we can see this view's frame
layer.borderColor = UIColor.black.cgColor
layer.borderWidth = 1
}
override func layoutSubviews() {
super.layoutSubviews()
// update gradient layer
// frame
// colors
// locations
gLayer.frame = bounds
gLayer.colors = colorArray.map({ $0.cgColor })
if locationsArray.count > 0 {
gLayer.locations = locationsArray
}
switch direction {
case .horizontal:
gLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
case .vertical:
gLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
gLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
case .diagnal:
gLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
}
}
// touch code to drag the circular mask around
private var curPos: CGPoint = .zero
private var lPos: CGPoint = .zero
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
curPos = touch.location(in: self)
lPos = maskLayer.position
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let newPos = touch.location(in: self)
let diffX = newPos.x - curPos.x
let diffY = newPos.y - curPos.y
CATransaction.begin()
CATransaction.setDisableActions(true)
maskLayer.position = CGPoint(x: lPos.x + diffX, y: lPos.y + diffY)
CATransaction.commit() }
}
To help make it clear, I added touch handling code so you can drag the circle around inside the view:
Edit - after comment (but still missing details), let's try this again...
We can get the desired output by using multiple layers.
a white-filled gray-bordered rectangle CAShapeLayer
a white-filled NON-bordered oval CAShapeLayer
a CAGradientLayer masked with an oval CAShapeLayer
a NON-filled gray-bordered oval CAShapeLayer
So, we start with a view:
add a white-filled gray-bordered rectangle CAShapeLayer:
add a white-filled NON-bordered oval CAShapeLayer (red first, to show it clearly):
add a CAGradientLayer:
mask it with an oval CAShapeLayer:
finally, add a NON-filled gray-bordered oval CAShapeLayer:
Here's the example code:
class GradientMaskingViewController: UIViewController {
let gradView = MultiLayeredGradView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
gradView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(gradView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
gradView.widthAnchor.constraint(equalToConstant: 240.0),
gradView.heightAnchor.constraint(equalTo: gradView.widthAnchor),
gradView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
gradView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
}
class MultiLayeredGradView: UIView {
private let rectLayer = CAShapeLayer()
private let filledCircleLayer = CAShapeLayer()
private let gradLayer = CAGradientLayer()
private let maskLayer = CAShapeLayer()
private let outlineCircleLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
// add filled-bordered rect layer as a sublayer
layer.addSublayer(rectLayer)
// add filled circle layer as a sublayer
layer.addSublayer(filledCircleLayer)
// add gradient layer as a sublayer
layer.addSublayer(gradLayer)
// mask it
gradLayer.mask = maskLayer
// add outline circle layer as a sublayer
layer.addSublayer(outlineCircleLayer)
let bColor: CGColor = UIColor.gray.cgColor
let fColor: CGColor = UIColor.white.cgColor
// filled-outlined
rectLayer.strokeColor = bColor
rectLayer.fillColor = fColor
rectLayer.lineWidth = 2
// filled
filledCircleLayer.fillColor = fColor
// clear-outlined
outlineCircleLayer.strokeColor = bColor
outlineCircleLayer.fillColor = UIColor.clear.cgColor
outlineCircleLayer.lineWidth = 2
// gradient layer properties
let colorArray: [UIColor] = [
.blue, .orange, .purple, .yellow
]
gradLayer.colors = colorArray.map({ $0.cgColor })
gradLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
gradLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
}
override func layoutSubviews() {
super.layoutSubviews()
// circle diameter is 45% of the width of the view
let circleDiameter: CGFloat = bounds.width * 0.45
// circle Top is at vertical midpoint
// circle is moved Left by 25% of the circle diameter
let circleBounds: CGRect = CGRect(x: bounds.minX - circleDiameter * 0.25,
y: bounds.maxY * 0.5,
width: circleDiameter,
height: circleDiameter)
// gradient layer fills the bounds
gradLayer.frame = bounds
let rectPath = UIBezierPath(rect: bounds).cgPath
rectLayer.path = rectPath
let circlePath = UIBezierPath(ovalIn: circleBounds).cgPath
filledCircleLayer.path = circlePath
outlineCircleLayer.path = circlePath
maskLayer.path = circlePath
}
}

Empty view, with only clickable borders

I need to have a view with borders, and only borders should be clickable.
Here is my code which creates custom view, but when I added gesture all view becomes clickable.
class RectangleView: UIView {
override func draw(_ rect: CGRect) {
let aPath = UIBezierPath()
aPath.move(to: CGPoint(x:0, y: 0))
aPath.addLine(to: CGPoint(x:rect.width, y: 0))
aPath.addLine(to: CGPoint(x:rect.width, y:rect.height))
aPath.addLine(to: CGPoint(x:0, y:rect.height))
aPath.close()
UIColor.green.setStroke()
aPath.stroke()
UIColor.clear.setFill()
aPath.fill()
}
}
Edited, here is the screenshot from other app
so I will click on view under my top view, this one will be in front and will be clickable, so I cant add smaller subView
The most correct way here is to override func point(inside point: CGPoint, with event: UIEvent?) -> Bool for UIView subclass.
Here is example:
class CustomView: UIView {
private let shapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
setupShapeLayer()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupShapeLayer() {
//Draw your own shape here
shapeLayer.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
let innerPath = UIBezierPath(arcCenter: center, radius: frame.width / 3, startAngle: CGFloat(0), endAngle: CGFloat.pi * 2, clockwise: true)
let outerPath = UIBezierPath(arcCenter: center, radius: frame.width / 4, startAngle: CGFloat(0), endAngle: CGFloat.pi * 2, clockwise: true)
//Subtract inner path
innerPath.append(outerPath.reversing())
shapeLayer.path = innerPath.cgPath
shapeLayer.lineWidth = 10
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.fillColor = UIColor.yellow.cgColor
layer.addSublayer(shapeLayer)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return shapeLayer.path?.contains(point) ?? false
}
}
Now you can add UITapGestureRecognizer to check how it works:
class ViewController: UIViewController {
#IBOutlet weak var resultLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let customView = CustomView.init(frame: view.frame)
view.addSubview(customView)
let recognizer = UITapGestureRecognizer(target: self, action: #selector(tapAction))
view.addGestureRecognizer(recognizer)
}
#objc func tapAction(_ sender: UITapGestureRecognizer) {
let location = sender.location(in: sender.view)
let subview = view?.hitTest(location, with: nil)
if subview is CustomView {
resultLabel.text = "CustomView"
}
else {
resultLabel.text = "Other"
}
}
}
Result of tapAction:
If you need several views, there are two possible options:
Store CAShapeLayer inside one CustomView and iterate through it inside point function
Add CustomView for each instance and iterate it through inside UITapGestureRecognizer tap action

Problems rotating UIView with drawing inside

I want to rotate a UIView with a drawing inside (circle, rectangle or line). The problem is when I rotate the view and refresh the drawing (I need ir when I change some properties of the drawing, i.e) the drawing doesn't follow the view...
UIView without rotation:
UIView with rotation:
Here is my simple code (little extract from the original code):
Main ViewController
class TestRotation: UIViewController {
// MARK: - Properties
var drawingView:DrawingView?
// MARK: - Actions
#IBAction func actionSliderRotation(_ sender: UISlider) {
let degrees = sender.value
let radians = CGFloat(degrees * Float.pi / 180)
drawingView?.transform = CGAffineTransform(rotationAngle: radians)
drawingView?.setNeedsDisplay()
}
// MARK: - Init
override func viewDidLoad() {
super.viewDidLoad()
drawingView = DrawingView(frame:CGRect(x:50, y:50, width: 100, height:75))
self.view.addSubview(drawingView!)
drawingView?.setNeedsDisplay()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
UIView
class DrawingView: UIView {
override func draw(_ rect: CGRect) {
let start = CGPoint(x: 0, y: 0)
let end = CGPoint(x: self.frame.size.width, y: self.frame.size.height)
drawCicrle(start: start, end: end)
}
func drawCicrle(start:CGPoint, end:CGPoint) {
//Contexto
let context = UIGraphicsGetCurrentContext()
if context != nil {
//Ancho
context!.setLineWidth(2)
//Color
context!.setStrokeColor(UIColor.black.cgColor)
context!.setFillColor(UIColor.orange.cgColor)
let rect = CGRect(origin: start, size: CGSize(width: end.x-start.x, height: end.y-start.y))
let path = UIBezierPath(ovalIn: rect)
path.lineWidth = 2
path.fill()
path.stroke()
}
}
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.gray
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
It seems that the problem is temporally solved if I don't refresh the view after rotating, but if I have to refresh later for some other reason (properties changed), the problem appears again.
What can I do? Thanks in advance
You are using the frame when you should be using the bounds.
The frame is the rectangle which contains the view. It is specified in the coordinate system of the superview of the view, and the frame changes when you rotate the view (because a rotated square extends further in the X and Y directions than a non-rotated square).
The bounds is the rectangle which contains the contents of the view. It is specified in the coordinate system of the view itself, and it doesn't change as the view is rotated or moved.
If you change frame to bounds in your draw(_:) function, it will work as you expect:
override func draw(_ rect: CGRect) {
let start = CGPoint(x: 0, y: 0)
let end = CGPoint(x: self.bounds.size.width, y: self.bounds.size.height)
drawCicrle(start: start, end: end)
}
Here's a demo showing the oval being redrawn as the slider moves.
Here's the changed code:
class DrawingView: UIView {
var fillColor = UIColor.orange {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
let start = CGPoint(x: 0, y: 0)
let end = CGPoint(x: self.bounds.size.width, y: self.bounds.size.height)
drawCircle(start: start, end: end)
}
func drawCircle(start:CGPoint, end:CGPoint) {
//Contexto
let context = UIGraphicsGetCurrentContext()
if context != nil {
//Ancho
context!.setLineWidth(2)
//Color
context!.setStrokeColor(UIColor.black.cgColor)
context!.setFillColor(fillColor.cgColor)
let rect = CGRect(origin: start, size: CGSize(width: end.x-start.x, height: end.y-start.y))
let path = UIBezierPath(ovalIn: rect)
path.lineWidth = 2
path.fill()
path.stroke()
}
}
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.gray
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
#IBAction func actionSliderRotation(_ sender: UISlider) {
let degrees = sender.value
let radians = CGFloat(degrees * Float.pi / 180)
drawingView?.transform = CGAffineTransform(rotationAngle: radians)
drawingView?.fillColor = UIColor(hue: CGFloat(degrees)/360.0, saturation: 1.0, brightness: 1.0, alpha: 1.0)
}

Animation origin iOS

I've been trying to create an animation that increases the height of a view without changing the origin (the top left and right edges stay in the same position)
import UIKit
class SampRect: UIView {
var res : CGRect?
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor(red: 0.91796875, green: 0.91796875, blue: 0.91796875, alpha: 1)
self.layer.cornerRadius = 5
let recognizer = UITapGestureRecognizer(target: self, action:#selector(SampRect.handleTap(_:)))
self.addGestureRecognizer(recognizer)
}
func handleTap(recognizer : UITapGestureRecognizer) {
let increaseSize = CABasicAnimation(keyPath: "bounds")
increaseSize.delegate = self
increaseSize.duration = 0.4
increaseSize.fromValue = NSValue(CGRect: frame)
res = CGRect(x: self.frame.minX, y: self.frame.minY, width: frame.width, height: self.frame.height + 10)
increaseSize.toValue = NSValue(CGRect: res!)
increaseSize.fillMode = kCAFillModeForwards
increaseSize.removedOnCompletion = false
layer.addAnimation(increaseSize, forKey: "bounds")
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
self.frame = res!
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
but the animation increase the size from the middle (push the top edge up and the bottom edge down)
what am I missing?
You can Try this.
let newheight:CGFloat = 100.0
UIView.animateWithDuration(0.5) {
self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width, newheight)
}

My CoreGraphics code is working really slowly. How can I make this custom UIScrollView perform better?

I made a custom class that creates a sort of timeline that is meant to be scrolled horizontally. Here's the code for my custom UIScrollView :
import UIKit
struct DataPoint {
var fillColor: UIColor = UIColor.grayColor()
init(color: UIColor) {
fillColor = color
}
}
#IBDesignable
class MyView: UIScrollView {
#IBInspectable var lineColor: UIColor = UIColor.grayColor()
#IBInspectable var lineHeight: CGFloat = 65
#IBInspectable var lineWidth: CGFloat = 15
#IBInspectable var lineGap: CGFloat = 25
#IBInspectable var lineCount: Int = 0
var dataPoints = [DataPoint(color: UIColor.greenColor()), DataPoint(color: UIColor.blueColor())]
override init(frame: CGRect) {
super.init(frame: frame)
setupValues()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupValues()
//fatalError("init(coder:) has not been implemented")
}
func setupValues() {
self.contentSize = CGSize(width: self.frame.width * 2, height: self.frame.height)
self.backgroundColor = UIColor.whiteColor()
self.lineCount = Int(self.frame.width / lineGap)
}
override internal func layoutSubviews() {
super.layoutSubviews()
self.setNeedsDisplay()
self.layoutIfNeeded()
}
override func drawRect(rect: CGRect) {
let ctx = UIGraphicsGetCurrentContext()
CGContextSaveGState(ctx)
for i in 0...lineCount {
let start = CGPoint(x: CGFloat(i) * lineGap, y: self.frame.height)
let end = CGPoint(x: CGFloat(i) * lineGap, y: self.frame.height - lineHeight)
drawLine(from: start, to: end, color: UIColor.grayColor())
if i % (lineCount / (dataPoints.count + 2)) == 0 && i != 0 && i != lineCount {
drawPoint(at: end, radius: 5, color: UIColor.orangeColor())
}
}
CGContextRestoreGState(ctx)
}
func drawLine(from start: CGPoint, to end: CGPoint, color: UIColor) {
let path = UIBezierPath()
path.moveToPoint(start)
path.addLineToPoint(end)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.CGPath
shapeLayer.strokeColor = color.CGColor
shapeLayer.lineWidth = 1
self.layer.addSublayer(shapeLayer)
}
func drawPoint(at center: CGPoint, radius: CGFloat, color: UIColor) {
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(0), endAngle: CGFloat(M_PI * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.CGPath
shapeLayer.fillColor = color.CGColor
shapeLayer.strokeColor = UIColor.blackColor().CGColor
shapeLayer.lineWidth = 0.5
self.layer.addSublayer(shapeLayer)
}
}
On my device and on the simulators, this is extremely slow and laggy. What exactly am I doing wrong here ? And what steps can I take to achieve a solid 60fps while scrolling ?
Hello I found your problem, your problem is
override internal func layoutSubviews() {
super.layoutSubviews()
self.setNeedsDisplay()
self.layoutIfNeeded()
}
you are accidentally in a never-ending paint loop
replace this by this
override internal func layoutSubviews() {
super.layoutSubviews()
}
or remove at all
I hope this Helps you, for me works great
It looks to me that you are drawing the same thing every time. Everything is static... So why not draw it all once into a graphics context and make a UIImage out of it to add to the background of the scrollview (or foreground, depending on anything else you are doing)?

Resources