前言
在前面幾篇關於SwiftUI的文章中,我們用一個具體的基本項目Demo來學習了下SwiftUI,里面包含了常見的一些控件使用以及數據處理和地圖等等,有興趣的小伙伴可以去翻翻以前的文章,在前面總結的時候我有說過要具體說一下這個很有趣的官方示例的,這篇我們就好好的說說這個有意思的圖,我們具體要解析的內容圖如下:

最后出來的UI效果就是上面這個樣子,這個看過SwiftUI官方文檔的朋友一定見過這張圖的,但不知道里面的代碼具體的每一行或者思路是不是都讀懂了,下面我們就認真的分析一下它的實現思路和具體代碼實際的作用。
解析實現
上面這張效果圖的實現我們把它分為三步走的方式,我們具體看看是那三步呢?然后我們就根據這三步具體的分析一下它的代碼和實現。
1、畫出底部的背景。
2、畫單獨的箭頭類型圖。
3、把他們做一個組裝,組裝出我們現在看到的效果實例。
1、底部視圖該怎樣畫呢?
最主要的還是Path的下面兩個方法,
/// Appends a straight line segment from the current point to the specified /// point. public mutating func addLine(to p: CGPoint)
這個方法是 Path 類的划線方法
/// Adds a quadratic Bézier curve to the path, with the specified end point /// and control point. public mutating func addQuadCurve(to p: CGPoint, control cp: CGPoint)
這個方法是 Path 類的畫貝塞爾曲線的方法,通過一個控制點從開始點到結束點畫一條曲線,
在通過這兩個主要方法畫出我們圖形的輪廓之后我們在通過 Shape 的fill 方法給填充一個線性漸變View( LinearGradient )就基本上有了底部視圖的效果。
/// Fills this shape with a color or gradient. /// /// - Parameters: /// - content: The color or gradient to use when filling this shape. /// - style: The style options that determine how the fill renders. /// - Returns: A shape filled with the color or gradient you supply. @inlinable public func fill<S>(_ content: S, style: FillStyle = FillStyle()) -> some View where S : ShapeStyle
那具體的代碼如下面所示,代碼注釋比較多,應該都能理解:
struct BadgeBackground: View {
/// 漸變色的開始和結束的顏色
static let gradientStart = Color(red: 239.0 / 255, green: 120.0 / 255, blue: 221.0 / 255)
static let gradientEnd = Color(red: 239.0 / 255, green: 172.0 / 255, blue: 120.0 / 255)
///
var body: some View {
/// geometry [dʒiˈɒmətri] 幾何學
/// 14之后改了它的對齊方式,向上對齊
GeometryReader { geometry in
Path{path in
/// 保證是個正方形
var width: CGFloat = min(geometry.size.width, geometry.size.height)
let height = width
/// 這個值越大 x的邊距越小 值越小 邊距越大 縮放系數
let xScale: CGFloat = 0.85
/// 定義的是x的邊距
let xOffset = (width * (1.0 - xScale)) / 2.0
width *= xScale
/// 這個點事圖中 1 的位置
path.move(to: CGPoint(
x: xOffset + width * 0.95 ,
y: height * (0.20 + HexagonParameters.adjustment))
)
/// 循環這個數組
HexagonParameters.points.forEach {
/// 從path開始的點到to指定的點添加一段直線
path.addLine(
to:.init(
/// useWidth: (1.00, 1.00, 1.00),
/// xFactors: (0.60, 0.40, 0.50),
x: xOffset + width * $0.useWidth.0 * $0.xFactors.0 ,
y: height * $0.useHeight.0 * $0.yFactors.0
)
)
/// 從開始的點到指定的點添加一個貝塞爾曲線
/// 這里開始的點就是上面添加直線結束的點
path.addQuadCurve(
to: .init(
x: xOffset + width * $0.useWidth.1 * $0.xFactors.1,
y: height * $0.useHeight.1 * $0.yFactors.1
),
control: .init(
x: xOffset + width * $0.useWidth.2 * $0.xFactors.2,
y: height * $0.useHeight.2 * $0.yFactors.2
)
)
}
}
/// 添加一個線性顏色漸變
.fill(LinearGradient(
gradient:.init(colors: [Self.gradientStart, Self.gradientEnd]),
/// 其實從 0.5 ,0 到 0.5 0.6 的漸變就是豎直方向的漸變
startPoint:.init(x: 0.5, y: 0),
endPoint: .init(x: 0.5, y: 0.6)
/// aspect 方向 Ratio 比率,比例
))
.aspectRatio(contentMode: .fit)
}
}
}
這時候的效果圖如下所示:

接着我們在看看箭頭是怎么畫出來的,具體的代碼中是把它分成了上面兩部分來畫,然后通過控制各個點的連接畫出了圖案,這次使用的還是Path的方法,具體的是下面這個:
/// Adds a sequence of connected straight-line segments to the path. public mutating func addLines(_ lines: [CGPoint])
注意區分 addLine 和 addLines,不要把他們搞混淆了!一個傳遞的參數是一個點一個是點的集合,在沒有畫之前你可能會覺得難,但其實真正看代碼還是比較簡單的,最后只需要填充一個你需要的顏色就可以,具體的代碼我們也不細說了,應為比較簡單,如下:
struct BadgeSymbol: View {
static let symbolColor = Color(red: 79.0 / 255, green: 79.0 / 255, blue: 191.0 / 255)
var body: some View {
GeometryReader { geometry in
Path { path in
let width = min(geometry.size.width, geometry.size.height)
let height = width * 0.75
let spacing = width * 0.030
let middle = width / 2
let topWidth = 0.226 * width
let topHeight = 0.488 * height
/// 上面部分
path.addLines([
CGPoint(x: middle, y: spacing),
CGPoint(x: middle - topWidth, y: topHeight - spacing),
CGPoint(x: middle, y: topHeight / 2 + spacing),
CGPoint(x: middle + topWidth, y: topHeight - spacing),
CGPoint(x: middle, y: spacing)
])
/// path 移動到這個點重新開始繪制 其實這句沒啥影響
/// path.move(to: CGPoint(x: middle, y: topHeight / 2 + spacing * 3))
path.addLines([
CGPoint(x: middle - topWidth, y: topHeight + spacing),
CGPoint(x: spacing, y: height - spacing),
CGPoint(x: width - spacing, y: height - spacing),
CGPoint(x: middle + topWidth, y: topHeight + spacing),
CGPoint(x: middle, y: topHeight / 2 + spacing * 3)
])
} .fill(Self.symbolColor)
}
}
}
這時候我們畫的效果如下:

組裝一下
通過上面的分析,我們把需要的基本上就都准備完畢了,然后我們需要的就是把它倆組一個組裝達到我們想要的效果,然后對這個箭頭再做一個簡單的封裝處理,按照上面的例子,需要對每一個箭頭做一個簡單的角度旋轉,旋轉的具體的數據也比較好計算,具體的代碼如下所示:
/// 八個角度設置箭頭
static let rotationCount = 8
///
var badgeSymbols: some View {
ForEach(0..<Badge.rotationCount) { i in
RotatedBadgeSymbol(
/// degrees 度數 八等分制
angle: .degrees(Double(i) / Double(Badge.rotationCount)) * 360.0
)
}
.opacity(0.5) /// opacity 透明度
}
簡單的封裝了下箭頭,代碼:
struct RotatedBadgeSymbol: View {
/// 角度
let angle: Angle
///
var body: some View {
BadgeSymbol()
.padding(-60)
/// 旋轉角度
.rotationEffect(angle, anchor: .bottom)
}
}
最后一步也比較簡單,這種某視圖在另一個制圖之上的需要用到 ZStack ,前面的文章中我們有介紹和使用過 HStack 和 VStack,這次在這里就用到了 VStack,他們之間沒有啥特備大的區別,理解視圖與視圖之間的層級和位置關系就沒問題。
首先肯定是背景在下面,然后箭頭視圖在上面,把它經過一個循環和旋轉角度添加,最后處理一下它的大小和透明底就有了我們需要的效果,具體的代碼如下:
var body: some View {
/// Z 軸 在底部背景之上
ZStack {
BadgeBackground()
GeometryReader { geometry in
self.badgeSymbols
/// 縮放比例
.scaleEffect(1.0 / 4.0, anchor: .top)
/// position 說的是badgeSymbols的位置
/// GeometryReader可以幫助我們獲取父視圖的size
.position(x: geometry.size.width / 2.0, y: (3.0 / 4.0) * geometry.size.height)
}
}
.scaledToFit()
}
最后附一份畫圖時候的點的數據方便大家學習:
struct HexagonParameters {
struct Segment {
let useWidth: (CGFloat, CGFloat, CGFloat)
let xFactors: (CGFloat, CGFloat, CGFloat)
let useHeight: (CGFloat, CGFloat, CGFloat)
let yFactors: (CGFloat, CGFloat, CGFloat)
}
static let adjustment: CGFloat = 0.085
static let points = [
Segment(
useWidth: (1.00, 1.00, 1.00),
xFactors: (0.60, 0.40, 0.50),
useHeight: (1.00, 1.00, 0.00),
yFactors: (0.05, 0.05, 0.00)
),
Segment(
useWidth: (1.00, 1.00, 0.00),
xFactors: (0.05, 0.00, 0.00),
useHeight: (1.00, 1.00, 1.00),
yFactors: (0.20 + adjustment, 0.30 + adjustment, 0.25 + adjustment)
),
Segment(
useWidth: (1.00, 1.00, 0.00),
xFactors: (0.00, 0.05, 0.00),
useHeight: (1.00, 1.00, 1.00),
yFactors: (0.70 - adjustment, 0.80 - adjustment, 0.75 - adjustment)
),
Segment(
useWidth: (1.00, 1.00, 1.00),
xFactors: (0.40, 0.60, 0.50),
useHeight: (1.00, 1.00, 1.00),
yFactors: (0.95, 0.95, 1.00)
),
Segment(
useWidth: (1.00, 1.00, 1.00),
xFactors: (0.95, 1.00, 1.00),
useHeight: (1.00, 1.00, 1.00),
yFactors: (0.80 - adjustment, 0.70 - adjustment, 0.75 - adjustment)
),
Segment(
useWidth: (1.00, 1.00, 1.00),
xFactors: (1.00, 0.95, 1.00),
useHeight: (1.00, 1.00, 1.00),
yFactors: (0.30 + adjustment, 0.20 + adjustment, 0.25 + adjustment)
)
]
}
