Search code examples
iosswiftsvguikituibezierpath

Mask UIView (UICollectionView) correctly


Ok, I need to mask a horizontal UICollectionViewso it looks like a ribbon. My web dev counterpart got it done with an SVG that he used as a masking layer.

Here is my current situation:enter image description here

And here is what I need (Photo from our web app):enter image description here

Small detail in image spacing aside, I have an SVG like this: enter image description here

Which I can successfully convert into UIBezierPath code (Paintcode):

let pathPath = UIBezierPath()
pathPath.move(to: CGPoint(x: 0.05, y: 0))
pathPath.addCurve(to: CGPoint(x: 162.02, y: 3.8), controlPoint1: CGPoint(x: 0.05, y: 2.1), controlPoint2: CGPoint(x: 72.57, y: 3.8))
pathPath.addCurve(to: CGPoint(x: 324, y: 0), controlPoint1: CGPoint(x: 251.48, y: 3.8), controlPoint2: CGPoint(x: 324, y: 2.1))
pathPath.addLine(to: CGPoint(x: 324, y: 150.2))
pathPath.addCurve(to: CGPoint(x: 162.02, y: 154), controlPoint1: CGPoint(x: 324, y: 152.3), controlPoint2: CGPoint(x: 251.48, y: 154))
pathPath.addCurve(to: CGPoint(x: 0.05, y: 150.2), controlPoint1: CGPoint(x: 72.57, y: 154), controlPoint2: CGPoint(x: 0.05, y: 152.3))
pathPath.addCurve(to: CGPoint(x: 0, y: 0.17), controlPoint1: CGPoint(x: 0.05, y: 148.1), controlPoint2: CGPoint(x: 0, y: 0.17))
pathPath.usesEvenOddFillRule = true
UIColor.lightGray.setFill()
pathPath.fill()

Now what do I do?


Solution

  • You should use the mask property of your UICollectionView by setting it to a view whose alpha channel indicates what part of the UICollectionView you want to mask out. In outline it will probably be something like:

    // If you don't have a custom subclass of UICollectionView... you can handle the resize in the
    // UICollectionViewController or whatever view contoller is handling your view.
    //
    // If you do have a custom subclass of UICollectionView... you could do something similar 
    // in layoutSubviews.
    
    class MyViewController : UICollectionViewController {
    
        // recreate the mask after the view lays out it's subviews
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
    
            if let maskImage = createMaskingImage(size: self.view.bounds.size) {
                let maskView = UIView(frame: self.view.bounds)
                maskView.layer.contents = maskImage
    
                self.view.mask = maskView
            }
        }
    }
    
    // create a masking image for a view of the given size
    func createMaskingImage(size: CGSize) -> UIImage? {
    
        let drawingBounds = CGRect(origin: CGPoint.zero, size: size)
    
        UIGraphicsBeginImageContext(size)
    
        // We're going to jump down to the level of cgContext for scaling
        // You might be able to do this from the level of UIGraphics, but I don't know how so...
        // Implicitly unwrapped optional, but if we can't get a CGContext we're in trouble
    
        let cgContext = UIGraphicsGetCurrentContext()!
        let maskingPath = createMaskingPath()
        let pathBounds = maskingPath.bounds;
    
        cgContext.saveGState()
    
        // Clearing the image may not strictly be necessary
        cgContext.clear(drawingBounds)
    
        // Scale the context so that when we draw the path it fits in the drawing bounds
        // Could just use "size" here instead of drawingBounds.size, but I think this makes it a 
        // little more explicit that we're matching up two rects
        cgContext.scaleBy(x: drawingBounds.size.width / pathBounds.size.width,
                          y: drawingBounds.size.height / pathBounds.size.height)
        cgContext.setFillColor(UIColor.lightGray.cgColor)
        cgContext.addPath(maskingPath.cgPath)
        cgContext.fillPath(using: .evenOdd)
    
        cgContext.restoreGState()
    
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    
        return image
    }
    
    func createMaskingPath() -> UIBezierPath {
        let path = UIBezierPath()
    
        path.move(to: CGPoint(x: 0.05, y: 0))
        path.addCurve(to: CGPoint(x: 162.02, y: 3.8), controlPoint1: CGPoint(x: 0.05, y: 2.1), controlPoint2: CGPoint(x: 72.57, y: 3.8))
        path.addCurve(to: CGPoint(x: 324, y: 0), controlPoint1: CGPoint(x: 251.48, y: 3.8), controlPoint2: CGPoint(x: 324, y: 2.1))
        path.addLine(to: CGPoint(x: 324, y: 150.2))
        path.addCurve(to: CGPoint(x: 162.02, y: 154), controlPoint1: CGPoint(x: 324, y: 152.3), controlPoint2: CGPoint(x: 251.48, y: 154))
        path.addCurve(to: CGPoint(x: 0.05, y: 150.2), controlPoint1: CGPoint(x: 72.57, y: 154), controlPoint2: CGPoint(x: 0.05, y: 152.3))
        path.addCurve(to: CGPoint(x: 0, y: 0.17), controlPoint1: CGPoint(x: 0.05, y: 148.1), controlPoint2: CGPoint(x: 0, y: 0.17))
    
        return path
    }