传统开发都喜欢使用.9图来做这种气泡,但是因为.9图只是一个图片,没有办法做动画效果、颜色动画、背景模糊效果等等。可编程性比较差,所以我个人来讲还是比较喜欢用SwiftUI的Shape的。

用SwiftUI来绘制气泡还是比较简单的,这里举一个例子。

实际效果

实际效果

圆角矩形部分

其实图中这种气泡只是分成了上下两个部分,一个是上面的小尾巴部分,一个是下面的圆角矩形部分。

我们很轻松就可以写下面圆角矩形的部分,并且来自适应里面的内容。

1
2
3
4
5
Text("你好啊!!你好吗,真的好吗好的好的")
.padding(12)
.foregroundColor(Color.white)
.background(.blue)
.cornerRadius(12, antialiased: true)

上面的小尾巴我们可以拆分下来,就是一个圆弧。

圆弧形状

所以我们只需要用SwiftUI的Shape写一个这个圆弧就可以了。

圆弧绘制

这个圆弧别看路径小,但是如果手写的话那也是相当麻烦的。所以这个时候可以绘制一个SVG形状。通过sketch、Figma等UI软件,或者让UI画一个这个小尾巴的SVG格式。或者给你SVG代码。

在Sketch中,我们可以直接复制SVG代码。

复制SVG代码

图片中的例子为:

1
2
3
4
5
<svg width="30px" height="8.60013256px" viewBox="0 0 30 8.60013256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="文章内" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M14.3798678,0 C19.603373,0 24.2717151,8.60013256 30,8.60013256 L7.10542736e-15,8.60013256 C4.16609031,8.60013256 9.49943302,0 14.3798678,0 Z" id="形状结合" fill="#387AF8"></path>
</g>
</svg>

使用SVG转换工具

我们需要将SVG转换成Shape。可以到这里进行转换。

转换形状

使用

Shape已经有了,我们就可以直接使用了。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct TalkTail: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let width = rect.size.width
let height = rect.size.height
path.move(to: CGPoint(x: 0.47933*width, y: 0))
path.addCurve(to: CGPoint(x: width, y: height), control1: CGPoint(x: 0.65345*width, y: 0), control2: CGPoint(x: 0.80906*width, y: height))
path.addLine(to: CGPoint(x: 0, y: height))
path.addCurve(to: CGPoint(x: 0.21336*width, y: 0.55294*height), control1: CGPoint(x: 0.06453*width, y: height), control2: CGPoint(x: 0.13745*width, y: 0.7841*height))
path.addLine(to: CGPoint(x: 0.22328*width, y: 0.52272*height))
path.addCurve(to: CGPoint(x: 0.22825*width, y: 0.50758*height), control1: CGPoint(x: 0.22493*width, y: 0.51767*height), control2: CGPoint(x: 0.22659*width, y: 0.51263*height))
path.addLine(to: CGPoint(x: 0.23821*width, y: 0.47728*height))
path.addCurve(to: CGPoint(x: 0.47933*width, y: 0), control1: CGPoint(x: 0.31803*width, y: 0.23508*height), control2: CGPoint(x: 0.40045*width, y: 0))
path.closeSubpath()
return path
}
}

struct ContentView: View {
var body: some View {
VStack(alignment: .leading, spacing: 0) {
TalkTail().fill(Color.blue)
.frame(width: 22, height: 8)
.padding(.leading, 12)
Text("你好啊!!你好吗,真的好吗好的好的")
.padding(12)
.foregroundColor(Color.white)
.background(.blue)
.cornerRadius(12, antialiased: true)
}

}
}

番外:添加边框(iOS 17+)

如果需要添加边框,我们可以利用ZStack的互相遮挡的关系来实现。iOS17之前的版本需要再在上一层复制一层无边框的填充来保证内部没有边框。这里只以iOS 17+为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct ContentView: View {
var body: some View {
ZStack(alignment: .topLeading) {
TalkTail()
.strokeBorder(.black, lineWidth: 3)
.fill(Color.blue)
.frame(width: 22, height: 8)
.padding(.leading, 20)

Text("你好啊!!你好吗,真的好吗好的好的")
.padding(12)
.foregroundColor(Color.white)
.background(Color.blue) // 蓝色背景
.cornerRadius(12) // 圆角
.padding(4) // 这里添加额外的外部填充来模拟外边框的空间
.background(Color.blue) // 这是外边框的颜色,确保它与内部背景颜色相同,或根据需要进行更改
.cornerRadius(16) // 外部圆角应大于内部文本的圆角
.overlay(
RoundedRectangle(cornerRadius: 16) // 这个圆角应该与最外层的背景相匹配
.stroke(Color.black, lineWidth: 2) // 可以调整颜色和线宽以符合设计要求
)
.padding(.top,7) // 这是原来的顶部填充


TalkTail()
.fill(Color.blue)
.frame(width: 22, height: 8)
.padding(.leading, 20)
}
}
}

实现效果:

实现效果