SwiftUI 官方画图实例详细解析
前言
在前面几篇关于SwiftUI的文章中,我们用一个具体的基本项目Demo来学习了下SwiftUI,里面包含了常见的一些控件使用以及数据处理和地图等等,有兴趣的小伙伴可以去翻翻以前的文章,在前面总结的时候我有说过要具体说一下这个很有趣的官方示例的,这篇我们就好好的说说这个有意思的图,我们具体要解析的内容图如下:
最后出来的UI效果就是上面这个样子,这个看过SwiftUI官方文档的朋友一定见过这张图的,但不知道里面的代码具体的每一行或者思路是不是都读懂了,下面我们就认真的分析一下它的实现思路和具体代码实际的作用。
解析实现
上面这张效果图的实现我们把它分为三步走的方式,我们具体看看是那三步呢?然后我们就根据这三步具体的分析一下它的代码和实现。
1、画出底部的背景。
2、画单独的箭头类型图。
3、把他们做一个组装,组装出我们现在看到的效果实例。
1、底部视图该怎样画呢?
最主要的还是Path的下面两个方法,
1 2 3 | /// Appends a straight line segment from the current point to the specified /// point. public mutating func addLine(to p: CGPoint) |
这个方法是 Path 类的划线方法
1 2 3 | /// 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 )就基本上有了底部视图的效果。
1 2 3 4 5 6 7 | /// 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 |
那具体的代码如下面所示,代码注释比较多,应该都能理解:
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | 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的方法,具体的是下面这个:
1 2 | /// Adds a sequence of connected straight-line segments to the path. public mutating func addLines ( _ lines : [ CGPoint ]) |
注意区分 addLine 和 addLines,不要把他们搞混淆了!一个传递的参数是一个点一个是点的集合,在没有画之前你可能会觉得难,但其实真正看代码还是比较简单的,最后只需要填充一个你需要的颜色就可以,具体的代码我们也不细说了,应为比较简单,如下:
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 34 35 36 37 38 39 40 41 | 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 ) } } } |
这时候我们画的效果如下:
组装一下
通过上面的分析,我们把需要的基本上就都准备完毕了,然后我们需要的就是把它俩组一个组装达到我们想要的效果,然后对这个箭头再做一个简单的封装处理,按照上面的例子,需要对每一个箭头做一个简单的角度旋转,旋转的具体的数据也比较好计算,具体的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /// 八个角度设置箭头 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 透明度 } |
简单的封装了下箭头,代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct RotatedBadgeSymbol : View { /// 角度 let angle : Angle /// var body : some View { BadgeSymbol () . padding (- 60 ) /// 旋转角度 . rotationEffect ( angle , anchor : . bottom ) } } |
最后一步也比较简单,这种某视图在另一个制图之上的需要用到 ZStack ,前面的文章中我们有介绍和使用过 HStack 和 VStack,这次在这里就用到了 VStack,他们之间没有啥特备大的区别,理解视图与视图之间的层级和位置关系就没问题。
首先肯定是背景在下面,然后箭头视图在上面,把它经过一个循环和旋转角度添加,最后处理一下它的大小和透明底就有了我们需要的效果,具体的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 () } |
最后附一份画图时候的点的数据方便大家学习:
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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 ) ) ] } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!