Search code examples
iosswiftuiviewswift4uibezierpath

Parallelogram view using UIBezierPath


I am trying to create custom parallelogram view using UIBezierPath but not getting a perfect one.

Following is my custom view code

class CustomView: UIView {
    override func draw(_ rect: CGRect) {
        let offset  = 60.0;
        let path = UIBezierPath()

           path.move(to: CGPoint(x: self.frame.origin.x + CGFloat(offset), y: self.frame.origin.y))
           path.addLine(to: CGPoint(x: self.frame.width + self.frame.origin.x  , y: self.frame.origin.y))
           path.addLine(to: CGPoint(x: self.frame.origin.x + self.frame.width - CGFloat(offset) , y: self.frame.origin.y + self.frame.height))
           path.addLine(to: CGPoint(x: self.frame.origin.x, y: self.frame.origin.y + self.frame.height))


        // Close the path. This will create the last line automatically.
         path.close()
        UIColor.red.setFill()
        path.fill()

        let shapeLayer = CAShapeLayer()
        shapeLayer.path = path.cgPath
        self.layer.mask = shapeLayer;

    }
}

And i create the view using

let customView = CustomView()
customView.frame = CGRect(origin: CGPoint(x: 10, y: 20), size: CGSize(width: 250, height: 250))
self.view.addSubview(customView)

But i got the view like this and it is not a perfect parallelogram.

enter image description here


Solution

  • The problem is the use of frame within draw(_:). The issue is that frame is “the size and position of the view in its superview’s coordinate system” (emphasis added). How you render this shape within this view generally has nothing to do with where this view is located in its superview. And if this view doesn’t happen to be at 0, 0 of its superview, you can experience cropping.

    But don’t use rect parameter, either. “The first time your view is drawn, this rectangle is typically the entire visible bounds of your view. However, during subsequent drawing operations, the rectangle may specify only part of your view.” You risk having shape radically changed if the OS decides it only needs to update a part of your CustomView.

    Use bounds, instead, which is in the view’s own coordinate system. And, using minX, minY, maxX, and maxY simplifies the code a bit.

    @IBDesignable
    class CustomView: UIView {
    
        @IBInspectable var offset:    CGFloat = 60        { didSet { setNeedsDisplay() } }
        @IBInspectable var fillColor: UIColor = .red      { didSet { setNeedsDisplay() } }
    
        override func draw(_ rect: CGRect) {
            let path = UIBezierPath()
    
            path.move(to: CGPoint(x: bounds.minX + offset, y: bounds.minY))
            path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
            path.addLine(to: CGPoint(x: bounds.maxX - offset, y: bounds.maxY))
            path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
    
            // Close the path. This will create the last line automatically.
            path.close()
            fillColor.setFill()
            path.fill()
        }
    
    }
    

    enter image description here

    As an aside, I’m not setting the mask. If you’re animating, it seems that it might be inefficient to constantly reset the mask every time draw is called. I’d personally just set the background color of the view to .clear. But that’s not relevant to the immediate question.