pathuikitshapes

UIKit Shape won't become more curved


I've tried everything, but it does not make what I need. I think it's impossible. The half circle is too sharp, but it won't build a more cornered one. Middle ark won't become more curved no matter what I try. You can see it in c4 part

What I have

What I'm trying to achieve

struct TabBarShape: Shape {

    func path(in rect: CGRect) -> Path {
        var path = Path()

        let middleRad: CGFloat = rect.height - 10.0
        let cornerRad: CGFloat = 12.0

        // Define corner points
        let topLeftC = CGPoint(x: rect.minX + cornerRad, y: rect.minY + cornerRad)
        let topRightC = CGPoint(x: rect.maxX - cornerRad, y: rect.minY + cornerRad)
        let botRightC = CGPoint(x: rect.maxX - cornerRad, y: rect.maxY - cornerRad)
        let botLeftC = CGPoint(x: rect.minX + cornerRad, y: rect.maxY - cornerRad)

        // 1: Start at the top left arc point
        var pt = CGPoint(x: rect.minX, y: rect.minY + cornerRad)
        path.move(to: pt)

        // c1: Top-left corner
        path.addArc(center: topLeftC, radius: cornerRad, startAngle: .degrees(180), endAngle: .degrees(270), clockwise: false)

        // 2: Move to middle left before the curve
        pt = CGPoint(x: rect.midX - middleRad, y: rect.minY)
        path.addLine(to: pt)

        // c2: Top-left middle arc
        pt.y += middleRad * 0.5
        path.addArc(center: pt, radius: middleRad * 0.5, startAngle: .degrees(-90), endAngle: .degrees(0), clockwise: false)

        // c3: Top-middle right arc
        pt.x += middleRad
        path.addArc(center: pt, radius: middleRad * 0.5, startAngle: .degrees(180), endAngle: .degrees(0), clockwise: true)

        // c4: Complete middle-right arc
        pt.x += middleRad
        path.addArc(center: pt, radius: middleRad * 0.5, startAngle: .degrees(180), endAngle: .degrees(270), clockwise: false)

        // 3: Top-right line to corner
        pt = CGPoint(x: rect.maxX - cornerRad, y: rect.minY)
        path.addLine(to: pt)

        // c5: Top-right corner
        path.addArc(center: topRightC, radius: cornerRad, startAngle: .degrees(-90), endAngle: .degrees(0), clockwise: false)

        // 4: Right side down to bottom-right corner
        pt = CGPoint(x: rect.maxX, y: rect.maxY - cornerRad)
        path.addLine(to: pt)

        // c6: Bottom-right corner
        path.addArc(center: botRightC, radius: cornerRad, startAngle: .degrees(0), endAngle: .degrees(90), clockwise: false)

        // 5: Bottom-left line to corner
        pt = CGPoint(x: rect.minX + cornerRad, y: rect.maxY)
        path.addLine(to: pt)

        // c7: Bottom-left corner
        path.addArc(center: botLeftC, radius: cornerRad, startAngle: .degrees(90), endAngle: .degrees(180), clockwise: false)

        path.closeSubpath()

        return path
    }
}



Solution

  • You can do this by using .addCurve with control points for the "gap" instead of .addArc:


    Sample view subclass:

    class TabBarShapeView: UIView {
        
        // adjust these as desired
        let gapWidth: CGFloat = 120.0
        let gapHeight: CGFloat = 36.0
        let cornerRad: CGFloat = 12.0
        let fillColor: UIColor = .init(red: 0.0, green: 0.0, blue: 0.5, alpha: 1.0)
        
        var shapeLayer: CAShapeLayer!
        override class var layerClass: AnyClass {
            return CAShapeLayer.self
        }
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        private func commonInit() {
            shapeLayer = self.layer as? CAShapeLayer
            shapeLayer.fillColor = fillColor.cgColor
        }
        
        func updateMe() {
            
            let b = bounds.insetBy(dx: 1.0 / UIScreen.main.scale, dy: 1.0 / UIScreen.main.scale)
    
            let leftPt: CGPoint = .init(x: b.midX - gapWidth * 0.5, y: 0.0)
            let rightPt: CGPoint = .init(x: bounds.maxX - leftPt.x, y: leftPt.y)
            
            let bottomCenterPt: CGPoint = .init(x: b.midX, y: gapHeight)
            
            // control points for left side of gap
            let lcp1: CGPoint = .init(x: leftPt.x + gapWidth * 0.25, y: 0.0)
            let lcp2: CGPoint = .init(x: leftPt.x + gapWidth * 0.25, y: bottomCenterPt.y)
    
            // control points for right side of gap
            let rcp1: CGPoint = .init(x: b.maxX - lcp2.x, y: lcp2.y)
            let rcp2: CGPoint = .init(x: b.maxX - lcp1.x, y: lcp1.y)
    
            let shape: UIBezierPath = UIBezierPath()
            
            shape.move(to: leftPt)
    
            // add left-side of gap curve
            shape.addCurve(to: bottomCenterPt, controlPoint1: lcp1, controlPoint2: lcp2)
            
            // add right-side of gap curve
            shape.addCurve(to: rightPt, controlPoint1: rcp1, controlPoint2: rcp2)
            
            // add top-right, bottom-right, bottom-left, top-left curved corners
            shape.addArc(withCenter: .init(x: b.maxX - cornerRad, y: b.minY + cornerRad), radius: cornerRad, startAngle: -.pi * 0.5, endAngle: .pi * 0.0, clockwise: true)
            shape.addArc(withCenter: .init(x: b.maxX - cornerRad, y: b.maxY - cornerRad), radius: cornerRad, startAngle:  .pi * 0.0, endAngle: .pi * 0.5, clockwise: true)
            shape.addArc(withCenter: .init(x: b.minX + cornerRad, y: b.maxY - cornerRad), radius: cornerRad, startAngle:  .pi * 0.5, endAngle: .pi * 1.0, clockwise: true)
            shape.addArc(withCenter: .init(x: b.minX + cornerRad, y: b.minY + cornerRad), radius: cornerRad, startAngle:  .pi * 1.0, endAngle: .pi * 1.5, clockwise: true)
            
            shape.close()
            
            shapeLayer.path = shape.cgPath
            
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            updateMe()
        }
    
    }
    

    Controller to show the view:

    class TabBarShapeTestVC: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .systemBackground
            
            let pv = TabBarShapeView()
            pv.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(pv)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                
                pv.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                pv.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 12.0),
                pv.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -12.0),
                pv.heightAnchor.constraint(equalToConstant: 60.0),
                
            ])
        }
        
    }
    

    Result:

    result