Home > other >  Create border to UIImageView with sliced
Create border to UIImageView with sliced

Time:07-12

I am creating view like WhatsApp status, where I am getting stories uploaded by user in array.

I want to show border to user profile image with number of array count of storied. something like this in image below.

I want to divide the border of UIImageview as per the array count and when user views the story change the colour of border.

For example, user added 5 stories, so I wanted to show border with 5 slices and if user viewed 2 stories out of 5 then these two slices of border will be changed to gray colour and remaining 3 will be as green colour.

Any help to get this thing done.

thanks in advance.

enter image description here

I tried this, but its not giving me expected result

func setImageViewWithBorder() {
        
        self.img_userProfile.backgroundColor = .clear
        self.img_userProfile.clipsToBounds = true
        
        let maskLayer = CAShapeLayer()
        maskLayer.frame = img_userProfile.frame
        maskLayer.path = UIBezierPath(rect: self.img_userProfile.frame).cgPath
        self.img_userProfile.layer.mask = maskLayer
        
        
        let line = NSNumber(value: Float(self.img_userProfile.frame.width / 2))
        
        let borderLayer = CAShapeLayer()
        borderLayer.path = maskLayer.path
        borderLayer.fillColor = UIColor.clear.cgColor
        borderLayer.strokeColor = UIColor.green.cgColor
        borderLayer.lineDashPattern = [line]
        borderLayer.lineDashPhase = self.img_userProfile.frame.width / 4
        borderLayer.lineWidth = 10
        borderLayer.frame = self.img_userProfile.frame
        self.img_userProfile.layer.addSublayer(borderLayer)
    }

CodePudding user response:

You almost certainly want to use a couple CAShapeLayers, using arcs to create the segments (rather than trying to manipulate the dash pattern):

  • One layer with stroke set to gray
  • One layer with stroke set to green
  • define a "gap" degrees value... I'd start with 10 degrees
  • calculate the arc segment degrees by subtracting the number of gaps from 360 degrees, and dividing that by the number of arcs
  • use a UIBezierPath to draw the "viewed" arcs
  • use a UIBezierPath to draw the "not-viewed" arcs

Here's a quick example...

First, since we generally think in terms of degrees, but bezier arcs use radians, we'll use this extension:

extension FloatingPoint {
    var degreesToRadians: Self { self * .pi / 180 }
    var radiansToDegrees: Self { self * 180 / .pi }
}

Then, a sample UIView subclass:

class DashedCircleView: UIView {
    
    var storyCount: Int = 0 {
        didSet { setNeedsLayout() }
    }
    var viewedCount: Int = 0 {
        didSet { setNeedsLayout() }
    }
    
    public var storyColor: UIColor = .systemGreen {
        didSet {
            colorLayer.strokeColor = storyColor.cgColor
        }
    }
    public var viewedColor: UIColor = .gray {
        didSet {
            colorLayer.strokeColor = viewedColor.cgColor
        }
    }

    private let grayLayer = CAShapeLayer()
    private let colorLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {
        layer.addSublayer(grayLayer)
        layer.addSublayer(colorLayer)
        grayLayer.strokeColor = viewedColor.cgColor
        colorLayer.strokeColor = storyColor.cgColor
        grayLayer.fillColor = UIColor.clear.cgColor
        colorLayer.fillColor = UIColor.clear.cgColor
        grayLayer.lineWidth = 5
        colorLayer.lineWidth = 5
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        let cPT: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)

        if storyCount == 0 {
            
            // clear both shape layer paths
            grayLayer.path = nil
            colorLayer.path = nil
            
        } else if storyCount == 1 {
            
            // complete 360 degee arc
            //  so no spaces
            let bez = UIBezierPath()
            let a1: Double = -90
            let a2: Double = 270
            
            bez.addArc(withCenter: cPT, radius: bounds.midX, startAngle: a1.degreesToRadians, endAngle: a2.degreesToRadians, clockwise: true)
            
            if viewedCount == 1 {
                grayLayer.path = bez.cgPath
                colorLayer.path = nil
            } else {
                colorLayer.path = bez.cgPath
                grayLayer.path = nil
            }
        } else {
            
            let grayBez = UIBezierPath()
            let colorBez = UIBezierPath()
            let trackBez = UIBezierPath()
            
            // space between arcs
            let gapDegrees: Double = 10
            
            let availableDegrees: Double = 360 - Double(storyCount) * gapDegrees
            let arcDegrees: Double = availableDegrees / Double(storyCount)
            
            var a1: Double = -90

            for i in 0..<storyCount {
                if i < viewedCount {
                    grayBez.addArc(withCenter: cPT, radius: bounds.midX, startAngle: a1.degreesToRadians, endAngle: a1.degreesToRadians   arcDegrees.degreesToRadians, clockwise: true)
                } else {
                    colorBez.addArc(withCenter: cPT, radius: bounds.midX, startAngle: a1.degreesToRadians, endAngle: a1.degreesToRadians   arcDegrees.degreesToRadians, clockwise: true)
                }
                
                // to provide a space (or gap) between arcs, we need to
                //  move to the start of the next arc
                //  a "cheap" way to do this is to use a "tracking" bezier path
                //  and add an arc including the gap degrees
                //  that will give us the "moveTo" point
                trackBez.addArc(withCenter: cPT, radius: bounds.midX, startAngle: a1.degreesToRadians, endAngle: (a1   arcDegrees   gapDegrees).degreesToRadians, clockwise: true)
                grayBez.move(to: trackBez.currentPoint)
                colorBez.move(to: trackBez.currentPoint)

                // increment the arc starting degrees
                a1  = arcDegrees   gapDegrees
            }
            
            colorLayer.path = colorBez.cgPath
            grayLayer.path = grayBez.cgPath
            
        }
        
        grayLayer.lineCap = storyCount > 1 ? .round : .butt
        colorLayer.lineCap = storyCount > 1 ? .round : .butt
        
    }
    
}

and a sample view controller to demonstrate:

class CircleTestVC: UIViewController {
    
    var numStories: Int = 0
    var numViewed: Int = 0
    
    let cView = DashedCircleView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        cView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(cView)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            cView.widthAnchor.constraint(equalToConstant: 160.0),
            cView.heightAnchor.constraint(equalTo: cView.widthAnchor),
            cView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            cView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
        ])

        cView.storyCount = numStories
        cView.viewedCount = numViewed
        
        // let's add a few buttons
        let stackView = UIStackView()
        stackView.spacing = 8
        stackView.distribution = .fillEqually
        ["Add Story", "Add Viewed", "Reset"].forEach { s in
            let b = UIButton()
            b.backgroundColor = .systemRed
            b.setTitle(s, for: [])
            b.setTitleColor(.white, for: .normal)
            b.setTitleColor(.lightGray, for: .highlighted)
            b.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
            stackView.addArrangedSubview(b)
        }
        
        stackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stackView)
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
        ])

    }
    
    @objc func btnTapped(_ sender: UIButton) {
        guard let t = sender.currentTitle else { return }
        switch t {
        case "Add Story":
            addStory()
        case "Add Viewed":
            addViewed()
        default:
            reset()
        }
    }
    
    @objc func addStory() {
        numStories  = 1
        cView.storyCount = numStories
    }
    @objc func addViewed() {
        numViewed  = 1
        numViewed = min(numViewed, numStories)
        cView.viewedCount = numViewed
    }
    @objc func reset() {
        numStories = 0
        numViewed = 0
        cView.storyCount = numStories
        cView.viewedCount = numViewed
    }
}

When we run it, it will look like this...

enter image description here

after tapping "Add Story" 5 times:

enter image description here

then tapping "Add Viewed" 2 times:

enter image description here

and after adding a few more stories and a few more views:

enter image description here

  • Related