はじめに
UILabelに吹き出しをつけようと思い、いろいろと調べた中で画像を利用せずLayerを追加することで実現できることがわかったため、共有します。
これまでは吹き出し用の画像を文字数に合わせリサイズしていたのですが、サイズ調整がうまくいかず綺麗に描画できなかったため、こちらの方式に切り替えました。
実装方針
- 吹き出し用のBezierPathで吹き出しのPathをつくる
- CAShapeLayerに吹き出しのPathを設定
- UILabelのSubLayerにCAShapeLayerを足す
※こちらの記事を参考にしています
コードの全量
UILabelのExtensionとして実装したコードの全量は以下になります。
extension UILabel {
static let chatBubbleLayerName = "chatBubble"
func addChatBubble(isIncoming: Bool, filledWith color: UIColor) {
if let sublayers = layer.sublayers {
for sublayer in sublayers {
if let name = sublayer.name, name == UILabel.chatBubbleLayerName {
return
}
}
}
let horizontalMargin: CGFloat = 10
let verticalMargin: CGFloat = 6
let width: CGFloat = frame.size.width + horizontalMargin * 2
let height: CGFloat = frame.size.height + verticalMargin * 2
let bezierPath = UIBezierPath()
if isIncoming {
bezierPath.move(to: CGPoint(x: 22, y: height))
bezierPath.addLine(to: CGPoint(x: width - 17, y: height))
bezierPath.addCurve(to: CGPoint(x: width, y: height - 17), controlPoint1: CGPoint(x: width - 7.61, y: height), controlPoint2: CGPoint(x: width, y: height - 7.61))
bezierPath.addLine(to: CGPoint(x: width, y: 17))
bezierPath.addCurve(to: CGPoint(x: width - 17, y: 0), controlPoint1: CGPoint(x: width, y: 7.61), controlPoint2: CGPoint(x: width - 7.61, y: 0))
bezierPath.addLine(to: CGPoint(x: 21, y: 0))
bezierPath.addCurve(to: CGPoint(x: 4, y: 17), controlPoint1: CGPoint(x: 11.61, y: 0), controlPoint2: CGPoint(x: 4, y: 7.61))
bezierPath.addLine(to: CGPoint(x: 4, y: height - 11))
bezierPath.addCurve(to: CGPoint(x: 0, y: height), controlPoint1: CGPoint(x: 4, y: height - 1), controlPoint2: CGPoint(x: 0, y: height))
bezierPath.addLine(to: CGPoint(x: -0.05, y: height - 0.01))
bezierPath.addCurve(to: CGPoint(x: 11.04, y: height - 4.04), controlPoint1: CGPoint(x: 4.07, y: height + 0.43), controlPoint2: CGPoint(x: 8.16, y: height - 1.06))
bezierPath.addCurve(to: CGPoint(x: 22, y: height), controlPoint1: CGPoint(x: 16, y: height), controlPoint2: CGPoint(x: 19, y: height))
} else {
bezierPath.move(to: CGPoint(x: width - 22, y: height))
bezierPath.addLine(to: CGPoint(x: 17, y: height))
bezierPath.addCurve(to: CGPoint(x: 0, y: height - 17), controlPoint1: CGPoint(x: 7.61, y: height), controlPoint2: CGPoint(x: 0, y: height - 7.61))
bezierPath.addLine(to: CGPoint(x: 0, y: 17))
bezierPath.addCurve(to: CGPoint(x: 17, y: 0), controlPoint1: CGPoint(x: 0, y: 7.61), controlPoint2: CGPoint(x: 7.61, y: 0))
bezierPath.addLine(to: CGPoint(x: width - 21, y: 0))
bezierPath.addCurve(to: CGPoint(x: width - 4, y: 17), controlPoint1: CGPoint(x: width - 11.61, y: 0), controlPoint2: CGPoint(x: width - 4, y: 7.61))
bezierPath.addLine(to: CGPoint(x: width - 4, y: height - 11))
bezierPath.addCurve(to: CGPoint(x: width, y: height), controlPoint1: CGPoint(x: width - 4, y: height - 1), controlPoint2: CGPoint(x: width, y: height))
bezierPath.addLine(to: CGPoint(x: width + 0.05, y: height - 0.01))
bezierPath.addCurve(to: CGPoint(x: width - 11.04, y: height - 4.04), controlPoint1: CGPoint(x: width - 4.07, y: height + 0.43), controlPoint2: CGPoint(x: width - 8.16, y: height - 1.06))
bezierPath.addCurve(to: CGPoint(x: width - 22, y: height), controlPoint1: CGPoint(x: width - 16, y: height), controlPoint2: CGPoint(x: width - 19, y: height))
}
bezierPath.close()
let chatBubbleLayer = CAShapeLayer()
chatBubbleLayer.path = bezierPath.cgPath
chatBubbleLayer.fillColor = color.cgColor
let adjust: CGFloat = 4
if isIncoming {
chatBubbleLayer.frame = CGRect((0 - horizontalMargin), (0 - verticalMargin), width - adjust, height)
} else {
chatBubbleLayer.frame = CGRect((0 - horizontalMargin + adjust), (0 - verticalMargin), width - adjust, height)
}
chatBubbleLayer.name = UILabel.chatBubbleLayerName
layer.insertSublayer(chatBubbleLayer, at: 0)
}
func removeChatBubble() {
guard let sublayers = layer.sublayers else {
return
}
for sublayer in sublayers {
if let name = sublayer.name, name == UILabel.chatBubbleLayerName {
sublayer.removeFromSuperlayer()
return
}
}
}
}
コードの解説
isIncoming
は吹き出しの向きを示しています。true
ら相手からのチャットメッセージの向き。
func addChatBubble(isIncoming: Bool, filledWith color: UIColor)
}
同じ吹き出しレイヤーが追加されないようするためのチェック処理。
if let sublayers = layer.sublayers {
for sublayer in sublayers {
if let name = sublayer.name, name == UILabel.chatBubbleLayerName {
return
}
}
}
吹き出しとUILabelのテキストの余白、吹き出しのサイズをここで設定しています。
let horizontalMargin: CGFloat = 10
let verticalMargin: CGFloat = 6
let width: CGFloat = frame.size.width + horizontalMargin * 2
let height: CGFloat = frame.size.height + verticalMargin * 2
BezierPathで吹き出しのPathを作っていきます。
吹き出しの直線部分を上記のwidth
とheight
を使って調整しています。
let bezierPath = UIBezierPath()
if isIncoming {
bezierPath.move(to: CGPoint(x: 22, y: height))
bezierPath.addLine(to: CGPoint(x: width - 17, y: height))
bezierPath.addCurve(to: CGPoint(x: width, y: height - 17), controlPoint1: CGPoint(x: width - 7.61, y: height), controlPoint2: CGPoint(x: width, y: height - 7.61))
bezierPath.addLine(to: CGPoint(x: width, y: 17))
bezierPath.addCurve(to: CGPoint(x: width - 17, y: 0), controlPoint1: CGPoint(x: width, y: 7.61), controlPoint2: CGPoint(x: width - 7.61, y: 0))
bezierPath.addLine(to: CGPoint(x: 21, y: 0))
bezierPath.addCurve(to: CGPoint(x: 4, y: 17), controlPoint1: CGPoint(x: 11.61, y: 0), controlPoint2: CGPoint(x: 4, y: 7.61))
bezierPath.addLine(to: CGPoint(x: 4, y: height - 11))
bezierPath.addCurve(to: CGPoint(x: 0, y: height), controlPoint1: CGPoint(x: 4, y: height - 1), controlPoint2: CGPoint(x: 0, y: height))
bezierPath.addLine(to: CGPoint(x: -0.05, y: height - 0.01))
bezierPath.addCurve(to: CGPoint(x: 11.04, y: height - 4.04), controlPoint1: CGPoint(x: 4.07, y: height + 0.43), controlPoint2: CGPoint(x: 8.16, y: height - 1.06))
bezierPath.addCurve(to: CGPoint(x: 22, y: height), controlPoint1: CGPoint(x: 16, y: height), controlPoint2: CGPoint(x: 19, y: height))
} else {
bezierPath.move(to: CGPoint(x: width - 22, y: height))
bezierPath.addLine(to: CGPoint(x: 17, y: height))
bezierPath.addCurve(to: CGPoint(x: 0, y: height - 17), controlPoint1: CGPoint(x: 7.61, y: height), controlPoint2: CGPoint(x: 0, y: height - 7.61))
bezierPath.addLine(to: CGPoint(x: 0, y: 17))
bezierPath.addCurve(to: CGPoint(x: 17, y: 0), controlPoint1: CGPoint(x: 0, y: 7.61), controlPoint2: CGPoint(x: 7.61, y: 0))
bezierPath.addLine(to: CGPoint(x: width - 21, y: 0))
bezierPath.addCurve(to: CGPoint(x: width - 4, y: 17), controlPoint1: CGPoint(x: width - 11.61, y: 0), controlPoint2: CGPoint(x: width - 4, y: 7.61))
bezierPath.addLine(to: CGPoint(x: width - 4, y: height - 11))
bezierPath.addCurve(to: CGPoint(x: width, y: height), controlPoint1: CGPoint(x: width - 4, y: height - 1), controlPoint2: CGPoint(x: width, y: height))
bezierPath.addLine(to: CGPoint(x: width + 0.05, y: height - 0.01))
bezierPath.addCurve(to: CGPoint(x: width - 11.04, y: height - 4.04), controlPoint1: CGPoint(x: width - 4.07, y: height + 0.43), controlPoint2: CGPoint(x: width - 8.16, y: height - 1.06))
bezierPath.addCurve(to: CGPoint(x: width - 22, y: height), controlPoint1: CGPoint(x: width - 16, y: height), controlPoint2: CGPoint(x: width - 19, y: height))
}
bezierPath.close()
最後にCAShapeLayerに吹き出しのPathと色を設定し、SubLayerの最下層にインサートします。
吹き出しが左右非対称のためマージンのとりかたを吹き出しの向きによって変えています。
let chatBubbleLayer = CAShapeLayer()
chatBubbleLayer.path = bezierPath.cgPath
chatBubbleLayer.fillColor = color.cgColor
let adjust: CGFloat = 4
if isIncoming {
chatBubbleLayer.frame = CGRect((0 - horizontalMargin), (0 - verticalMargin), width - adjust, height)
} else {
chatBubbleLayer.frame = CGRect((0 - horizontalMargin + adjust), (0 - verticalMargin), width - adjust, height)
}
chatBubbleLayer.name = UILabel.chatBubbleLayerName
layer.insertSublayer(chatBubbleLayer, at: 0)
吹き出し内の文字数が変わる場合は一度削除して再描画したかったので、吹き出しレイヤーの削除メソッドも用意しました。
func removeChatBubble() {
guard let sublayers = layer.sublayers else {
return
}
for sublayer in sublayers {
if let name = sublayer.name, name == UILabel.chatBubbleLayerName {
sublayer.removeFromSuperlayer()
return
}
}
}