欢迎光临散文网 会员登陆 & 注册

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

2023-02-15 12:00 作者:爱上树の蜗牛  | 我要投稿

原创链接: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 值)和midXminXmaxX之间的中点)。

然后我们可以创建一个精确大小的红色三角形,如下所示:

StrokeStyle形状还支持用于创建更高级笔画的相同参数:

理解PathShape之间区别的关键是可重用性:路径旨在完成一件特定的事情,而形状具有绘图空间的灵活性,并且还可以接受参数以让我们进一步自定义它们。

为了演示这一点,我们可以创建一个Arc接受三个参数的形状:开始角度、结束角度以及是否顺时针绘制圆弧。这可能看起来很简单,特别是因为Path它有一个addArc()方法,但正如你将看到的那样,它有几个有趣的杂项。

让我们从最简单的弧形开始:

我们现在可以像这样创建一个圆弧:

如果你查看弧线的预览,很可能它看起来与你预期的完全不同。我们要求顺时针旋转从 0 度到 110 度的弧,但我们似乎得到了逆时针旋转的 90 度到 200 度的弧。

这里发生的事情有两个方面:

  1. 在 SwiftUI 看来 0 度不是笔直向上,而是笔直向右。

  2. 形状从左下角而不是左上角测量它们的坐标,这意味着 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之上的,因此无需在其中添加两者。


SwiftUI学习100天(Day43 - 项目 9,第一部分)的评论 (共 条)

分享到微博请遵守国家法律