SwiftUI学习100天(Day43 - 项目 9,第一部分)

原创链接:https://www.hackingwithswift.com/100/swiftui
以下内容仅供学习参考:

今天开始另一个新技术项目,我们将专注于绘图。这是 SwiftUI 的一个领域,你可能认为你不需要太多,但事实并非如此:SwiftUI 使高性能绘图变得如此简单,每个人都可以访问,你会找到可以发挥你的技能的地方在你构建的几乎每个应用程序中。
绘画的另一个好处——这将在本项目的第二部分和第三部分中变得更加明显——有助于营造一种嬉戏感。在接下来的几天里,你会发现只需几行代码就可以创建漂亮的设计,而我在准备我的示例时浪费了无数时间,只是玩玩和玩得开心。
不要相信我的话——著名的荷兰印象派画家文森特梵高说,“我有时认为没有什么比绘画更令人愉快的了。”
我想他是在做某事!
不管怎样,我希望你能以开放的心态对待接下来的几天。也许你想跟随编码(我希望你这样做!),或者你可能只是想坐下来看看有什么可能——无论哪种方式,我想你都会对 SwiftUI 的绘图的智能程度印象深刻!
今天你有四个主题要完成,你将在其中学习路径、形状、可插入形状等。

绘图:简介
在这个技术项目中,我们将仔细研究 SwiftUI 中的绘图,包括创建自定义路径和形状、为你的更改设置动画、解决性能问题等等——这是一个非常大的话题,值得密切关注。
在后台,SwiftUI 使用我们在其他 Apple 框架上使用的相同绘图系统:Core Animation 和 Metal。大多数时候 Core Animation 负责我们的绘图,无论是自定义路径和形状还是 UI 元素TextField
,但当事情真的变得复杂时,我们可以向下移动到 Metal——Apple 的低级框架,它针对复杂的绘图进行了优化。SwiftUI 的一个巧妙特性是这两者几乎可以互换:我们可以通过一个小的改变从 Core Animation 转移到 Metal。
不管怎样,我们有很多内容要讲,所以请创建一个名为 Drawing 的新 App 项目,让我们深入了解……



使用 SwiftUI 创建自定义路径
SwiftUI 为我们提供了一个专门用于绘制自定义形状的类型Path
。它的级别非常低,我的意思是你通常希望将它包装在其他东西中以使其更有用,但由于它是其他工作的基础构建块,我们将从那里开始。
就像颜色、渐变和形状一样,路径本身就是视图。这意味着我们可以像使用文本视图和图像一样使用它们,尽管你会看到它有点笨拙。
让我们从一个简单的形状开始:画一个三角形。有几种创建路径的方法,包括一种接受绘图指令闭包的方法。这个闭包必须接受一个参数,这是绘制的路径。我意识到一开始这可能有点费脑筋,因为我们正在创建一个路径,并且在我们正在传递的路径的初始化器内部绘制路径,但可以这样想:SwiftUI给我们创建了一个空的路径,然后让我们有机会尽可能多地添加它。
Paths 有很多方法可以创建正方形、圆形、弧形和直线形状。对于我们的三角形,我们需要移动到一个起始位置,然后添加如下三行:
我们以前没有用过CGPoint
,但我确实在项目 6 中偷偷对CGSize
做了一个快速参考
。“CG”是 Core Graphics 的缩写,它提供了一系列基本类型,让我们可以参考 X/Y 坐标 ( CGPoint
)、宽度和高度 ( CGSize
) 和矩形框 ( CGRect
)。
当我们的三角形代码运行时,你会看到一个大的黑色三角形。你看到它相对于屏幕的位置取决于你使用的是什么模拟器,这是这些原始路径问题的一部分:我们需要使用精确的坐标,所以如果你想单独使用路径,你要么需要接受GeometryReader
在所有设备上调整大小或使用类似的东西
来相对于它们的容器缩放它们。
我们很快就会看到一个更好的选择,但首先让我们看看为路径着色。一种选择是使用fill()
修饰符,如下所示:
我们还可以使用stroke()
修饰符在路径周围绘制而不是填充它:
不过,这看起来不太正确——我们三角形的底角很漂亮而且很锋利,但顶角坏了。发生这种情况是因为 SwiftUI 确保线条与前后的内容整齐地连接起来,而不仅仅是一系列单独的线条,但我们的最后一行后面没有任何内容,因此无法建立连接。
解决此问题的一种方法是要求 SwiftUI 关闭子路径,这是我们在路径中绘制的形状:
另一种方法是使用 SwiftUI 的专用StrokeStyle
结构,它使我们能够控制每条线应如何连接到它之后的线(线连接),以及当它结束后没有连接时应如何绘制每条线(线帽)。这特别有用,因为 join 和 cap 的选项之一是.round
,它创建了柔和的圆形:
有了它,你就可以删除对 path.closeSubpath()
的调用
,因为它不再需要了。
使用圆角解决了我们边缘粗糙的问题,但是并没有解决固定坐标的问题。为此,我们需要从路径继续前进,看看更复杂的东西:形状。



SwiftUI 中的路径与形状
SwiftUI 支持使用两种略有不同的类型进行自定义绘图:路径和形状。路径是一系列的绘图指令,例如“从这里开始,到这里画一条线,然后在那里画一个圆”,都是使用绝对坐标。相比之下,形状不知道将在哪里使用或将使用多大,而是要求将其自身绘制在给定的矩形内。
有用的是,形状是使用路径构建的,所以一旦你理解了路径,形状就很容易了。此外,就像路径、颜色和渐变一样,形状是视图,这意味着我们可以将它们与文本视图、图像等一起使用。
SwiftUI 实现为具有单一必需方法的Shape
协议:给定以下矩形,你想绘制什么路径?这仍然会像直接使用原始路径一样创建和返回路径,但是因为我们已经掌握了形状将被使用的大小,所以我们确切地知道要绘制我们的路径有多大——我们不再需要依赖固定坐标。
例如,之前我们使用Path
创建了一个三角形
,但我们可以将其包裹在一个形状中以确保它自动占据所有可用空间,如下所示:
使这项工作变得容易得多CGRect
,它提供了有用的属性,例如minX
(矩形中的最小 X 值)、maxX
(矩形中的最大 X 值)和midX
(
minX
和
maxX
之间的中点)。
然后我们可以创建一个精确大小的红色三角形,如下所示:
StrokeStyle
形状还支持用于创建更高级笔画的相同参数:
理解
Path
和Shape
之间区别的关键
是可重用性:路径旨在完成一件特定的事情,而形状具有绘图空间的灵活性,并且还可以接受参数以让我们进一步自定义它们。
为了演示这一点,我们可以创建一个Arc
接受三个参数的形状:开始角度、结束角度以及是否顺时针绘制圆弧。这可能看起来很简单,特别是因为Path
它有一个addArc()
方法,但正如你将看到的那样,它有几个有趣的杂项。
让我们从最简单的弧形开始:
我们现在可以像这样创建一个圆弧:
如果你查看弧线的预览,很可能它看起来与你预期的完全不同。我们要求顺时针旋转从 0 度到 110 度的弧,但我们似乎得到了逆时针旋转的 90 度到 200 度的弧。
这里发生的事情有两个方面:
在 SwiftUI 看来 0 度不是笔直向上,而是笔直向右。
形状从左下角而不是左上角测量它们的坐标,这意味着 SwiftUI 从一个角度到另一个角度是相反的。在我看来,这是非常陌生的。
我们可以使用一种新方法来解决这两个问题,该path(in:)
方法从开始角和结束角减去 90 度,并翻转方向,以便 SwiftUI 的行为符合自然预期的方式:
运行该代码,看看你的想法——对我来说,它产生了一种更自然的工作方式,并巧妙地隔离了 SwiftUI 的绘图行为。



使用 InsettableShape 添加 strokeBorder() 支持
如果你创建一个没有特定大小的形状,它会自动扩展以占据所有可用空间。例如,这将创建一个填满我们视图的圆圈,并给它一个 40 磅的蓝色边框:
仔细观察边框的左右边缘——你是否注意到它们是如何被切断的?
你在这里看到的是 SwiftUI 在形状周围绘制边框的方式的副作用。如果你递给某人一个圆的铅笔轮廓,并要求他们用粗笔在圆上画画,他们会准确地描绘出圆的线——大约一半的笔在线内,一半在线外。这就是 SwiftUI 为我们所做的,但是当我们的形状到达屏幕边缘时,这意味着边框的外部部分最终超出了我们的屏幕边缘。
现在尝试使用这个圆圈:
现在我们把stroke()
改成strokeBorder()
,得到
了一个更好的结果:我们所有的边界都是可见的,因为 Swift 抚摸着圆圈的内部而不是在线的中心。
之前我们构建了一个这样的Arc
形状:
就像Circle
,它会自动占用所有可用空间。但是,这种代码将不起作用:
如果你打开 Xcode 的错误消息,你会看到它说“Value of type 'Arc' has no member 'strokeBorder’(类型 'Arc' 的值没有成员 'strokeBorder')”——也就是说,strokeBorder()
修饰符不存在于Arc
.
SwiftUI的Circle
和我们的Arc
之间有一个很小但很重要的区别
:两者都符合Shape
协议,但Circle
也符合协议:InsettableShape
. 这是一种可以插入(向内缩小)一定量以产生另一种形状的形状。它生成的内嵌形状可以是任何其他类型的不可设置形状,但实际上它应该是相同的形状,只是较小的矩形。
为了让Arc
符合InsettableShape
。
我们需要向它添加一个额外的方法:inset(by:)
。这将被赋予插入量(笔划线宽的一半),并且应该返回一种新的可插入形状——在我们的例子中,这意味着我们应该创建一个插入弧。问题是,我们不知道弧的实际大小,因为path(in:)
还没有被调用。
解决方案其实很简单:如果我们给Arc
形状一个insetAmount
默认为 0 的新属性,我们可以在inset(by:)
调用时添加它。需要时,我们可以调用多次inset(by:)
,例如,如果我们想手动调用一次,然后使用strokeBorder()
.
首先,将这个新属性添加到Arc
:
现在给它这个inset(by:)
方法:
重要提示:这是我们需要使用CGFloat
的极少数地方之一
,这是一种古老的Double
形式
,有点奇怪,它已经进入了 SwiftUI。它也被用于许多其他地方,但大多数情况下 Swift 允许我们使用Double
替代它
!
传入的amount
参数应应用于所有边,在弧的情况下,这意味着我们应该使用它来减小绘制半径。因此,将addArc()
内部调用更改path(in:)
为:
有了这个改变,我们现在可以像这样Arc
符合InsettableShape
:
注意: InsettableShape
实际上是建立在 Shape
之上的
,因此无需在其中添加两者。


