1

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)

jms
  • 747
  • 6
  • 19

1 Answers1

0

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.

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • wow...thanks. That seems to make much sense since I experienced similar phenomena. My knowledge on the caveats of IB seem to be lacking (honestly I started working with xib quite recently). So, is it correct as a programmer to leave an xib that on the outside only gives a partial representation of what I actually want to achieve. Like for example when another programmer takes on my project, and since XIBs are supposed to be reusable, he/she might be confused when looking at my component unless he/she dives into the code – jms Mar 04 '18 at 13:09
  • Hmmm... hard to say. I would think that, generally, one wouldn't use a XIB to develop an `@IBDesignable` component... *particularly* when the "meat" of the component can only exist via code execution. Unless your `SpinningView` is considerably more complex than what you've shown, I think I'd go for code-only. I suppose you *could* add a bordered, clear, round view that would be visible in your XIB, and then remove it (via code) when your XIB is added to a storyboard... but that might end up being more complicated and misleading anyway. – DonMag Mar 05 '18 at 13:52