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.
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 CAShapeLayer
s, 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...
after tapping "Add Story" 5 times:
then tapping "Add Viewed" 2 times:
and after adding a few more stories and a few more views: