简体   繁体   中英

How to create an IBDesignable custom UIView from a UIBezierPath?

I want to shape a UIView and be able to see its shape in the Interface Builder, when I set the following class to my UIView it fails to build, I'm not sure where's the error.

@IBDesignable
class CircleExampleView: UIView {

    override func layoutSubviews() {
        setupMask()
    }

    func setupMask() {

        let path = makePath()

        // mask the whole view to that shape
        let mask = CAShapeLayer()
        mask.path = path.cgPath
        self.layer.mask = mask
    }

    private func makePath() -> UIBezierPath {

        //// Oval Drawing
        let ovalPath = UIBezierPath(ovalIn: CGRect(x: 11, y: 12, width: 30, height: 30))
        UIColor.gray.setFill()
        ovalPath.fill()

        return ovalPath
    }
}

A couple of observations:

  1. You are calling setFill followed by fill , but you only do that if drawing into a graphics context (eg, in draw(_:) ). When using CAShapeLayer , you should instead set the fillColor of the CAShapeLayer .

  2. You are using the CAShapeLayer to set the mask of your view. If your UIView doesn't have a discernible backgroundColor , you won't see anything.

    If you set the background color of the view to, say, blue, as shown below, your mask will reveal that blue background wherever the mask allows it to (in the oval of your path).

  3. You have implemented layoutSubviews . You generally would do that only if you were doing something here that was contingent upon the bounds of the view. For example, here's a rendition where the oval path is based upon the bounds of the view:

     @IBDesignable class CircleView: UIView { override func layoutSubviews() { super.layoutSubviews() setupMask() } private func setupMask() { let mask = CAShapeLayer() mask.path = path.cgPath mask.fillColor = UIColor.gray.cgColor layer.mask = mask } private var path: UIBezierPath { return UIBezierPath(ovalIn: bounds) } }
  4. As E. Coms said, if you override layoutSubviews , you really should call the super implementation. This isn't critical, as the default implementation actually does nothing, but it's best practice. Eg if you later changed this class to subclass some other UIView subclass, you don't want to have to go to revisit all these overrides.

  5. If you have a designable view, it's advisable to put that in a separate target. That way, the rendering of the view in the storyboard is not dependent upon any work that may be underway in the main project. As long as the designables target (often the name of your main target with Kit suffix) can build, the designable view can be rendered.

For example, here is a rendition of your designable view, in a separate framework target, and used in a storyboard where the view in question has a blue backgroundColor :

在此处输入图片说明


For what it's worth, I think it's exceedingly confusing to have to mask to reveal the background color inside the oval. An app developer has to set "background" color in order to set what's inside the oval, but not the background.

I might instead remove the "mask" logic and instead give the designable view an inspectable property, fillColor , and just add a CAShapeLayer as a sublayer, using that fillColor :

@IBDesignable
class CircleView: UIView {

    private var shapeLayer = CAShapeLayer()

    @IBInspectable var fillColor: UIColor = .blue {
        didSet {
            shapeLayer.fillColor = fillColor.cgColor
        }
    }

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

        configure()
    }

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

        configure()
    }

    private func configure() {
        shapeLayer.fillColor = fillColor.cgColor
        layer.addSublayer(shapeLayer)
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        shapeLayer.path = UIBezierPath(ovalIn: bounds).cgPath
    }
}

This accomplishes the same thing, but I think the distinction of fill colors vs background colors is more intuitive. But you may have had other reasons for using the masking approach, but just make sure if you do that, that you have something to reveal after it's masked (eg a background color or something else you're rendering).

Please add "super.layoutSubviews()"

override func layoutSubviews() {
    setupMask()
    super.layoutSubviews()
}

By this way, your design view will be "up to date" .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM