xyk blog

最近は iOS 開発の記事が多めです。

Swift - UIBezierPath でクーポン風画像のパスを描く

検証環境:
Xcode 12.4
Swift 5.3.2

UIBezierPath のよるお絵描きシリーズ。
今回は UIBezierPath を使って、以下のようなクーポン用画像としてよく使われる図形をを描いてみる。

Playground コードサンプル

import UIKit
import PlaygroundSupport

class CouponFrameView: UIView {
    
    private let lineWidth: CGFloat = 8
    private let strokeColor = UIColor.systemYellow
    
    private lazy var borderLayer: CAShapeLayer = {
        let shapeLayer = CAShapeLayer()
        shapeLayer.lineWidth = lineWidth
        shapeLayer.strokeColor = strokeColor.cgColor
        shapeLayer.fillColor = UIColor.white.cgColor
        shapeLayer.lineJoin = .round // パスの接続部分を丸くする
        return shapeLayer
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        configure()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        borderLayer.path = makeCouoponPath(rect: bounds)
    }
    
    private func configure() {
        // layer.backgroundColor = UIColor.systemGray4.cgColor // 矩形確認用
        layer.addSublayer(borderLayer)
    }
    
    private func makeCouoponPath(rect: CGRect) -> CGPath {
        
        let half = lineWidth / 2
        let rect = rect.insetBy(dx: half, dy: half) // 矩形内に収まるように線幅の半分小さくする
        
        let cornerRadius: CGFloat = 32
        let hollowRadius: CGFloat = 16

        let path = UIBezierPath()
        path.move(to: CGPoint(x: rect.minX + cornerRadius, y: rect.minY)) // 1
        path.addLine(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY)) // 2
        path.addArc(withCenter: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY + cornerRadius), // 3
                    radius: cornerRadius,
                    startAngle: -radian(90),
                    endAngle: 0,
                    clockwise: true)
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY - hollowRadius)) // 4
        path.addArc(withCenter: CGPoint(x: rect.maxX, y: rect.midY), // 5
                    radius: hollowRadius,
                    startAngle: -radian(90),
                    endAngle: radian(90),
                    clockwise: false)
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadius)) // 6
        path.addArc(withCenter: CGPoint(x: rect.maxX - cornerRadius, y: rect.maxY - cornerRadius), // 7
                    radius: cornerRadius,
                    startAngle: 0,
                    endAngle: radian(90),
                    clockwise: true)
        path.addLine(to: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY)) // 8
        path.addArc(withCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY - cornerRadius), // 9
                    radius: cornerRadius,
                    startAngle: radian(90),
                    endAngle: radian(180),
                    clockwise: true)
        path.addLine(to: CGPoint(x: rect.minX, y: rect.midY + hollowRadius)) // 10
        path.addArc(withCenter: CGPoint(x: rect.minX, y: rect.midY), // 11
                    radius: hollowRadius,
                    startAngle: radian(90),
                    endAngle: -radian(90),
                    clockwise: false)
        path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius)) // 12
        path.addArc(withCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + cornerRadius), // 13
                    radius: cornerRadius,
                    startAngle: radian(180),
                    endAngle: -radian(90),
                    clockwise: true)
        path.close()
        
        return path.cgPath
    }
    
    private func radian(_ degree: CGFloat) -> CGFloat {
        return degree * .pi / 180.0
    }
}

class MyViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemGray5
        
        let couponFrameView = CouponFrameView()
        view.addSubview(couponFrameView)
        couponFrameView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            couponFrameView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            couponFrameView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            couponFrameView.widthAnchor.constraint(equalToConstant: 500),
            couponFrameView.heightAnchor.constraint(equalToConstant: 200),
        ])
    }
}

PlaygroundPage.current.liveView = MyViewController()

パスの描画順序

その他の UIBezierPath シリーズ

xyk.hatenablog.com

xyk.hatenablog.com