Search code examples
iosswiftinterface-builderxibcashapelayer

CAShapeLayer not visible in XIB swift iOS


I'm building a custom view using an XIB file. However, I am facing a problem where the layers I add to the view (trackLayer) are not shown on the xib (circleLayer is an animation and I don't expect it to render in xib which is not possible to my knowledge). The code of the owner class for the XIB is shown as follows:

@IBDesignable
class SpinningView: UIView {

@IBOutlet var contentView: SpinningView!

//MARK: Properties
let circleLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()

let circularAnimation: CABasicAnimation = {
    let animation = CABasicAnimation()
    animation.fromValue = 0
    animation.toValue = 1
    animation.duration = 2
    animation.fillMode = kCAFillModeForwards
    animation.isRemovedOnCompletion = false
    return animation
}()

//MARK: IB Inspectables
@IBInspectable var strokeColor: UIColor = UIColor.red {
    ...
}

@IBInspectable var trackColor: UIColor = UIColor.lightGray {
    ...
}

@IBInspectable var lineWidth: CGFloat = 5 {
    ...
}

@IBInspectable var fillColor: UIColor = UIColor.clear {
    ...
}

@IBInspectable var isAnimated: Bool = true {
    ...
}

//MARK: Initialization
override init(frame: CGRect) {
    super.init(frame: frame)
    initSubviews()
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)

    setup()
    initSubviews()
}

//MARK: Private functions
private func setup() {
    setupTrack()
    setupAnimationOnTrack()
}

private func setupTrack(){
    trackLayer.lineWidth = lineWidth
    trackLayer.fillColor = fillColor.cgColor
    trackLayer.strokeColor = trackColor.cgColor
    layer.addSublayer(trackLayer)
}

private func setupAnimationOnTrack(){
    circleLayer.lineWidth = lineWidth
    circleLayer.fillColor = fillColor.cgColor
    circleLayer.strokeColor = strokeColor.cgColor
    circleLayer.lineCap = kCALineCapRound
    layer.addSublayer(circleLayer)
    updateAnimation()
}

private func updateAnimation() {
    if isAnimated {
        circleLayer.add(circularAnimation, forKey: "strokeEnd")
    }
    else {
        circleLayer.removeAnimation(forKey: "strokeEnd")
    }
}

//MARK: Layout contraints
override func layoutSubviews() {
    super.layoutSubviews()

    let center = CGPoint(x: bounds.midX, y: bounds.midY)
    let radius = min(bounds.width / 2, bounds.height / 2) - circleLayer.lineWidth / 2

    let startAngle = -CGFloat.pi / 2
    let endAngle = 3 * CGFloat.pi / 2
    let path = UIBezierPath(arcCenter: .zero, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)

    trackLayer.position = center
    trackLayer.path = path.cgPath

    circleLayer.position = center
    circleLayer.path = path.cgPath
}

private func initSubviews() {
    let bundle = Bundle(for: SpinningView.self)
    let nib = UINib(nibName: "SpinningView", bundle: bundle)
    nib.instantiate(withOwner: self, options: nil)
    contentView.frame = bounds
    addSubview(contentView)
}
}

When a view is subclassed in the Main.storyboard, I can see the image and the tracklayer as follows IB UIView but when I go over to the XIB, it does not have the trackLayer(circle around the image) XIB Image. While one can argue that it is working and why I am bothering with this, I think it is important that I design the XIB properly since another person might just see it as an view with only an image (no idea of the animating feature)


Solution

  • When you are designing your XIB, it is not an instance of your @IBDesignable SpinningView. You are, effectively, looking at the source of your SpinningView.

    So, the code behind it is not running at that point - it only runs when you add an instance of SpinningView to another view.

    Take a look at this simple example:

    @IBDesignable
    class SpinningView: UIView {
    
        @IBOutlet var contentView: UIView!
        @IBOutlet var theLabel: UILabel!
    
        //MARK: Initialization
        override init(frame: CGRect) {
            super.init(frame: frame)
            initSubviews()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            initSubviews()
        }
    
        private func initSubviews() {
            let bundle = Bundle(for: SpinningView.self)
            let nib = UINib(nibName: "SpinningView", bundle: bundle)
            nib.instantiate(withOwner: self, options: nil)
            contentView.frame = bounds
            addSubview(contentView)
    
            // this will change the label's textColor in Storyboard
            // when a UIView's class is set to SpinningView
            theLabel.textColor = .red
        }
    
    }
    

    When I am designing the XIB, this is what I see:

    enter image description here

    But when I add a UIView to another view, and assign its class as SpinningView, this is what I see:

    enter image description here

    The label's text color get's changed to red, because the code is now running.

    Another way to think about it... you have trackColor, fillColor, etc vars defined as @IBInspectable. When you're designing your XIB, you don't see those properties in Xcode's Attributes Inspector panel - you only see them when you've selected an instance of SpinningView that has been added elsewhere.

    Hope that makes sense.