検証環境:
Xcode 11.6
Swift 5.2.4

こんな吹き出しを UIBezierPath を使って描画してみます。
パスの描画順は以下図の流れになっています。
吹き出し部分は addQuadCurve メソッドを使い、曲線上の終点とコントロールポイント1つを指定します。
この時の終点は円周上にありますが、円周上の点の座標の求め方は以下図のように三角関数を利用します。
以下はPlayground で実装したコードサンプルです。
import UIKit import PlaygroundSupport class CalloutView: UIView { private let lineWidth: CGFloat = 8 private let strokeColor = UIColor.systemRed private let arrowHeight: CGFloat = 36 private lazy var borderLayer: CAShapeLayer = { let shapeLayer = CAShapeLayer() shapeLayer.lineWidth = lineWidth shapeLayer.strokeColor = strokeColor.cgColor shapeLayer.fillColor = UIColor.white.cgColor 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 = makeCalloutPath(rect: bounds) } private func configure() { //layer.backgroundColor = UIColor.systemGray4.cgColor // 矩形確認用 layer.addSublayer(borderLayer) } private func makeCalloutPath(rect: CGRect) -> CGPath { let centerX: CGFloat = rect.midX let centerY: CGFloat let radius: CGFloat if rect.width > rect.height - arrowHeight { radius = (rect.maxY - arrowHeight - lineWidth) / 2 centerY = radius + lineWidth / 2 } else { radius = (rect.maxX - lineWidth) / 2 centerY = rect.midY - arrowHeight / 2 } let controlPoint = CGPoint(x: centerX, y: centerY + radius + arrowHeight * 2) let path = UIBezierPath() // 1 path.addArc(withCenter: CGPoint(x: centerX, y: centerY), radius: radius, startAngle: radian(270), endAngle: 0, clockwise: true) // 2 path.addArc(withCenter: CGPoint(x: centerX, y: centerY), radius: radius, startAngle: 0, endAngle: radian(75), clockwise: true) // 3 path.addQuadCurve(to: CGPoint(x: centerX - (radius * sin(radian(15))), y: centerY + (radius * cos(radian(15)))), controlPoint: controlPoint) // 4 path.addArc(withCenter: CGPoint(x: centerX, y: centerY), radius: radius, startAngle: radian(105), endAngle: radian(180), clockwise: true) // 5 path.addArc(withCenter: CGPoint(x: centerX, y: centerY), radius: radius, startAngle: radian(180), endAngle: radian(270), 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 calloutView = CalloutView() view.addSubview(calloutView) calloutView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ calloutView.centerXAnchor.constraint(equalTo: view.centerXAnchor), calloutView.centerYAnchor.constraint(equalTo: view.centerYAnchor), calloutView.widthAnchor.constraint(equalToConstant: 250), calloutView.heightAnchor.constraint(equalToConstant: 286), ]) } } PlaygroundPage.current.liveView = MyViewController()