遠い遠い昔にUIKitで書いたコードをSwiftUIで使えるように書き直し。
https://dishware.sakura.ne.jp/swift/archives/111
線の引き方の流れは過去記事の方に記載してあります。参考にしたサイトとかは無くなっちゃってるのもありますが。
#7月とかに多かったゲリラ豪雨時なんかの雷は嫌いなんですが、INZM聴いてるとリピートしている今日この頃。ラップ部分で喉痛めちゃったりしないのかな・・と心配になる。
使い方は #Preview を参照してください。shapeColor も fillColor も省略することが可能です。flip の Bool 値を true にすると反転表示になります。
iOS16 向けに作っていますが、iOS15とかでどうなるかは未確認なのでご了承ください。
import SwiftUI
struct Balloon<Content: View>: View {
let shapeColor: Color
let fillColor: Color?
let flip: Bool
let content: Content
init(shapeColor: Color = .black, fillColor: Color? = nil, flip: Bool = false, @ViewBuilder content: () -> Content) {
self.shapeColor = shapeColor
self.fillColor = fillColor
self.flip = flip
self.content = content()
}
var body: some View {
content
.padding()
.background(content: {
BalloonShape(shapeColor: self.shapeColor, fillColor: self.fillColor)
.rotation3DEffect(.degrees(self.flip ? 180 : 0), axis: (x: 0, y: 1, z: 0))
})
}
}
private struct BalloonShape: View {
var shapeColor: Color = .black
var fillColor: Color? = nil
init(shapeColor: Color = .black, fillColor: Color? = nil) {
self.shapeColor = shapeColor
self.fillColor = fillColor
}
var body: some View {
GeometryReader { proxy in
balloonPath(proxy: proxy)
.stroke(shapeColor, lineWidth: 1)
.background(self.fillColor != nil ? self.fillColor : .clear)
.clipShape(balloonPath(proxy: proxy))
}
}
private func balloonPath(proxy: GeometryProxy) -> Path {
let padding: CGFloat = 5.0
let cornerRadius: CGFloat = 15.5
let arrowWidth: CGFloat = 8.0
let arrowHeight: CGFloat = 25.0
let arrowTopCurveHeight: CGFloat = 4.0
let shapeRect = CGRect(x: 0, y: 0, width: proxy.size.width, height: proxy.size.height)
let balloonRect = CGRect(x: shapeRect.origin.x + padding, y: shapeRect.origin.y + padding, width: shapeRect.size.width - (padding * 2) - arrowWidth, height: shapeRect.size.height - (padding * 2))
let leftX = balloonRect.minX + arrowWidth
let rightX = balloonRect.maxX
let topY = balloonRect.minY
let bottomY = balloonRect.maxY
return Path({ path in
path.move(to: CGPoint(x: leftX, y: topY + cornerRadius))
path.arc(x: leftX + cornerRadius, y: topY + cornerRadius, startAngle: 180.0, endAngle: 270.0)
path.arc(x: rightX - cornerRadius, y: topY + cornerRadius, startAngle: 270.0, endAngle: 360.0)
path.arc(x: rightX - cornerRadius, y: bottomY - cornerRadius, startAngle: 0.0, endAngle: 90.0)
path.arc(x: leftX + cornerRadius, y: bottomY - cornerRadius, startAngle: 90.0, endAngle: 135.0)
path.addQuadCurve(to: CGPoint(x: leftX - cornerRadius, y: bottomY), control: CGPoint(x: leftX - arrowWidth, y: bottomY))
path.addQuadCurve(to: CGPoint(x: leftX, y: bottomY - arrowHeight), control: CGPoint(x: leftX, y: bottomY - arrowTopCurveHeight))
path.closeSubpath()
})
}
}
extension Path {
mutating func arc(x: CGFloat, y: CGFloat, cornerRadius: CGFloat = 15.5, startAngle: CGFloat, endAngle: CGFloat) {
addArc(center: CGPoint(x: x, y: y), radius: cornerRadius, startAngle: Angle(radians: startAngle * CGFloat.pi / 180.0), endAngle: Angle(radians: endAngle * CGFloat.pi / 180.0), clockwise: false)
}
}
#Preview {
VStack {
HStack {
Balloon(shapeColor: .blue, fillColor: .blue) {
Text("Knock, knock!")
.foregroundStyle(.white)
.padding(12)
}
Spacer()
}.padding(.horizontal)
HStack {
Spacer()
Balloon(shapeColor: .green, fillColor: .green, flip: true) {
Text("Who’s there?")
.padding(12)
}
}.padding(.horizontal)
HStack {
Balloon {
Text("I'm the new GOAT.")
.padding(12)
}
Spacer()
}.padding(.horizontal)
}
}
プレビューで表示すると以下のような感じになります。テキスト以外を組み込んだ時の確認はしていないのでレイアウト崩れちゃったらごめんなさい😌
TipKit風の吹き出しの場合は以下の記事に書いてあります。が、他の方のブログの方がより良いやり方が載っている気がします。