SwiftUI 官方画图实例详细解析
前言
在前面几篇关于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)
)
]
}
SwiftUI 官方画图实例详细解析的更多相关文章
- rtmp官方标准规范详细解析
标准规范学习: rtmp消息结构,包括几个部分: 时戳:4 byte,单位毫秒.超过最大值后会翻转. 长度:消息负载的长度. 类型ID:Type Id 一部分ID范围用于rtmp的控制信令.还有一部 ...
- 小白详细解析C#反射特性实例
套用MSDN上对于反射的定义:反射提供了封装程序集.模块和类型的对象(Type 类型).可以使用反射动态创建类型的实例,将类型绑定到现有对象,或从现有对象获取类型并调用其方法或访问其字段和属性.如果代 ...
- ZT Linux系统环境下的Socket编程详细解析
Linux系统环境下的Socket编程详细解析 来自: http://blog.163.com/jiangh_1982/blog/static/121950520082881457775/ 什么是So ...
- java类生命周期详细解析
(一)详解java类的生命周期 引言 最近有位细心的朋友在阅读笔者的文章时,对java类的生命周期问题有一些疑惑,笔者打开百度搜了一下相关的问题,看到网上的资料很少有把这个问题讲明白的,主要是因为目前 ...
- springmvc 项目完整示例06 日志–log4j 参数详细解析 log4j如何配置
Log4j由三个重要的组件构成: 日志信息的优先级 日志信息的输出目的地 日志信息的输出格式 日志信息的优先级从高到低有ERROR.WARN. INFO.DEBUG,分别用来指定这条日志信息的重要程度 ...
- 转:二十一、详细解析Java中抽象类和接口的区别
转:二十一.详细解析Java中抽象类和接口的区别 http://blog.csdn.net/liujun13579/article/details/7737670 在Java语言中, abstract ...
- 单表扫描,MySQL索引选择不正确 并 详细解析OPTIMIZER_TRACE格式
单表扫描,MySQL索引选择不正确 并 详细解析OPTIMIZER_TRACE格式 一 表结构如下: 万行 CREATE TABLE t_audit_operate_log ( Fid b ...
- 在PHP中使用CURL,“撩”服务器只需几行——php curl详细解析和常见大坑
在PHP中使用CURL,"撩"服务器只需几行--php curl详细解析和常见大坑 七夕啦,作为开发,妹子没得撩就"撩"下服务器吧,妹子有得撩的同学那就左拥妹子 ...
- 微信消息体签名及加解密功能详细解析以及.net实现
原文:微信消息体签名及加解密功能详细解析以及.net实现 前言 微信消息体签名及加密功能已上线,明文传输确实存在安全风险,鉴于微信的用户范围使用之广泛,必定会成为众矢之的.所以大家还是尽快接入安全模式 ...
随机推荐
- 轮廓检测论文解读 | Richer Convolutional Features for Edge Detection | CVPR | 2017
有什么问题可以加作者微信讨论,cyx645016617 上千人的粉丝群已经成立,氛围超好.为大家提供一个遇到问题有可能得到答案的平台. 0 概述 论文名称:"Richer Convoluti ...
- Leetcode——练习
平时没事刷刷Leetcode,还办了个年会员.为了自己150刀.为了自己的大脑投资,从不差钱儿.刷刷题能练习coding,此外看一些别人的优秀的答案,能增长见解.大家共同努力,共勉. 十.Google ...
- Graphql Tutorials(Episode 02)
1.前言 我们在上篇已经了解Graphql的使命以及Graphql的概况,接下来,我们跑起来另外一个Helloworld来开启继续学习. 2.Helloworld(使用Graphql 原生API) 这 ...
- js下 Day15、正则表达式
一.正则表达式简介 什么是正则表达式 正则表达式,也叫规则表达式, 是对字符串操作的一种逻辑公式. 为什么要使用正则? 1.使用极简单的方式,去匹配字符串 2.速度快,代码少 3.在复杂的字符串中快速 ...
- MyBatisPlus-常用注解
一.@TableName 映射数据库的表名 package com.md.entity; import com.baomidou.mybatisplus.annotation.*; import co ...
- 多任务-python实现-gevent(2.1.15)
@ 目录 1.说明 2.代码 关于作者 1.说明 上个博文携程实现的多任务 依然是一个进程,一个线程,只不过执行了不同的代码部分 这里使用gevent,或者greenlet 当gevent执行的时候遇 ...
- 高可用K8S构建3master+3node+keepalived+haproxy
视频地址:https://www.bilibili.com/video/BV1w4411y7Go?p=66 所需安装包在视频评论区 安装准备 系统: CentOS-7-x86_64-Minimal-1 ...
- C#中未在本地计算机上注册“Microsoft.Jet.OLEDB.4.0”提供程序
解决方法 方法一 "设置应用程序池默认属性"/"常规"/"启用32位应用程序",设置为 true. 方法二 生成->配置管理器-> ...
- postgresql 创建分表
划分指的是将逻辑上的一个大表分成一些小的物理上的片.划分有很多益处: 1.在某些情况下查询性能能够显著提升,特别是当那些访问压力大的行在一个分区或者少数几个分区时.划分可以取代索引的主导列.减小索引尺 ...
- 自动化运维工具-Ansible之6-Jinja2模板
自动化运维工具-Ansible之6-Jinja2模板 目录 自动化运维工具-Ansible之6-Jinja2模板 Ansible Jinja2模板概述 Ansible Jinja2模板使用 Ansib ...