版本记录
版本号 | 时间 |
---|---|
V1.0 | 2020.01.10 星期五 |
前言
今天翻阅苹果的API文档,发现多了一个框架SwiftUI,这里我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
1. SwiftUI框架详细解析 (一) —— 基本概览(一)
2. SwiftUI框架详细解析 (二) —— 基于SwiftUI的闪屏页的创建(一)
3. SwiftUI框架详细解析 (三) —— 基于SwiftUI的闪屏页的创建(二)
4. SwiftUI框架详细解析 (四) —— 使用SwiftUI进行苹果登录(一)
5. SwiftUI框架详细解析 (五) —— 使用SwiftUI进行苹果登录(二)
6. SwiftUI框架详细解析 (六) —— 基于SwiftUI的导航的实现(一)
7. SwiftUI框架详细解析 (七) —— 基于SwiftUI的导航的实现(二)
8. SwiftUI框架详细解析 (八) —— 基于SwiftUI的动画的实现(一)
9. SwiftUI框架详细解析 (九) —— 基于SwiftUI的动画的实现(二)
开始
首先看下主要内容:
主要内容:在本SwiftUI教程中,您将学习如何构建各种自定义图表,以有效地为用户建模iOS应用数据。
下面看下写作环境
Swift 5, iOS 13, Xcode 11
下面就是正文了。
图表是向用户显示数据的绝佳方法。 它们帮助用户掌握大量信息中固有的关系。 您可以使用图表来吸引人们注意趋势,弄清原因并帮助用户真正地可视化信息。
在本SwiftUI
教程中,您将学习如何创建各种自定义图表来帮助可视化应用程序的数据。
尽管SwiftUI不提供本机图表库,但它具有丰富的图形功能,可用于构建自定义图表。 在本教程中,您将向应用程序添加图表,以显示大雾山国家公园(Great Smoky Mountains National Park)
及其周围多个气象站的历史气象数据。
1. Why Use a Chart?
查看一些数据可能会很有启发性,但是盯着一长串数字并不是获得洞察力的最佳方法。 数字列表并不能使您更容易了解某个月的温暖程度或确定最干旱的月份。
以图形方式呈现信息时,大多数人都可以更轻松地掌握信息。 图表可以提供旨在告知查看者的数据的图形表示。
Charts vs. Graphs
人们经常互换使用术语“图表”和“图形”,但他们不是同一回事。
图形表示出值之间的任何关系。 一个简单的图形可以显示给定x的y值。 生成的曲线可能很漂亮,但没有提供任何可清晰的信息。
图表应该讲一个故事。 它使观看者更容易理解和解释,从而更好地理解数据。 简而言之,所有图表都是图形,但并非所有图形都是图表。
打开起始工程并运行
该应用程序显示五个站点的数据:
- 北卡罗来纳州切诺基和田纳西州加特林堡
(Cherokee, NC and Gatlinburg, TN)
:穿过公园的主要道路上的两个城市。 -
Newfound Gap
:主要道路跨越的区域。 -
Townsend 5 S
:公园西南部的区域。 -
Mount LeConte
:公园里最高的山脉之一。
该数据集包含每个位置每天的降水,降雪和温度范围。
点按某个位置可显示有关该位置的信息,显示该位置的地图以及三个天气信息选项卡。这三个标签显示了每天的温度范围,每个月的总降水量以及每天有雪的降雪量。
首先,您需要在应用中添加条形图,以显示降水量数据。
Refactoring for Charts
条形图为每个数据点提供一个条形图。每个条的长度代表一个数值,可以水平或垂直延伸以满足您的需求。
展开Tabs
组,然后打开PrecipitationTab.swift
。您会看到一个标准的SwiftUI
List()
,该整数从0
到11
之间循环,代表一年中的月份,并显示每个月的总降雨量。包含的帮助器函数将整数更改为月份名称,并对每个月份的数量求和。
右键单击空的Charts
组,然后选择New File
。选择SwiftUI
视图,然后单击下一步。将新视图命名为PrecipitationChart
。
确保该组设置为Charts
,然后单击Create
。打开新文件。如果“画布”不可见,请从菜单中选择Editor ▸ Canvas
以将其打开,以便查看进度。
在PrecipitationChart
结构体的顶部添加以下代码:
var measurements: [DayInfo]
您可以使用此变量将度量传递到图表中。 现在更新PrecipitationChart_Previews
以传递度量以进行预览。 在这种情况下,您将传递Mt. LeConte
的测量值。
PrecipitationChart(measurements: WeatherInformation()!.stations[2].measurements)
首先,您将在此新视图中复制现有函数。 首先,在measurements
后添加两个帮助函数:
func sumPrecipitation(_ month: Int) -> Double {
self.measurements.filter {
Calendar.current.component(.month, from: $0.date) == month + 1
}.reduce(0, { $0 + $1.precipitation })
}
func monthAbbreviationFromInt(_ month: Int) -> String {
let ma = Calendar.current.shortMonthSymbols
return ma[month]
}
sumPrecipitation(_ :)
使用filter
仅获取传递给函数的月份的度量。 它调整传入的整数(从零开始而不是从1开始)。 reduce
计算这些测量的降水值之和。
monthAbbreviationFromInt(_ :)
获取当前日历的缩写月份符号列表,并返回与传递的整数匹配的月份符号。
更新body
以复制现有列表:
List(0..<12) { month in
Text("\(self.monthAbbreviationFromInt(month)): " +
"\(self.sumPrecipitation(month))\"")
}
打开PrecipitationTab.swift
并删除不再需要的sumPrecipitation(_ :)
和monthAbbreviationFromInt(_ :)
方法。 在body
内部,使用对新视图的调用来替换List
和enclosure
:
PrecipitationChart(measurements: station.measurements)
注意:运行该应用程序时,请确保在选择要查看结果的位置后位于
Precipitation
选项卡上。
Raising the SwiftUI Bar
SwiftUI
包含多个形状视图,其中包括一个矩形形状,可以很好地构建条形图。 打开PrecipitationChart.swift
并将其替换为:
// 1
HStack {
// 2
ForEach(0..<12) { month in
// 3
VStack {
// 4
Spacer()
// 5
Rectangle()
.fill(Color.green)
.frame(width: 20, height: CGFloat(self.sumPrecipitation(month)) * 15.0)
// 6
Text("\(self.monthAbbreviationFromInt(month))")
.font(.footnote)
.frame(height: 20)
}
}
}
以下是逐步进行的细分:
- 1) 您已经创建了垂直条,因此您可以使用
HStack
在设备上水平排列子视图。 - 2) 您可以使用
ForEach
遍历月份。 - 3) 您可以对图表中的每个条使用
VStack
来垂直堆叠元素。 - 4) 您可以在堆栈中的其他视图上指定大小,然后该
Spacer
将展开以填充剩余的空间。实际上,它告诉SwiftUI
将空白放在VStack
的顶部。 - 5) 您使用
Rectangle SwiftUI shape
原语。它创建一个与视图包含的frame
对齐的矩形,并用绿色填充它。您指定宽度和高度恒定的frame
作为月份的总降水量(以英寸为单位)乘以15
。一个月有1英寸的降雨会形成一个20
点宽,15
点长的矩形。一个有七英寸大雨的月份显示为一个矩形,宽20点,长105点。 - 6) 您还为每个条提供标签,在这种情况下为一年的一个月。在堆栈的底部,“文本”视图包含相应月份的缩写名称,并带有
.footnote
字体和一个静态高度。提供静态高度可确保条形底部对齐。
Adding a drop more detail
通过利用SwiftUI
提供的功能,您已经构建了不错的条形图。 外部HStack
均匀分布图表的条形图,这有助于提高可读性。 条形图的高度显示一年中降雨的比例。
但是,该图表并未明确指出确切的降水量。 将以下代码添加到body
中的Spacer
后面以显示该数据:
Text("\(self.sumPrecipitation(month).stringToOneDecimal)")
.font(.footnote)
.rotationEffect(.degrees(-90))
.offset(y: 35)
.zIndex(1)
您已在每个栏中添加了文本视图。 使用Double
类型的扩展方法,它显示该月的总降水量,四舍五入到小数点后一位。 您可以在DoubleExtension.swift
中找到它。
文本视图的字体设置为与月份标签匹配,并逆时针旋转文本90度,使其平行于条形流动。 然后将视图向下偏移35个点,并将其放置在条形图内。
SwiftUI按照阅读顺序渲染视图。 这意味着降雨量通常会位于栅栏后面,因为它占用的空间相同。
将zIndex
属性设置为默认零值以外的值会告诉SwiftUI
覆盖该默认顺序。 将其设置为1
会告诉SwiftUI
使用默认的zIndex
(包括栏)在视图顶部绘制Text
。
构建并运行应用程序以测试此新文本视图。 然后去北卡罗来纳州切诺基站(Cherokee, NC)
,选择降水(precipitation tab)
标签,看一个有趣的bug
。 2018年7月几乎没有下雨,使得条形太短而无法包含其文字。
要修复此错误,您需要通过以下方式替换文本视图中的偏移量,从而对偏移量进行检查:
.offset(y: self.sumPrecipitation(month) < 2.4 ? 0 : 35)
如果一个月的降水量少于2.4
英寸,这将导致条形图长36
点,文本将保留在条形图的顶部。
很好!您现在已经成功地用条形图替换了列表。此图表使查看者可以查看所有原始列表数据,并获得更清晰的指南,以了解每个月的降水差异。
有了降水图,您就可以创建降雪的水平条形图了。
Building a Horizontal Bar Chart
烟山山脉(Smoky Mountains)
是美国东部海拔最高的地区。但是,在那些海拔较高的地方,它们收到的积雪比您预期的要少。
雪的稀缺意味着像降水图那样按月分组的图表将在年初和年底显示变化而中间没有任何变化。取而代之的是,您将使用水平条形图绘制雪状图,该条形图仅显示一年中降雪的日子。
右键单击Xcode
中的Charts
组,然后选择New File
。选择SwiftUI
视图,然后单击Next
。
将新视图命名为SnowfallChart
,并确保该组设置为Charts
。单击Create
并打开新文件。
您需要通过将以下代码添加到该结构的顶部来再次将measurements
传递到该视图:
var measurements: [DayInfo]
您将使用Mount LeConte
进行预览,因为它具有最多的降雪天和最大的降雪量。 将预览更改为:
SnowfallChart(measurements: WeatherInformation()!.stations[2].measurements)
下面,将body
更改为以下:
// 1
List(measurements.filter { $0.snowfall > 0.0 }) { measurement in
HStack {
// 2
Text("\(measurement.dateString)")
.frame(width: 100, alignment: .trailing)
// 3
Rectangle()
.fill(Color.blue)
.frame(width: CGFloat(measurement.snowfall * 10.0), height: 5.0)
// 4
Spacer()
Text("\(measurement.snowfall.stringToOneDecimal)\"")
}
}
以下是分步细分:
- 1) 您创建一个
List
,其中包含每个降雪测量的条目。 - 2) 您从下雪的日期开始每一行。默认情况下,
Text
视图的大小适合其包含的文本,行的宽度却保持变化。应用恒定的宽度可确保条形图从每一行的相同水平位置开始。您将文本对齐到显示降雪量的条形开头旁边的frame
的.trailing
一侧。 - 3) 您将蓝色矩形用作条形柱。由于这是水平而不是垂直的图表,因此请为条形图赋予恒定的高度,并根据积雪量设置宽度。由于视图上的水平空间较少,因此与上一个图表相比,使用更少的点表示每一英寸的降雪。
- 4) 在
Spacer()
填充栏后的空白区域之后,您将显示以英寸为单位的降雪量,再次舍入为十分之一英寸。
返回SnowfallTab.swift
,使用对新视图的调用替换List
及其在body
内部的闭包:
SnowfallChart(measurements: station.measurements)
图表现在显示了一年的降雪量。 看看12月会有特别大的降雪。
Adding Grid Lines
由于降雪量差异很大,因此您可以通过添加网格线进一步阐明图表。 这些是在图表或图形上以恒定值放置的线。 这使观察者更容易测量条的长度。
首先,将SnowfallChart
中Rectangle()
的代码更改为:
ZStack {
Rectangle()
.fill(Color.blue)
.frame(width: CGFloat(measurement.snowfall * 10.0), height: 5.0)
}
ZStack
使您可以在同一空间中叠加多个子视图。 在这种情况下,您将覆盖条形图和网格线。 您将以1英寸的间隔绘制网格线,以达到16英寸的最大尺寸。
在Rectangle
之后的ZStack
中添加以下代码:
ForEach(0..<17) { mark in
Rectangle()
.fill(Color.gray)
.offset(x: CGFloat(mark) * 10.0)
.frame(width: 1.0)
.zIndex(1)
}
在这里,您为每个月的数据绘制了一个用灰色填充的矩形。 offset(x:y :)
修饰符将每条线向右移动适当的量,然后设置一个宽度为1的frame
,将矩形变成一条线。 再次设置Rectangle
的zIndex
,使其显示在条形顶部。
请注意,通过不为frame
设置高度,将扩展为包含frame
的视图的高度。 如果查看当前状态,您会发现有些不对劲。
网格线和条形不一定总是正好对齐。 默认情况下,ZStack
将其子视图对齐在中心,但是您可以通过稍作修改来显式指定子视图的对齐方式。 将声明ZStack
的行更改为:
ZStack(alignment: .leading) {
现在条形和网格都正确显示了
如果您使用许多网格线,则可以通过定期提供视觉提示来帮助查看器。 将对fill(_:style :)
的调用更改为:
.fill(mark % 5 == 0 ? Color.black : Color.gray)
这使用Swift
三元运算符,使用余数运算符将每五个指示器上黑。
现在,您已经获得了创建几个基本图表的经验,现在可以继续创建用于温度数据的更复杂的热图。
Creating a Heat Map
在Charts
组中创建一个新的SwiftUI
视图,并将新视图命名为TemperatureChart
。 打开TemperatureChart.swift
并在结构的开头添加一个用于测量数据的变量。
var measurements: [DayInfo]
更改预览以提供以下信息:
TemperatureChart(measurements: WeatherInformation()!.stations[1].measurements)
该图表应传达全年每个站点的高温和低温。 您将需要使用一些辅助函数来实现这种可视化。 在Measurements
变量之后,将以下方法添加到结构中:
func degreeHeight(_ height: CGFloat, range: Int) -> CGFloat {
height / CGFloat(range)
}
func dayWidth(_ width: CGFloat, count: Int) -> CGFloat {
width / CGFloat(count)
}
该图表将调整以适合视图,而不是使用通过反复试验确定的固定量。 对于图表,这两个函数计算在垂直方向上一度的温度下获取的点,在水平方向上计算一日的水平点。 这两个函数都将维度的大小除以元素数。 结果给出了视图中每个元素要使用的点数。
使用该结果,您可以确定给定日期和温度的视图中的点位置。 在前两个函数之后添加以下两个函数:
func dayOffset(_ date: Date, dWidth: CGFloat) -> CGFloat {
CGFloat(Calendar.current.ordinality(of: .day, in: .year, for: date)!) * dWidth
}
func tempOffset(_ temperature: Double, degreeHeight: CGFloat) -> CGFloat {
CGFloat(temperature + 10) * degreeHeight
}
dayOffset(_:dWidth :)
从传入的日期计算一年中的日期,然后乘以dWidth
参数。 这将计算水平位置以在视图中绘制此测量值。
tempOffset(_:degreeHeight :)
进行类似的计算以获取给定温度的点。 由于温度范围是从-10
度开始的,因此在相乘之前将温度加10。 这会将范围的底部移至零点。
现在将body
更改为以下内容:
// 1
GeometryReader { reader in
ForEach(self.measurements) { measurement in
// 2
Path { p in
// 3
let dWidth = self.dayWidth(reader.size.width, count: 365)
let dHeight = self.degreeHeight(reader.size.height, range: 110)
// 4
let dOffset = self.dayOffset(measurement.date, dWidth: dWidth)
let lowOffset = self.tempOffset(measurement.low, degreeHeight: dHeight)
let highOffset = self.tempOffset(measurement.high, degreeHeight: dHeight)
// 5
p.move(to: CGPoint(x: dOffset, y: reader.size.height - lowOffset))
p.addLine(to: CGPoint(x: dOffset, y: reader.size.height - highOffset))
// 6
}.stroke()
}
}
这里有很多东西,但是函数简化了许多所需的计算。代码的工作方式如下:
- 1) 您创建
GeometryReader
来包装图表。GeometryReader
展开以填充包含它的视图。该闭包还提供了GeometryProxy
参数,该参数包含有关视图大小的信息。
在先前的图表中,您使用了恒定大小来生成看起来正确的东西。现在,您可以使用带有早期函数的这些值来计算图表的最佳值。 - 2) 路径
Path
提供了一种创建二维形状的方法。在这里,您将创建一条垂直线,连接每天的低温和高温。路径在SwiftUI
中还具有一些独特功能,您可以在其中定义变量,从而简化路径点的计算。 - 3) 在这里,您可以使用这两个函数使用
GeometryReader
中的尺寸,以1度温度和1天为单位计算尺寸。您使用的温度范围是110,因为-10
至100
华氏度涵盖了今年数据中所有位置的温度范围。 - 4) 现在,您可以使用这些功能确定日期的垂直点以及高温和低温。
- 5) 这些线将路径移至低温点,并向高温添加线。垂直视图坐标从视图顶部开始,然后向下增加。当您希望点从底部开始并向上移动时,可以从
reader.size.height
中减去垂直位置以获得所需的位置。 - 6)
stroke()
告诉SwiftUI
以当前系统颜色概述您创建的路径。
打开TemperatureTab.swift
并用它替换body
以使用新视图:
VStack {
Text("Temperatures for 2018")
TemperatureChart(measurements: station.measurements)
}.padding()
构建并运行该应用程序。 选择任意位置,然后查看温度tab
。 请注意,该图表可以适应较小的应用内视图和较大的预览。
图表的形状很好地显示了温度的变化,但看起来有些平淡。 接下来,通过将图表转换为热图来使其变得更加有趣,该热图使用颜色更清楚地指示温度。
Adding Heat Map Color
热图使用颜色以图形方式表示值。 天气图通常使用多种颜色来表示温度,低温时从紫色和蓝色阴影开始,低温时向黄色,橙色和红色阴影移动。 计算这些颜色和变化可能涉及一些复杂的数学运算,但此处不涉及。
在SwiftUI
中,您使用渐变表示颜色的过渡。 线性渐变可沿单个轴在两种或多种颜色之间创建平滑的颜色过渡。 在measurements
之后和辅助函数之前,在TemperatureChart.swift
中添加以下内容:
let tempGradient = Gradient(colors: [
.purple,
Color(red: 0, green: 0, blue: 139.0/255.0),
.blue,
Color(red: 30.0/255.0, green: 144.0/255.0, blue: 1.0),
Color(red: 0, green: 191/255.0, blue: 1.0),
Color(red: 135.0/255.0, green: 206.0/255.0, blue: 250.0/255.0),
.green,
.yellow,
.orange,
Color(red: 1.0, green: 140.0/255.0, blue: 0.0),
.red,
Color(red: 139.0/255.0, green: 0.0, blue: 0.0)
])
这定义了一个由12
种颜色组成的渐变,以110
度的温度范围以十度的增量均匀划分,从紫色(-10度)到深红色(100度)。
现在,在主体视图的注释六处将stroke()
更改为:
.stroke(LinearGradient(
gradient: self.tempGradient,
startPoint: UnitPoint(x: 0.0, y: 1.0),
endPoint: UnitPoint(x: 0.0, y: 0.0)))
您可以使用先前定义的渐变色将纯色替换为线性渐变。 使用startPoint
和endPoint
参数,您可以执行几乎神奇的事情。
这两个参数都是UnitPoint
,它们以点无关的方式定义空间,其中0.0
和1.0
标记视图的边缘。 每个方向的零点位于原点:视图的左上角。
您可以将渐变的起点设置在视图的左下角,将端点设置在视图的左上角。 由于它是线性渐变,因此渐变仅在垂直方向上变化。 每种颜色在每个点的整个视图中水平延伸。
将其应用于路径意味着梯度仅显示在stroked
部分:低温和高温之间的范围。
Adding Grid Lines and Labels
现在剩下的就是通过添加网格线(类似于您在条形图中所做的操作)使观看者的视觉看起来更容易一些。 在TemperatureChart.swift
中的现有辅助函数之后添加以下辅助函数:
func tempLabelOffset(_ line: Int, height: CGFloat) -> CGFloat {
height - self.tempOffset(
Double(line * 10),
degreeHeight: self.degreeHeight(height, range: 110))
}
这会将网格划分为十度的块,并传递一个整数,该整数表示该块的起始温度除以十,再加上视图的总高度。 该函数计算适当的垂直偏移。
在body
中的ForEach
循环的右括号后添加以下代码以绘制温度网格线和标签:
// 1
ForEach(-1..<11) { line in
// 2
Group {
Path { path in
let y = self.tempLabelOffset(line, height: reader.size.height)
path.move(to: CGPoint(x: 0, y: y))
path.addLine(to: CGPoint(x: reader.size.width, y: y))
// 4
}.stroke(line == 0 ? Color.black : Color.gray)
// 5
if line >= 0 {
Text("\(line * 10)°")
.offset(x: 10, y: self.tempLabelOffset(line, height: reader.size.height))
}
}
}
这是新代码的细分:
- 1) 您可以在
-1
到10
的范围内循环,代表-10
到100
华氏度的温度。 - 2)
Group
视图在SwiftUI
中起到了一些粘合作用,它结合了其子视图,但不直接呈现元素。 在这里,它允许您在循环内使用Path
和Text()
视图。 - 3) 您可以使用刚添加的函数来计算该线的温度位置。 然后,在该垂直位置从视图的左侧到右侧水平绘制线。
- 4) 您将大多数网格线绘制为灰色。 为了使零度线突出,您可以将其显示为黑色。
- 5) 对于除第一条网格线以外的所有文本,您都将添加一个文本标签。 由于您不再位于
Path
闭包内,因此需要重新计算该线所代表的温度的位置。 您再次使用tempLabelOffset(_:height :)
函数来计算垂直位置。
完成温度后,您需要数月的指示器和标签。 在现有的辅助函数之后添加以下两个辅助函数:
func offsetFirstOfMonth(_ month: Int, width: CGFloat) -> CGFloat {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "M/d/yyyy"
let foM = dateFormatter.date(from: "\(month)/1/2018")!
let dayWidth = self.dayWidth(width, count: 365)
return self.dayOffset(foM, dWidth: dayWidth)
}
func monthAbbreviationFromInt(_ month: Int) -> String {
let ma = Calendar.current.shortMonthSymbols
return ma[month - 1]
}
添加以下代码,将前一个ForEach
循环的右括号后的月份网格线和标签添加到body
的末尾:
ForEach(1..<13) { month in
Group {
Path { path in
let dOffset = self.offsetFirstOfMonth(month, width: reader.size.width)
path.move(to: CGPoint(x: dOffset, y: reader.size.height))
path.addLine(to: CGPoint(x: dOffset, y: 0))
}.stroke(Color.gray)
Text("\(self.monthAbbreviationFromInt(month))")
.font(.subheadline)
.offset(
x: self.offsetFirstOfMonth(month, width: reader.size.width) +
5 * self.dayWidth(reader.size.width, count: 365),
y: reader.size.height - 25.0)
}
}
这里没有您以前没有用过的东西。 与以前一样,Group
会包装网格线和月份标签。 然后,在与每个月的第一天相对应的偏移处绘制一条垂直线。
然后,您将获得每个月的文本缩写,并以相同的偏移量加上一点点的偏移量进行绘制,以将文本移动到月中。 您将获得每个月的文本缩写,并以相同的偏移量加上一点点的偏移量进行绘制,以将文本移至月中。
现在,您的图表可以很好地概述每个位置的温度范围。 每条垂直线的顶部和底部均与颜色相结合,以清楚地显示一年中不同时间的温度。 网格线和标签可帮助观看者确定一年中的某个时间或温度范围。
如果您想了解更多信息,那么对于所有UI内容,Apple人机界面指南都是一个不错的起点。 您将在《人机界面指南》中找到有关Charts的简短部分。 为图表选择颜色时,还应该阅读Color准则。
后记
本篇主要讲述了基于SwiftUI构建各种自定义图表,感兴趣的给个赞或者关注~~~