Search code examples
swiftuiviewuikitoutline

Swift: Make the outline of a UIView sketch-like


I want to make the outlines of a UIView look "wavey" like someone drew them.

I have this example from PowerPoint, which allows to do it (should work with any size and corner radius):

enter image description here

Currently this is what I have:

myView.layer.borderWidth = 10
myView.layer.borderColor = UIColor.blue.cgColor
myView.layer.cornerRadius = 5 // Optional

Thank


Solution

  • You can create "wavy" lines by using a UIBezierPath with a combination of quad-curves, lines, arcs, etc.

    We'll start with a simple line, one-quarter of the width of the view:

    enter image description here

    Our path would consist of:

    • move to 0,0
    • add line to 80,0

    If we change that to a quad-curve:

    enter image description here

    Now we're doing:

    • move to 0,0
    • add quad-curve to 80,0 with control point 40,40

    If we add another quad-curve going the other way:

    enter image description here

    Now we're doing:

    • move to 0,0
    • add quad-curve to 80,0 with control point 40,40
    • add quad-curve to 160,0 with control point 120,-40

    and we can extend that the width of the view:

    enter image description here

    of course, that doesn't look like your "sketch" target, so let's change the control-point offsets from 40 to 2:

    enter image description here

    Now it looks a bit more like a hand-draw "sketched" line.

    It's too uniform, though, and it's partially outside the bounds of the view, so let's inset it by 8-pts and, instead of four 25% segments, we'll use (for example) five segments of these widths:

    0.15, 0.2, 0.2, 0.27, 0.18
    

    enter image description here

    If we take the same approach to go down the right-hand side, back across the bottom, and up the left-hand side, we can get this:

    enter image description here

    Here's some example code to produce that view:

    class SketchBorderView: UIView {
        
        let borderLayer: CAShapeLayer = CAShapeLayer()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() -> Void {
            borderLayer.fillColor = UIColor.clear.cgColor
            borderLayer.strokeColor = UIColor.blue.cgColor
            layer.addSublayer(borderLayer)
            backgroundColor = .yellow
        }
        override func layoutSubviews() {
            
            let incrementVals: [CGFloat] = [
                0.15, 0.2, 0.2, 0.27, 0.18,
            ]
            let lineOffsets: [[CGFloat]] = [
                [ 1.0, -2.0],
                [-1.0,  2.0],
                [-1.0, -2.0],
                [ 1.0,  2.0],
                [ 0.0, -2.0],
            ]
    
            let pth: UIBezierPath = UIBezierPath()
            
            // inset bounds by 8-pts so we can draw the "wavy border"
            //  inside our bounds
            let r: CGRect = bounds.insetBy(dx: 8.0, dy: 8.0)
            
            var ptDest: CGPoint = .zero
            var ptControl: CGPoint = .zero
    
            // start at top-left
            ptDest = r.origin
            pth.move(to: ptDest)
    
            // we're at top-left
            for i in 0..<incrementVals.count {
    
                ptDest.x += r.width * incrementVals[i]
                ptDest.y = r.minY + lineOffsets[i][0]
    
                ptControl.x = pth.currentPoint.x + ((ptDest.x - pth.currentPoint.x) * 0.5)
                ptControl.y = r.minY + lineOffsets[i][1]
    
                pth.addQuadCurve(to: ptDest, controlPoint: ptControl)
    
            }
            
            // now we're at top-right
            for i in 0..<incrementVals.count {
                
                ptDest.y += r.height * incrementVals[i]
                ptDest.x = r.maxX + lineOffsets[i][0]
                
                ptControl.y = pth.currentPoint.y + ((ptDest.y - pth.currentPoint.y) * 0.5)
                ptControl.x = r.maxX + lineOffsets[i][1]
                
                pth.addQuadCurve(to: ptDest, controlPoint: ptControl)
                
            }
            
            // now we're at bottom-right
            for i in 0..<incrementVals.count {
                
                ptDest.x -= r.width * incrementVals[i]
                ptDest.y = r.maxY + lineOffsets[i][0]
                
                ptControl.x = pth.currentPoint.x - ((pth.currentPoint.x - ptDest.x) * 0.5)
                ptControl.y = r.maxY + lineOffsets[i][1]
                
                pth.addQuadCurve(to: ptDest, controlPoint: ptControl)
                
            }
            
            // now we're at bottom-left
            for i in 0..<incrementVals.count {
                
                ptDest.y -= r.height * incrementVals[i]
                ptDest.x = r.minX + lineOffsets[i][0]
                
                ptControl.y = pth.currentPoint.y - ((pth.currentPoint.y - ptDest.y) * 0.5)
                ptControl.x = r.minX + lineOffsets[i][1]
                
                pth.addQuadCurve(to: ptDest, controlPoint: ptControl)
                
            }
    
            borderLayer.path = pth.cgPath
        }
        
    }
    

    and an example controller:

    class SketchTestVC: UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let v = SketchBorderView()
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                v.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                v.centerYAnchor.constraint(equalTo: g.centerYAnchor),
                v.widthAnchor.constraint(equalToConstant: 320.0),
                v.heightAnchor.constraint(equalTo: v.widthAnchor),
            ])
        }
        
    }
    

    Using that code, though, we still have too much uniformity, so in actual use we'd want to randomize the number of segments, the widths of the segments, and the control-point offsets.

    Of course, to get your "rounded rect" you'd want to add arcs at the corners.

    I expect this should get you on your way though.