2
votes

We have a custom UIView that uses constraints in Interface Builder and we have modified this view to have a custom shape which we need to draw.

The shape size depends on the constraints which are applied to this custom view in Interface Builder, so in order to get the shape in the custom UIView at the right size, we need to call it in LayoutSubviews.

The issue is when LayoutSubviews is getting called during screen orientation, another shape is been added to the screen since it is called in the custom UIView's setup function.

This behavior can be solved by adding a boolean that determines if this is the first time LayoutSubviews is called and only if it is, then adds the shape layer to the view.

I have made some research here and on the web and found an article on how to achieve the described behavior with a different approach.

https://thomasclowes.com/ios-when-to-layout-your-views/

The author mentioned a custom class BaseView and a protocol which can be implemented to solve this behavior.

The solution is written under "UIView and layoutSubviews".

I am struggling to understand the solution and wanted to know if someone can clarify and show a code example on how to achieve the solution mentioned in the article.

Thanks in advance.

class CustomView: UIView {

    private var firstTimeInit = true

    override func layoutSubviews() {
        super.layoutSubviews()

        setup()
    }

    private func setup() {

        // Check if the the method was already executed
        if firstTimeInit == false { return }

        let label = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 21))
        label.font = UIFont.systemFont(ofSize: 13, weight: .bold)
        label.text = "Daily"
        label.textAlignment = .center
        label.sizeToFit()
        addSubview(label)
        label.center.x = bounds.midX

        let shapePath = UIBezierPath()
        shapePath.move(to: CGPoint(x: 0, y: 0))
        shapePath.addLine(to: CGPoint(x: frame.width, y: 0))

        let shapeLayer = CAShapeLayer()
        shapeLayer.path = shapePath.cgPath
        shapeLayer.strokeColor = UIColor(hex: 0xD5E5EF).cgColor
        shapeLayer.lineWidth = 3
        shapeLayer.lineCap = .round
        shapeLayer.position.y = frame.height / 2
        shapeLayer.lineDashPattern = [frame.width / 2.5, frame.width / 5, frame.width / 2.5] as [NSNumber]
        layer.addSublayer(shapeLayer)

        firstTimeInit = false
    }
}
2

2 Answers

2
votes

Why don't you create your UILabel and CAShapeLayer once, add it to subview in constructor and then only use layoutSubviews to recalculate their frame or pattern?

Like this:

class CustomView: UIView {

    private let label = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 21))
    private let shapeLayer = CAShapeLayer()

    override init(frame: CGRect) {
        super.init(frame: frame)

        label.font = UIFont.systemFont(ofSize: 13, weight: .bold)
        label.text = "Daily"
        label.textAlignment = .center
        addSubview(label)

        shapeLayer.strokeColor = UIColor(hex: 0xD5E5EF).cgColor
        shapeLayer.lineWidth = 3
        shapeLayer.lineCap = .round
        layer.addSublayer(shapeLayer)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        label.sizeToFit()
        label.center.x = bounds.midX

        let shapePath = UIBezierPath()
        shapePath.move(to: CGPoint(x: 0, y: 0))
        shapePath.addLine(to: CGPoint(x: frame.width, y: 0))

        shapeLayer.path = shapePath.cgPath
        shapeLayer.position.y = frame.height / 2
        shapeLayer.lineDashPattern = [frame.width / 2.5, frame.width / 5, frame.width / 2.5] as [NSNumber]
    }
}

This way your custom shape would adapt to frame change, without duplicating itself.

Obviously, you may need to actually implement required init?(coder aDecoder: NSCoder) if you use it.

0
votes

One way you can do is by using didSet method in your custom view. for example

class CustomView: UIView {

    var firstTimeInit : Bool = false {
        didSet {
            layoutSubviews()
        }
    }


    override func layoutSubviews() {
        super.layoutSubviews()
        // i am only changing the color to check if the layoutSubview is called. 
        // i have a viewController in storyboard with a UIView Background Color set to gray. 
        backgroundColor = UIColor.red
    }

}

To use make sure your didSet method is called in the custom view you can use it in the viewdidLoad function of the view controller you will be loading the custom view in.

class ViewController: UIViewController {

    let customView = CustomView()

    override func viewDidLoad() {
        super.viewDidLoad()
        customView.viewDidSet = true
    }
}