אורי orihpt
אורי orihpt

Reputation: 2644

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

Upvotes: 2

Views: 257

Answers (2)

DonMag
DonMag

Reputation: 77690

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.

Upvotes: 6

Noman Umar
Noman Umar

Reputation: 409

use this extension to solve the issue

import Foundation import UIKit

extension UIView {
    func dropShadow(scale: Bool = true) {
        layer.masksToBounds = false
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOpacity = 0.2
        layer.shadowOffset = .zero
        layer.shadowRadius = 5
        layer.shouldRasterize = true
        layer.rasterizationScale = scale ? UIScreen.main.scale : 1
    }
   
    @IBInspectable
    var cornerRadius: CGFloat {
        get {
            return layer.cornerRadius
        }
        set {
            layer.cornerRadius = newValue
            layer.masksToBounds = newValue > 0
        }
    }
    
    @IBInspectable
    var borderWidth: CGFloat {
        get {
            return layer.borderWidth
        }
        set {
            layer.borderWidth = newValue
        }
    }
    
    @IBInspectable
    var borderColor: UIColor? {
        get {
            let color = UIColor.init(cgColor: layer.borderColor!) //UIColor.init(CGColor: layer.borderColor!)
            return color
        }
        set {
            layer.borderColor = newValue?.cgColor
        }
    }
    
    @IBInspectable
    var shadowRadius: CGFloat {
        get {
            return layer.shadowRadius
        }
        set {
            layer.shadowRadius = newValue
        }
    }
    
    @IBInspectable
    var shadowOpacity: Float {
        get {
            return layer.shadowOpacity
        }
        set {
            layer.shadowOpacity = newValue
        }
    }
    
    @IBInspectable
    var shadowOffset: CGSize {
        get {
            return layer.shadowOffset
        }
        set {
            layer.shadowOffset = newValue
        }
    }
    
    @IBInspectable
    var shadowColor: UIColor? {
        get {
            if let color = layer.shadowColor {
                return UIColor(cgColor: color)
            }
            return nil
        }
        set {
            if let color = newValue {
                layer.shadowColor = color.cgColor
            } else {
                layer.shadowColor = nil
            }
        }
    }
}

it will look like this enter image description here

Upvotes: 0

Related Questions