skip to Main Content

Please, help! How can I make a transparent outline for circles like this (
so that the background shows through)?
I tried using a mask, but this code does not work..
enter image description here

Perhaps there are some other solutions?

My code:

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let backgroundImage = UIImageView(frame: UIScreen.main.bounds)
        backgroundImage.image = UIImage(named: "background")
        backgroundImage.contentMode = .scaleAspectFill
        view.addSubview(backgroundImage)
        view.sendSubviewToBack(backgroundImage)

        // Параметры для кругов
        let circleSize: CGFloat = 100 
        let borderWidth: CGFloat = 4 
        let circleSpacing: CGFloat = 30 

        var previousCircleView: UIImageView?

        for i in 0..<3 {
            let circleImageView = UIImageView()
            circleImageView.image = UIImage(named: "ggg")
            circleImageView.contentMode = .scaleAspectFill
            view.addSubview(circleImageView)

            let maskLayer = CAShapeLayer()
            let circlePath = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: circleSize, height: circleSize))
            let borderPath = UIBezierPath(ovalIn: CGRect(x: -borderWidth, y: -borderWidth, width: circleSize + 2 * borderWidth, height: circleSize + 2 * borderWidth))
            borderPath.append(circlePath)
            borderPath.usesEvenOddFillRule = true

            maskLayer.path = borderPath.cgPath
            maskLayer.fillRule = .evenOdd
            circleImageView.layer.mask = maskLayer

            circleImageView.snp.makeConstraints { make in
                make.width.height.equalTo(circleSize + 2 * borderWidth)
                make.centerY.equalToSuperview()

                if let previous = previousCircleView {
                    make.left.equalTo(previous.snp.right).offset(-(circleSize - circleSpacing + borderWidth))
                } else {
                    make.left.equalToSuperview().offset(20)
                }
            }

            previousCircleView = circleImageView
        }
    }
}

2

Answers


  1. If your goal is a series of crescents, ending in a full circle, creating a shape layer and a mask will involve drawing part-circle arcs, and some trig.

    You could instead do this by creating an image view with the contents you want. In that case, you could write code that runs in a loop, drawing a circle in an opaque color, then switching to clear mode and erasing the outline of the circle. Each new circle will erase part of the previous circle, but the last circle won’t be clipped. That’s what your sample image shows.

    Here is the code I wrote to create that image, loosely based on your starting code:

    import UIKit
    
    class ViewController: UIViewController {
        
        var imageView = UIImageView(frame: CGRectZero)
        
        
        // Install an image in imageView that contains a series of crescent shapes, ending in a full circle.
        private func setShades(count: Int) {
            var image = UIImage()
            let lineWidth = 7.0
            let circleDiameter = 50.0
            let width = CGFloat(count+1)/2.0 * circleDiameter
            let bounds = CGRect(origin: CGPointZero, size: CGSize(width: width, height: circleDiameter))
            let circleColor =  UIColor(_colorLiteralRed: 161.0/255, green: 62.0/255, blue: 3.0/255, alpha: 1.0)
            let renderer = UIGraphicsImageRenderer(size: bounds.size)
            image = renderer.image { context in
                for index in 0..<count {
                    let x = CGFloat(index) * circleDiameter / 2
                    let circleRect = CGRect(
                        x: x, y: 0,
                        width: circleDiameter, height: circleDiameter)
                    let circle = UIBezierPath.init(ovalIn: circleRect)
                    circle.lineWidth = lineWidth
                    circleColor.setFill()
                    context.cgContext.setBlendMode(.normal)
                    
                    //Fill the circle with our circle color
                    circle.fill()
                    
                    //Switch the drawing mode to .clear, so we erase anything we draw
                    context.cgContext.setBlendMode(.clear)
                    
                    // Erase the outline of this circle
                    circle.stroke()
                }
            }
            // Place the image view near the bottom of the content view, and on the right side.
            let frame = CGRect(x: view.bounds.maxX - bounds.width, y: view.bounds.maxY - 100 - bounds.height, width: bounds.width, height: bounds.height)
            imageView.frame = frame
            imageView.image = image
        }
    
        override func viewDidLayoutSubviews() {
            // You need to create teh image view when the view changes its subviews
            //so that it is placed correctly (e.g. after device rotation)
            setShades(count: 4)
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
    
            let backgroundImage = UIImageView(frame: UIScreen.main.bounds)
            backgroundImage.image = UIImage(named: "background")
            backgroundImage.contentMode = .scaleAspectFill
            view.addSubview(backgroundImage)
            view.addSubview(imageView)
        }
    }
    

    Here is a sample project on Github that creates the image below:

    enter image description here

    Login or Signup to reply.
  2. Each of those shapes is composed of two arcs. The only trick, requiring a little trigonometry, is to calculate the start and end angle for each. E.g., you can do something like:

    func addSublayers(to view: UIView) {
        let radius: CGFloat = 150
        let borderWidth: CGFloat = 10
        let circleCenterOffset: CGFloat = 120
        let startCenter = CGPoint(x: 200, y: 200)
    
        for i in 0..<3 {
            let shapeLayer = createShapeLayer()
            shapeLayer.path = circleDifference(
                center: point(startCenter, xOffset: CGFloat(i) * circleCenterOffset),
                radius: radius,
                offset: circleCenterOffset,
                width: borderWidth
            ).cgPath
            view.layer.addSublayer(shapeLayer)
        }
    
        let shapeLayer = createShapeLayer()
        shapeLayer.path = UIBezierPath(arcCenter: point(startCenter, xOffset: 3 * circleCenterOffset), radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
        view.layer.addSublayer(shapeLayer)
    }
    
    private func point(_ point: CGPoint, xOffset: CGFloat) -> CGPoint {
        CGPoint(x: point.x + xOffset, y: point.y)
    }
    
    private func createShapeLayer() -> CAShapeLayer {
        let shapeLayer = CAShapeLayer()
        shapeLayer.strokeColor = UIColor.clear.cgColor
        shapeLayer.fillColor = UIColor.red.cgColor
        return shapeLayer
    }
    
    private func circleDifference(center: CGPoint, radius: CGFloat, offset: CGFloat, width: CGFloat) -> UIBezierPath {
        // Heron’s Formula
        let r1 = radius
        let r2 = radius + width
        let s = (r1 + r2 + offset) / 2                        // semiperimeter
        let a = sqrt(s * (s - r1) * (s - r2) * (s - offset))  // area
        let h = 2 * a / offset
    
        // angles
        let angle1 = asin(h / r1)
        let angle2 = asin(h / r2)
    
        // path
        let path = UIBezierPath()
        path.addArc(
            withCenter: center,
            radius: r1,
            startAngle: angle1,
            endAngle: 2 * .pi - angle1,
            clockwise: true
        )
        path.addArc(
            withCenter: point(center, xOffset: offset),
            radius: r2,
            startAngle: 3 * .pi / 2 - (.pi / 2 - angle2),
            endAngle: .pi / 2 + (.pi / 2 - angle2),
            clockwise: false
        )
        path.close()
        return path
    }
    

    Resulting in:

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search