uguboz uguboz - 2 years ago 99
iOS Question

Save and Redraw in a loop Swift 4

I am trying to draw gauge ticks as in the below image in a loop.

Gauge Ticks

I followed the below steps:

1. create a CAShapeLayer and CALayer
2. for loop
3. draw center top line with UIBezier
4. rotate the layer in the loop
5. add CAShapeLayer to the CALayer with .addSubLayer
5. end loop


Code

let angle = CGFloat(Double.pi / 180 * 10)

turnLayer.frame = self.bounds
self.layer.addSublayer(turnLayer)
let strokeColor = UIColor.black.cgColor
thickLayer.frame = frame
thickLayer.strokeColor = strokeColor
thickLayer.fillColor = UIColor.clear.cgColor
thickLayer.lineWidth = 8.0


for idx in -10...10 {
var p: CGPoint = CGPoint.init(x: bounds.width/2, y: bounds.height/8)
let path = UIBezierPath()
path.move(to: p)
p = CGPoint.init(x: bounds.width/2, y: bounds.height/8 + 32)
path.addLine(to: p)
thickLayer.path = path.cgPath
thickLayer.setAffineTransform(CGAffineTransform(rotationAngle: angle * CGFloat(idx)))
turnLayer.addSublayer(thickLayer)

}


Problem

I am lost about how to save and redraw the center top line

Rob Rob
Answer Source

Rather than adding a bunch of rotated layers, it might be easier to have a single CAShapeLayer with the strokes for all the ticks, e.g.,

let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 3
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.lightGray.cgColor
view.layer.addSublayer(shapeLayer)

let maxRadius = min(view.bounds.width, view.bounds.height) / 2
let center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)

let path = UIBezierPath()

for i in 0 ... 16 {
    let angle = -(CGFloat.pi / 2.0) + CGFloat.pi / 2.0 * (CGFloat(i - 8)) / 9.0
    let outerRadius = maxRadius
    let innerRadius = maxRadius - (i % 2 == 0 ? 20 : 10)
    path.move(to: point(angle: angle, from: center, radius: outerRadius))
    path.addLine(to: point(angle: angle, from: center, radius: innerRadius))
}

shapeLayer.path = path.cgPath

Where

func point(angle: CGFloat, from center: CGPoint, radius: CGFloat) -> CGPoint {
    return CGPoint(x: center.x + radius * cos(angle), y: center.y + radius * sin(angle))
}

That yields:

enter image description here

Obviously, tweak the angle, innerRadius and outerRadius to suit your purposes, but this illustrates an easier way to render all of those tick marks.


If comments, you seemed to object to the above approach, and effectively asked if it is possible to save a snapshot of a view (presumably so you can later show this image in an image view). Yes, it is:

func snapshot(of view: UIView, afterScreenUpdates: Bool = false) -> UIImage {
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, false, 0)
    view.drawHierarchy(in: view.bounds, afterScreenUpdates: afterScreenUpdates)
    let image = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()

    return image
}

So, following up with your question of "can I add the shapelayer and save a snapshot", the answer is yes, you can

let shapeLayer = ...
view.layer.addSublayer(shapeLayer)
let image = snapshot(of: view, afterScreenUpdates: true)

Then, if you wanted to use that snapshot, you'd have to remove the CAShapeLayer and then show the snapshot in some image view:

shapeLayer.removeFromSuperlayer()
imageView.image = image

Obviously, that's an incredibly inefficient approach (particularly because you have to use afterScreenUpdates value of true if you want recently added content to be included in the snapshot).

If you really wanted to efficiently create an image of all the ticks, you'd probably just stroke the UIBezierPath directly to an image context. All you'd need to know was the size of the UIImage to create:

func tickImage(size: CGSize) -> UIImage {
    UIGraphicsBeginImageContextWithOptions(size, false, 0)

    let path = UIBezierPath()
    path.lineWidth = 3

    let maxRadius = size.width / 2
    let center = CGPoint(x: size.width / 2, y: size.height)

    for i in 0 ... 16 {
        let angle = -(CGFloat.pi / 2.0) + CGFloat.pi / 2.0 * (CGFloat(i - 8)) / 9.0
        let outerRadius = maxRadius
        let innerRadius = maxRadius - (i % 2 == 0 ? 20 : 10)
        path.move(to: point(angle: angle, from: center, radius: outerRadius))
        path.addLine(to: point(angle: angle, from: center, radius: innerRadius))
    }

    UIColor.lightGray.setStroke()
    path.stroke()

    let image = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()

    return image
}

That having been said, I'm not sure why you'd do that instead of the CAShapeLayer in the beginning of this answer. I'd only do this "create UIImage" approach if I needed to save/upload this snapshot for use at some later date. Otherwise CAShapeLayer is a fine approach.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download