iosswiftuikit

Main view's border in my custom UIview class is on top of another view


Screenshot of my issue

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)
    }
}

Solution

  • 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:

    wrong

    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:

    fixed


    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:

    final