0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Swift】UILabelにLayerを追加して吹き出しをつける

Last updated at Posted at 2020-05-09

はじめに

UILabelに吹き出しをつけようと思い、いろいろと調べた中で画像を利用せずLayerを追加することで実現できることがわかったため、共有します。
これまでは吹き出し用の画像を文字数に合わせリサイズしていたのですが、サイズ調整がうまくいかず綺麗に描画できなかったため、こちらの方式に切り替えました。

実現イメージ

実装方針

  1. 吹き出し用のBezierPathで吹き出しのPathをつくる
  2. CAShapeLayerに吹き出しのPathを設定
  3. 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を作っていきます。
吹き出しの直線部分を上記のwidthheightを使って調整しています。

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
        }
    }
}
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?