I have an issue where the main view's border of a custom UIview class I have is going on top of the rankbadgeview within this custom class. I can't for the life of me figure out why. I've tried bringing the rankbadgeview subview to the front along with the uilabel inside of it and still doesn't work. Can anybody help me spot the issue?
class LeaderboardCircleView: UIView {
private let mainLabel = UILabel()
let rankBadgeView = UIView()
private let rankLabel = UILabel()
private let scoreLabel = UILabel()
init(mainText: String, rankText: String, backgroundColor: UIColor, score: Int) {
super.init(frame: .zero)
setupViews()
configure(mainText: mainText, rankText: rankText, backgroundColor: backgroundColor, score: score)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateScoreLabelWith(score: Int) {
let trophyAttachment = NSTextAttachment()
trophyAttachment.image = UIImage(systemName: "trophy.fill")?.withTintColor(.systemYellow, renderingMode: .alwaysOriginal)
trophyAttachment.bounds = CGRect(x: 0, y: -2, width: 16, height: 16)
let trophyString = NSAttributedString(attachment: trophyAttachment)
let scoreString = NSAttributedString(string: " \(score) pts", attributes: [
.font: AppStyle.shared.headerFont(size: 12),
.foregroundColor: UIColor.darkGray
])
let fullScoreText = NSMutableAttributedString()
fullScoreText.append(trophyString)
fullScoreText.append(scoreString)
scoreLabel.attributedText = fullScoreText
}
private func setupViews() {
layer.borderWidth = 2
layer.borderColor = AppStyle.shared.primaryColor?.cgColor
translatesAutoresizingMaskIntoConstraints = false
clipsToBounds = false
mainLabel.translatesAutoresizingMaskIntoConstraints = false
mainLabel.textAlignment = .center
mainLabel.adjustsFontSizeToFitWidth = true
mainLabel.minimumScaleFactor = 0.5
mainLabel.numberOfLines = 2
mainLabel.font = AppStyle.shared.headerFont(size: 18)
mainLabel.textColor = .white
addSubview(mainLabel)
rankBadgeView.translatesAutoresizingMaskIntoConstraints = false
rankBadgeView.clipsToBounds = true
rankBadgeView.backgroundColor = AppStyle.shared.primaryColor
rankBadgeView.layer.zPosition = 1 // Sends it behind the border
addSubview(rankBadgeView)
rankLabel.translatesAutoresizingMaskIntoConstraints = false
rankLabel.textAlignment = .center
rankLabel.font = AppStyle.shared.headerFont(size: 12)
rankLabel.textColor = .white
rankBadgeView.addSubview(rankLabel)
scoreLabel.translatesAutoresizingMaskIntoConstraints = false
scoreLabel.textAlignment = .center
scoreLabel.adjustsFontSizeToFitWidth = true
scoreLabel.minimumScaleFactor = 0.5
scoreLabel.numberOfLines = 1
addSubview(scoreLabel)
NSLayoutConstraint.activate([
mainLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
mainLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10),
mainLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
mainLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
rankBadgeView.centerXAnchor.constraint(equalTo: centerXAnchor),
rankBadgeView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 12),
rankBadgeView.widthAnchor.constraint(equalToConstant: 30),
rankBadgeView.heightAnchor.constraint(equalToConstant: 30),
rankLabel.centerXAnchor.constraint(equalTo: rankBadgeView.centerXAnchor),
rankLabel.centerYAnchor.constraint(equalTo: rankBadgeView.centerYAnchor),
scoreLabel.topAnchor.constraint(equalTo: rankBadgeView.bottomAnchor, constant: 4),
scoreLabel.centerXAnchor.constraint(equalTo: centerXAnchor)
])
bringSubviewToFront(rankBadgeView)
rankBadgeView.bringSubviewToFront(scoreLabel)
}
private func configure(mainText: String, rankText: String, backgroundColor: UIColor, score: Int) {
mainLabel.text = mainText
self.backgroundColor = backgroundColor
rankLabel.text = rankText
let trophyAttachment = NSTextAttachment()
trophyAttachment.image = UIImage(systemName: "trophy.fill")?.withTintColor(.systemYellow, renderingMode: .alwaysOriginal)
trophyAttachment.bounds = CGRect(x: 0, y: -2, width: 16, height: 16)
let trophyString = NSAttributedString(attachment: trophyAttachment)
let scoreString = NSAttributedString(string: " \(score) pts", attributes: [
.font: AppStyle.shared.headerFont(size: 12),
.foregroundColor: UIColor.darkGray
])
let fullScoreText = NSMutableAttributedString()
fullScoreText.append(trophyString)
fullScoreText.append(scoreString)
scoreLabel.attributedText = fullScoreText
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.width / 2
rankBadgeView.layer.cornerRadius = rankBadgeView.bounds.width / 2
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.2
layer.shadowRadius = 6.0
layer.shadowOffset = CGSize(width: 0, height: 4)
layer.masksToBounds = false
rankBadgeView.layer.shadowColor = UIColor.black.cgColor
rankBadgeView.layer.shadowOpacity = 0.2
rankBadgeView.layer.shadowRadius = 4.0
rankBadgeView.layer.shadowOffset = CGSize(width: 0, height: 2)
rankBadgeView.layer.masksToBounds = false
}
func update(mainText: String, rankText: String, backgroundColor: UIColor, score: Int) {
configure(mainText: mainText, rankText: rankText, backgroundColor: backgroundColor, score: score)
}
}
From Apple's docs: "The border ... is composited above the receiver’s contents and sublayers..."
So, if we add a subview to a view, and set the view's layer.border
properties like this:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let mainView = UIView(frame: .init(x: 40, y: 100, width: 120, height: 100))
let subView = UIView(frame: .init(x: 60, y: 60, width: 100, height: 60))
mainView.backgroundColor = .systemBlue
subView.backgroundColor = .systemYellow
view.addSubview(mainView)
mainView.addSubview(subView)
mainView.layer.borderColor = UIColor.red.cgColor
mainView.layer.borderWidth = 4
}
}
we get this result:
Instead, as matt
commented, we want to draw (using a CAShapeLayer
) the bordered shape:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let mainView = UIView(frame: .init(x: 40, y: 100, width: 120, height: 100))
let subView = UIView(frame: .init(x: 60, y: 60, width: 100, height: 60))
mainView.backgroundColor = .systemBlue
subView.backgroundColor = .systemYellow
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 4
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.fillColor = UIColor.systemBlue.cgColor
let path = UIBezierPath(rect: .init(x: 0, y: 0, width: 120, height: 100))
shapeLayer.path = path.cgPath
mainView.layer.addSublayer(shapeLayer)
view.addSubview(mainView)
mainView.addSubview(subView)
}
}
and we get this:
Here is your class with CAShapeLayer
modification - look for related comments beginning with // DonMag -
:
class AppStyle: NSObject {
static let shared = AppStyle()
let primaryColor = UIColor(red: 0.260, green: 0.346, blue: 0.579, alpha: 1.0)
func headerFont(size: CGFloat) -> UIFont {
return UIFont.systemFont(ofSize: size, weight: .bold)
}
}
class LeaderboardCircleView: UIView {
private let mainLabel = UILabel()
let rankBadgeView = UIView()
private let rankLabel = UILabel()
private let scoreLabel = UILabel()
// DonMag - this shape layer will draw the main bordered-circle
private let mainCircleLayer: CAShapeLayer = CAShapeLayer()
init(mainText: String, rankText: String, backgroundColor: UIColor, score: Int) {
super.init(frame: .zero)
setupViews()
configure(mainText: mainText, rankText: rankText, backgroundColor: backgroundColor, score: score)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateScoreLabelWith(score: Int) {
let trophyAttachment = NSTextAttachment()
trophyAttachment.image = UIImage(systemName: "trophy.fill")?.withTintColor(.systemYellow, renderingMode: .alwaysOriginal)
trophyAttachment.bounds = CGRect(x: 0, y: -2, width: 16, height: 16)
let trophyString = NSAttributedString(attachment: trophyAttachment)
let scoreString = NSAttributedString(string: " \(score) pts", attributes: [
.font: AppStyle.shared.headerFont(size: 12),
.foregroundColor: UIColor.darkGray
])
let fullScoreText = NSMutableAttributedString()
fullScoreText.append(trophyString)
fullScoreText.append(scoreString)
scoreLabel.attributedText = fullScoreText
}
private func setupViews() {
// DonMag - don't set self's layer border properties
//layer.borderWidth = 2
//layer.borderColor = UIColor.red.cgColor // AppStyle.shared.primaryColor?.cgColor
// DonMag - main circle properties
mainCircleLayer.lineWidth = 4
mainCircleLayer.strokeColor = AppStyle.shared.primaryColor.cgColor
// DonMag - add circle shape layer
layer.addSublayer(mainCircleLayer)
translatesAutoresizingMaskIntoConstraints = false
clipsToBounds = false
mainLabel.translatesAutoresizingMaskIntoConstraints = false
mainLabel.textAlignment = .center
mainLabel.adjustsFontSizeToFitWidth = true
mainLabel.minimumScaleFactor = 0.5
mainLabel.numberOfLines = 2
mainLabel.font = AppStyle.shared.headerFont(size: 18)
mainLabel.textColor = .white
addSubview(mainLabel)
rankBadgeView.translatesAutoresizingMaskIntoConstraints = false
rankBadgeView.clipsToBounds = true
rankBadgeView.backgroundColor = AppStyle.shared.primaryColor
// DonMag - not needed
//rankBadgeView.layer.zPosition = 1 // Sends it behind the border
addSubview(rankBadgeView)
rankLabel.translatesAutoresizingMaskIntoConstraints = false
rankLabel.textAlignment = .center
rankLabel.font = AppStyle.shared.headerFont(size: 12)
rankLabel.textColor = .white
rankBadgeView.addSubview(rankLabel)
scoreLabel.translatesAutoresizingMaskIntoConstraints = false
scoreLabel.textAlignment = .center
scoreLabel.adjustsFontSizeToFitWidth = true
scoreLabel.minimumScaleFactor = 0.5
scoreLabel.numberOfLines = 1
addSubview(scoreLabel)
NSLayoutConstraint.activate([
mainLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
mainLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10),
mainLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
mainLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
rankBadgeView.centerXAnchor.constraint(equalTo: centerXAnchor),
rankBadgeView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 12),
rankBadgeView.widthAnchor.constraint(equalToConstant: 30),
rankBadgeView.heightAnchor.constraint(equalToConstant: 30),
rankLabel.centerXAnchor.constraint(equalTo: rankBadgeView.centerXAnchor),
rankLabel.centerYAnchor.constraint(equalTo: rankBadgeView.centerYAnchor),
scoreLabel.topAnchor.constraint(equalTo: rankBadgeView.bottomAnchor, constant: 4),
scoreLabel.centerXAnchor.constraint(equalTo: centerXAnchor)
])
// DonMag - not needed
//bringSubviewToFront(rankBadgeView)
//rankBadgeView.bringSubviewToFront(scoreLabel)
}
private func configure(mainText: String, rankText: String, backgroundColor: UIColor, score: Int) {
mainLabel.text = mainText
// DonMag - set self background to clear
self.backgroundColor = .clear
// DonMag - main circle layer fill color
mainCircleLayer.fillColor = backgroundColor.cgColor
rankLabel.text = rankText
let trophyAttachment = NSTextAttachment()
trophyAttachment.image = UIImage(systemName: "trophy.fill")?.withTintColor(.systemYellow, renderingMode: .alwaysOriginal)
trophyAttachment.bounds = CGRect(x: 0, y: -2, width: 16, height: 16)
let trophyString = NSAttributedString(attachment: trophyAttachment)
let scoreString = NSAttributedString(string: " \(score) pts", attributes: [
.font: AppStyle.shared.headerFont(size: 12),
.foregroundColor: UIColor.darkGray
])
let fullScoreText = NSMutableAttributedString()
fullScoreText.append(trophyString)
fullScoreText.append(scoreString)
scoreLabel.attributedText = fullScoreText
}
override func layoutSubviews() {
super.layoutSubviews()
// DonMag - don't set self's corner radius
//layer.cornerRadius = bounds.width / 2
// DonMag - oval (circle) path for main circle
mainCircleLayer.path = UIBezierPath(ovalIn: bounds).cgPath
rankBadgeView.layer.cornerRadius = rankBadgeView.bounds.width / 2
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.2
layer.shadowRadius = 6.0
layer.shadowOffset = CGSize(width: 0, height: 4)
layer.masksToBounds = false
rankBadgeView.layer.shadowColor = UIColor.black.cgColor
rankBadgeView.layer.shadowOpacity = 0.2
rankBadgeView.layer.shadowRadius = 4.0
rankBadgeView.layer.shadowOffset = CGSize(width: 0, height: 2)
rankBadgeView.layer.masksToBounds = false
}
func update(mainText: String, rankText: String, backgroundColor: UIColor, score: Int) {
configure(mainText: mainText, rankText: rankText, backgroundColor: backgroundColor, score: score)
}
}
and now we get the desired result, without the overlapping border: