大多数浏览器和
Developer App 均支持流媒体播放。
-
使用 SwiftUI 构建自定布局
SwiftUI 现在提供强大的工具,以便对您的布局进行升级并排列 App 界面的视图。我们将介绍网格容器,它可以帮助您创建高度可自定的 2D 布局。此外,我们还将说明如何利用布局协议,构建支持完全自定义行为的容器。我们将探索如何在您的布局类型之间创建无缝的动画过渡,并分享创建一流界面的技巧和最佳实践。
资源
相关视频
WWDC23
WWDC22
WWDC20
WWDC19
-
下载
♪ ♪
Paul: 大家好 欢迎收看 使用 SwiftUI 编写自定义布局 我是 Paul 负责开发者文档 SwiftUI 提供了一组丰富的构建模块 你可以使用它们来搭建 你的 App 界面 你们可以组合显示文本 图像 和图形等元素的 内置视图 以创建自定义的复合视图 SwiftUI 提供了布局工具 来将所有元素 进行更复杂的分组
像水平和垂直叠放之类的容器 让你告诉 SwiftUI 放置视图的 相对位置 而视图修饰器可以让你们 对间距和对齐 等内容进行额外的控制 在本次讲座中 我将介绍一些新工具 它们会使一些常见的布局 更容易构建 并且复杂的布局也能实现 在此过程中 我会介绍一些在 SwiftUI 中使用布局的技巧 首先 我将向你们展示 网格家族中的一个新成员 当你们需要展示一组静态视图时 它就非常适合二维布局 接下来我将讨论如何使用 新的布局协议 创建自定义视图容器类型 直接与布局引擎交互 然后我会讲到 ViewThatFits 这是一种容器类型 它自动从视图集合中 选择适合可用空间的视图 最后 我将向你展示 如何使用 AnyLayout 在布局类型之间添加无缝过渡 要查看所有这些新功能的实际效果 我们来看看 我一直在开发的一款 App
近年来 我的一些同事 总是在争论什么是最好的宠物伙伴 我有自己的看法 但我很好奇大家能否达成共识 所以我决定做一个 App 来进行投票 我还想让对皮毛过敏的人也加入 所以我额外加了一个选项 现在 我喜欢用 SwiftUI 做大部分的界面设计 因为使用预览制作原型非常容易 但作为起点 我画了一个 目标设计界面的速写 我预计投票会持续一段时间 所以我想要一个 显示当前排名的排行榜 我会把投票按钮放在底部 而在顶部 我将展示一些 大家投票的图片
好 我要做的第一件事是 搭建排行榜 我们来仔细看看 排行榜是一个二维元素网格 每个参选者都有 列显示名称 百分比和选票计数 在这里 我想实现几个具体的目标 首先 我希望两个文本的列宽 尽可能的压缩 仅需在各种情况下 容纳最宽单元格即可 因为我希望表示百分比的进度视图 获得尽可能多的空间 这个规则同样也会应用在 当数量变大的时候 以及对于说其它语言的朋友 或者是在设备上使用 不同字体大小的用户 其次 我希望名称是向前对齐 但数量是向后对齐的 现在 SwiftUI 已经有了 lazy grids 非常适合滚动内容 当你们有很多视图时 这些容器非常有效 因为它们只加载 可见或即将可见的视图 另一方面 这意味着容器 不能在两个维度上 自动调整单元格的大小
例如 LazyHGrid 可以计算出 每一列的宽度 因为它可以在绘制列之前 测量列中的所有视图 但它不能测量一行中的 每个视图来计算行高 为了实现这一点 lazy grids 需要你们 在初始化时提供其中一个维度的信息
如需详细了解 lazy grids 和 其他现有 SwiftUI 布局容器类型 请参阅 2020 年的 Stacks Grids 和 Outlines 讲座 但就我而言 我不需要滚动 我想让 SwiftUI 计算出每个单元格的高度和宽度 对于这种布局 SwiftUI 现在提供了一个网格视图 与 lazy grid 不同的是 网格一次性加载其所有视图 因此它可以 自动调整其单元格的大小 并在其列和行之间对齐 我们来看看它的代码 这是我以 Grid 形式编写的 排行榜的基本版本 这个特定的网格视图包含 三个 GridRow 实例 在一行中 每个视图对应一列 所以在这个示例中 每一行中的 第一个文本视图对应于第一列 进度视图在第二列 最后一个文本视图对应第三列 请注意 网格为每行和每列 分配了所需的空间 以容纳其最大的视图 因此 第一列文本的列宽足以容纳 最长的名称 但不能更宽 进度指示器等灵活视图 会占用网格提供的尽可能多的空间 在本例中 是为文本列 分配空间后剩余的内容 我想稍微调整一下 但首先 我来创建一个基本的数据模型 以存储投票数
我需要更多的逻辑来管理 以及在网络中共享这个数据 但当我创建接口原型时 只需要这样一个简单的结构 我会遵循 Identifiable 协议 这样我们可以更容易的 在 ForEach 中使用这种类型 遵循 Equatable 协议 可以实现动画过渡
我将创建一组示例数据 以便在我制作原型时在预览中使用 回到网格 我可以创建一个状态变量 并用示例数据进行初始化 使用这些数据 我现在可以使用 ForEach 创建行 请注意 渲染输出没有更改 因为它仍然显示相同的数据 已经很接近了 但我需要调整单元格对齐 现在 所有单元格都是居中对齐的 这是网格的默认设置 但如果你还记得 我希望名称是向前对齐的 数值是向后对齐的 为此 我将使用前沿对齐 来初始化网格 我在这里使用的值 适用于网格中的所有单元格 对前两列很有效 但最后一列呢 要影响单个列的对齐 我可以对该列中的任何一个单元格 应用 gridColumnAlignment 视图修饰器 我将在最后一列的文本视图中 执行此操作 好了 已经成功了 但现在我看一下 我觉得每行之间有个分隔符会更好 如果我只是使用分隔符 向 for-each 添加一个新行 这并不是我想要的 但请注意 这显示了一些有趣的东西 首先 因为分隔符是一个灵活的视图 会导致第一列占用更多的空间 基本上 网格现在为最后一列 提供所需的宽度 并在前两列之间划分剩余空间 其次 对于没有像其他网格行 一样多的视图的网格行 缺少的视图只会在后面的列中 创建空单元格 但我真正想要的是让分隔符 跨越网格的所有列 而 SwiftUI 有一个新的视图修饰器 可以让我做到这一点
通过在视图中 添加 gridCellColumns 修饰器 我可以设置单个视图跨越若干列 在这种情况下 一共3列 实际上 对于视图应该 跨越整个网格的情况 我可以通过在网格行之外 单独编写视图来简化这一点 好吧 我的排行榜状态很不错 接下来让我看看用于投票的按钮
乍一看 这里没有什么特别的东西 不过 我有一个特殊的要求 一方面 我不想为参与者 带来倾向性选择 由于某些选项的按钮更小 但我也不希望 按钮们和它们的容器一样大 在 iPad 或 Mac 上可能会非常大 相反 所有按钮的宽度 都应该等于最宽的按钮文字宽度 那么 如果我尝试用 Hstack创建它 会发生什么呢 我发现每个按钮的大小 都是为了适应它的文本标签 而 HStack 将这些水平地打包在一起 这种默认叠放行为正是你们 在很多情况下想要的 但它并不完全符合 我对这个项目的规范
如需重新了解 SwiftUI 中的 布局基础知识 请参阅 2019 年的讲座 Building Custom Views with SwiftUI 利用那个讲座中的概念 我们来看看这个视图层次结构 看看我可以改变什么 来达到我想要的行为
首先 叠放的容器 会为叠放视图建议一个尺寸 基于此 叠放视图为其三个按钮 建议一个尺寸 然后每个按钮将该尺寸 传递给它的文本标签 文本视图计算它们实际需要的大小 这取决于它们包含的字符串 并将此报告给按钮 该按钮将信息传递回去 叠放视图使用此信息调整自己的尺寸 将按钮放在其空间中 然后向容器报告自己的大小 好了 如果按钮取其文本的大小 如果我将每个文本视图包装在 一个灵活框架中并允许其扩大呢 文本没有改变 但按钮看到一个灵活的子视图 它尽可能多的占用了 HStack 提供的空间 然后叠放视图在它包含的视图之间 平均分配其空间 所以现在按钮都具有相同的大小了 这一点很棒 但它们的实际大小取决于 叠放的容器 叠放视图将会扩展 以填充容器提供的任何空间 这不是我想要的 我真正想要的是一种 自定义叠放类型 它要求每个按钮的理想大小 找到最宽的 然后为每个按钮提供相应的空间 幸运的是 SwiftUI 有一个新工具 可以让我做到这一点 使用布局协议 我可以设定一个自定义布局容器 它直接参与布局过程 其行为根据我的用例量身定制 我们看看它是怎么运行的 再看一下 HStack 我把它改成 EqualWidthHStack 我要对它进行设定 以解决我的特定问题 这种类型将平均分配按钮的宽度 其宽度就是最宽按钮的理想宽度 我将保留灵活的框架 以便文本较窄的按钮 可以扩展以填充叠放提供的空间 但是按钮仍然有一个 我可以测量的理想尺寸 即它们的文本宽度 让我们看看如何实现 MyEqualWidthHStack
我先创建一个遵循 Layout 协议的类型 对于一个基本的布局 我只需要两个必需的方法 让我们为其添加存根 第一个方法是 sizeThatFits 我将在其中计算并报告 我的布局容器有多大
我得到一个建议的视图大小输入 这是一个布局自身容器视图的 大小建议 我可以用 subviews 参数 建议布局的子视图大小
注意 我不能直接访问子视图 相反 子视图输入是一个代理的集合 让我以特定的方式与子视图交互 例如建议大小 每个代理都会根据我提出的建议 返回一个具体的大小 我将收集所有这些响应 并使用它们进行一些计算 然后将等宽 HStack 的具体大小 返回到其容器
我要实现的第二个函数 是 placeSubviews 我将使用这个来告诉我的布局 子视图出现在哪里 此方法采用相同的大小建议 和子视图输入 并且还接受边界输入 它表示我需要放置子视图的区域 边界是一个矩形 它的大小 与我在 sizeThatFits 里要求的大小相符 请记住 视图在 SwiftUI 中 选择自己的大小 因此我的布局容器将获取 它要求的大小 这个区域的原点在左上角 positive X 在右边 positive Y 在下面 即使是在从右到左的语言环境中 你也可以假设 所有的布局计算都是如此 因为框架在沿该方向布置视图时 会自动翻转每个视图的 x 位置 但是 不要假设 矩形的原点值为(0,0) 除此之外 允许非零原点 可以启用布局组合 其中一个布局的 placeSubviews 方法 调用另一个布局的相同方法 为了使其更容易操作 矩形提供了 访问区域重要部分的属性 如每个维度中的最小点 中心点和最大值点
现在 在我继续讲之前 请注意这两个方法都具有的 另一个参数 双向缓存 我可以使用它 来跨方法调用共享中间计算的结果 对于许多简单的布局 你不需要这个 我会暂时忽略缓存 但如果使用 Instruments 分析你的应用 表明需要提高布局代码的效率 可以考虑添加一个 关于这方面的更多信息 请查阅文档
好 我们来实现 sizeThatFits 记住 我想为我的容器返回一个大小 该大小适合所有水平排列 宽度都相同的按钮 首先 我会询问每个按钮的大小 提出一个大小并查看返回的内容 为了衡量子视图的灵活性 我可以使用最小 最大 和理想尺寸的特殊建议 进行多次测量 或者我可以提出一个特定的大小 在本例中 我使用未指定的大小建议 来要求理想大小
然后我将找到所有尺寸中 每个维度上的最大值 在本例中 金鱼按钮设置宽度 而且高度都相同 现在我把它重构成一个方法 因为放置子视图时还需要它 接下来 我需要考虑视图之间的间距 我可以使用固定间距 比如10点 但布局协议让我做得更好 在 SwiftUI 中 所有视图都有间距偏好 这表明视图希望其自身 和下一个视图之间拥有多少空间 这些首选项存储在可用于 布局容器的 ViewSpacing 实例中 视图可能喜欢在不同的边上 使用不同的值 甚至对不同类型的相邻视图 使用不同的值 例如 一个视图 与文本视图之间的空间 可能比它与图像之间的空间 要大或小 并且值也可能因平台而异 如果对你的布局有意义 你可以忽略这些参考值 这基本上是你们使用 自定义间距初始化内置叠放时 发生的情况 但在你自己的布局中尊重这些偏好 是获得自动遵循 Apple 界面指南结果的 好方法 从而与系统其他部分的外观相匹配 现在 每个视图对所有边都有偏好 当我把两个视图放在一起时 同一条边的偏好可能不匹配 为了解决这个问题 内置布局容器 使用两个首选项中较大的一个 我可以在自己的布局中 做同样的事情
子视图代理给出了一种方法 可以沿给定轴设定每个按钮 与其他按钮的首选间距 所以我通过扫描子视图 来创建一个数组的值 并在每个代理的间距实例上 调用距离方法 来获取下一个视图的间距实例 沿水平轴的间距 这个调用考虑了两个视图 在它们的共同边上的偏好 这个数组中的第一个元素告诉我 猫按钮相对于金鱼按钮水平 需要多少空间 下一个元素告诉我金鱼按钮 相对于狗按钮需要多少空间 我将强制数组中的 最后一个元素为零 因为没有任何按钮可进行比较 好 让我把它重构成一个方法 稍后再用 现在我可以结合间距值来找到总间距 并将其与宽度和高度测量值 一起使用 以返回大小值 这是我的布局需要的大小 给定它的子视图的理想大小 和每个子视图的首选间距 我需要实现的另一个函数 是 placeSubview 正如我前面提到的 我得到了容器的边界 以及可以用来引导按钮的 子视图代理的集合 首先 我像在 sizeThatFits 方法中那样 计算 maxSize 和间距数组 因为这里也需要这些值 然后我将创建一个 可用于每个子视图的尺寸建议 这一次基于我希望它们具有的尺寸 而不是它们的理想尺寸 我只需要一个建议 因为我希望所有按钮的大小相同 我会在我的第一个子视图的 水平维度上 找到一个起始位置 作为边界的前缘 加上按钮宽度的一半 请注意 我不依赖原点为零 而是从 minX 值开始 最后 我可以浏览每个子视图代理 并用一个点调用它的放置方法 声明该点在按钮方面代表什么 以及尺寸建议 每次通过循环 我都将水平位置 更新为视图的宽度 加上下一个视图对的间距 以便为下一次迭代做好准备 就是这样 现在让我们看看使用 新的视图布局类型会发生什么
就是这个 我将自己的自定义布局容器实例化 就像我使用内置 HStack 一样 按钮水平排列 宽度相同 现在 我想在这里暂停片刻 谈谈布局协议如何解决过去你们 可能尝试使用 Geometry reader 解决的问题 毕竟 Geometry reader 是测量视图大小的工具 但是在这种情况下 这不是最佳选择 这是因为 geometry reader 旨在测量其容器视图 并将该大小报告给其子视图 然后子视图使用这些信息 来绘制它自己的内容 注意 对于 geometry reader 来说 信息是向下传递的 reader所做的测量对其自身容器的 布局没有影响
这对于绘制随容器缩放的路径 非常有用 geometry reader 告诉路径逻辑 它必须使用多少空间 子视图中的路径逻辑相应地调整 如果容器改变了大小 路径也会改变 因为 geometry reader 会沿着新的大小传递 但是 对于我的按钮来说 我将在这里只关注一个 以便更容易看到 我需要测量文本视图 然后使用它来决定如何设置 以作为文本视图容器的框架 因此 我可以在文本视图的叠加中 添加一个 geometry reader 记住 测量其容器 然后以某种方式将测量数据 发送回框架 在正常的流程之外 但请注意 如果我这样做 就会绕过布局引擎 这可能会导致循环 读取器测量布局并改变框架 可能会改变布局 可能需要再次测量等等 现在要实现这个是有可能的 但如果我不小心 最终可能会导致我的 App 崩溃 因此 不建议使用这个策略 幸运的是 布局协议提供了一种 更好的方法来解决这个问题 允许你们在布局引擎中操作 好 让我们再看看按钮 这里我还想做点别的事 首先 为了更易于观看 我将把按钮重构为 它们自身的子视图 现在 我碰巧知道我的一位同事 在他们的设备上使用 更大的字体 我的 App 自动支持 Dynamic Type 因为我使用了默认字体 所以基本上可以 免费获得正确的行为 让我们看看如果增加字体大小 会发生什么 啊哦 按钮不适合了 请记住 我的自定义叠放 没有限制按钮的宽度 而只是让它们拥有理想的大小 在这种情况下 超过了显示的宽度 那么我能做什么呢 好吧 当视图不适合时 我可以修改布局 以做一些更复杂的事情 同时考虑到布局容器的大小建议 但是对于这种情况 我可以使用新的 ViewThatFits 容器 来为我完成大部分工作 这个新类型从我提供的视图列表中 选择第一个 适合可用空间的视图
通过将我的自定义叠放 包装在一个适合视图的结构中 然后添加相同内容的垂直叠放版本 我可以让 SwiftUI 知道按钮 什么时候需要以不同的方式排列 当然 内置的 VStack 没有我自定义水平叠放 所具有的等宽属性 所以我也实现了自定义叠放的 垂直版本 它与我之前说的非常相似 不同的是它将等宽的项目 放置在垂直轴而不是水平轴上
当然 当我删除动态类型大小覆盖时 它又回到了水平布局 现在 我还需要构建 App 的最后一个部分 那就是顶部的图像 我可以做一些简单的事情 比如只显示一组个人资料图片 但我想可以从中获得一些乐趣 所以我做了另一个自定义布局类型 以圆形排列方式绘制视图 然后根据排名旋转排列 所以这个配置显示金鱼排名第一 其他两个并列第二 然后如果狗放到猫前面 我可以旋转一下来展示 或者我可以通过旋转径向布局 展示一个更真实的结果 使用布局协议创建此布局 实际上非常简单 和之前一样 我只需要两种方法 对于适合的大小 我希望视图填充可用空间 所以我将返回容器视图建议的大小 我将使用 replace-unspecified-dimensions 方法 将建议转换为具体尺寸 如果容器要求理想的大小 该方法会自动处理可能出现的 nil 值 然后在放置子视图方法中 我将根据布局区域的大小 将每个子视图从中间偏移一些半径 并应用取决于视图索引的旋转 作为基线 会将视图 放置在圆周的 0 1 和 三分之二处 为了反映当前的排名 我还将应用一个对所有视图 产生同等影响的偏移量 但是我从哪里获得排名呢 记住 我的布局只能访问子视图代理 而不能访问视图 更不用说数据模型了 原来布局协议还有另一个妙招 它允许我们在每个子视图上存储值 并从布局协议方法中读取值 让我们看看我如何使用它 来传达排名信息 首先 我声明了一个新的类型 遵循 LayoutValueKey 协议 并给它一个默认值 除了在未显式设置视图时 为视图提供值外 默认值还建立关联值的类型 在本例中为整数 然后 我在 View 上 创建了一个方便的方法 使用 layoutValue 视图修饰器 来设置值 现在 在我的视图层次结构中 可以将便利排名修饰器 应用到布局中的视图 在这里 我计算每个宠物的排名 并将其添加到我的径向布局中 对应的宠物头像视图中 最后 回到我的放置子视图方法 我可以添加一些代码 通过使用布局值键作为索引 从每个子视图读取值 我可以使用排名来计算偏移量 我在这里就不赘述这种逻辑了 但它基本上 为任何可能的排名 产生了一个合适的角度 好吧 除了这个 如果三个并列会怎么样呢 无法通过旋转布局 来使所有视图排成一行 所以我必须替换 完全不同的布局逻辑 但是 已经有一种布局类型 可以做到这一点 那就是内置的 HStack 所以我真正想做的是 当检测到三个并列时 转换到 HStack 事实证明 也有一个新工具可以做到这一点 AnyLayout 类型允许你们 将不同的布局 应用于单个视图层次结构 这样 当你从一个布局类型 转换到另一个布局类型时 可以保持视图的标识 所以这里有 我们之前看到的径向布局 我所要做的就是用一个 新的布局类型替换它 取决于是否是三个并列 因为 isThreeWayTie 属性是 从状态派生的 所以 SwiftUI 会注意到它的变化 并识别出它需要重新绘制这个视图 但是由于视图层次结构的结构标识 始终保持不变 SwiftUI 将其视为一个变化的视图 而不是一个新的视图 因此 只需要多一行 我就可以在布局类型之间 创建平滑的过渡 而事实上 通过添加动画视图修饰器 我还可以获得径向布局的 所有不同状态之间的动画 因为径向布局的配置 依赖于相同的数据 这是它实际运行的样子 当我点击不同的按钮来更改 投票计数时 你可以看到头像如何平滑地移动 来反映当前的排名
这些是 SwiftUI 用于组合 你的 App 的视图布局的一些新工具 你可以使用 Grid 类型 构建静态信息的 高度可定制的二维布局 你们可以使用布局协议 来定义自己的通用布局 可重用布局 或高度针对特定用例的布局 当你想让 SwiftUI 从一组视图中 选择最适合可用空间的视图时 可以使用 ViewThatFits 你可以使用 AnyLayout 在布局类型之间无缝切换 感谢你今天加入我的行列 希望你能像我一样 从这些新的布局工具中获得乐趣
-
-
4:28 - Grid with explicit rows
struct Leaderboard: View { var body: some View { Grid { GridRow { Text("Cat") ProgressView(value: 0.5) Text("25") } GridRow { Text("Goldfish") ProgressView(value: 0.2) Text("9") } GridRow { Text("Dog") ProgressView(value: 0.3) Text("16") } } } }
-
5:16 - Data model
struct Pet: Identifiable, Equatable { let type: String var votes: Int = 0 var id: String { type } static var exampleData: [Pet] = [ Pet(type: "Cat", votes: 25), Pet(type: "Goldfish", votes: 9), Pet(type: "Dog", votes: 16) ] }
-
5:41 - Final Leaderboard
struct Leaderboard: View { var pets: [Pet] var totalVotes: Int var body: some View { Grid(alignment: .leading) { ForEach(pets) { pet in GridRow { Text(pet.type) ProgressView( value: Double(pet.votes), total: Double(totalVotes)) Text("\(pet.votes)") .gridColumnAlignment(.trailing) } Divider() } } .padding() } }
-
10:53 - Layout protocol stubs for required methods
struct MyEqualWidthHStack: Layout { func sizeThatFits( proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) -> CGSize { // Return a size. } func placeSubviews( in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) { // Place child views. } }
-
13:44 - Maximum size helper method
private func maxSize(subviews: Subviews) -> CGSize { let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) } let maxSize: CGSize = subviewSizes.reduce(.zero) { currentMax, subviewSize in CGSize( width: max(currentMax.width, subviewSize.width), height: max(currentMax.height, subviewSize.height)) } return maxSize }
-
15:40 - Spacing helper method
private func spacing(subviews: Subviews) -> [CGFloat] { subviews.indices.map { index in guard index < subviews.count - 1 else { return 0 } return subviews[index].spacing.distance( to: subviews[index + 1].spacing, along: .horizontal) } }
-
16:33 - Size that fits implementation
func sizeThatFits( proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) -> CGSize { // Return a size. guard !subviews.isEmpty else { return .zero } let maxSize = maxSize(subviews: subviews) let spacing = spacing(subviews: subviews) let totalSpacing = spacing.reduce(0) { $0 + $1 } return CGSize( width: maxSize.width * CGFloat(subviews.count) + totalSpacing, height: maxSize.height) }
-
16:51 - Place subviews implementation
func placeSubviews( in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) { // Place child views. guard !subviews.isEmpty else { return } let maxSize = maxSize(subviews: subviews) let spacing = spacing(subviews: subviews) let placementProposal = ProposedViewSize(width: maxSize.width, height: maxSize.height) var x = bounds.minX + maxSize.width / 2 for index in subviews.indices { subviews[index].place( at: CGPoint(x: x, y: bounds.midY), anchor: .center, proposal: placementProposal) x += maxSize.width + spacing[index] } }
-
18:07 - Custom layout instantiation
MyEqualWidthHStack { ForEach($pets) { $pet in Button { pet.votes += 1 } label: { Text(pet.type) .frame(maxWidth: .infinity) } .buttonStyle(.bordered) } }
-
20:12 - Buttons helper view
struct Buttons: View { @Binding var pets: [Pet] var body: some View { ForEach($pets) { $pet in Button { pet.votes += 1 } label: { Text(pet.type) .frame(maxWidth: .infinity) } .buttonStyle(.bordered) } } }
-
21:08 - Final voting buttons view
struct StackedButtons: View { @Binding var pets: [Pet] var body: some View { ViewThatFits { MyEqualWidthHStack { Buttons(pets: $pets) } MyEqualWidthVStack { Buttons(pets: $pets) } } } }
-
22:30 - Radial size that fits
func sizeThatFits( proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) -> CGSize { // Take whatever space is offered. return proposal.replacingUnspecifiedDimensions() }
-
22:52 - Radial place subviews without offsets
func placeSubviews( in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) { let radius = min(bounds.size.width, bounds.size.height) / 3.0 let angle = Angle.degrees(360.0 / Double(subviews.count)).radians let offset = 0 // This depends on rank... for (index, subview) in subviews.enumerated() { var point = CGPoint(x: 0, y: -radius) .applying(CGAffineTransform( rotationAngle: angle * Double(index) + offset)) point.x += bounds.midX point.y += bounds.midY subview.place(at: point, anchor: .center, proposal: .unspecified) } }
-
23:42 - Rank value
private struct Rank: LayoutValueKey { static let defaultValue: Int = 1 } extension View { func rank(_ value: Int) -> some View { layoutValue(key: Rank.self, value: value) } }
-
24:21 - Radial place subviews with offsets
func placeSubviews( in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) { let radius = min(bounds.size.width, bounds.size.height) / 3.0 let angle = Angle.degrees(360.0 / Double(subviews.count)).radians let ranks = subviews.map { subview in subview[Rank.self] } let offset = getOffset(ranks) for (index, subview) in subviews.enumerated() { var point = CGPoint(x: 0, y: -radius) .applying(CGAffineTransform( rotationAngle: angle * Double(index) + offset)) point.x += bounds.midX point.y += bounds.midY subview.place(at: point, anchor: .center, proposal: .unspecified) } }
-
25:18 - Final profile view
struct Profile: View { var pets: [Pet] var isThreeWayTie: Bool var body: some View { let layout = isThreeWayTie ? AnyLayout(HStackLayout()) : AnyLayout(MyRadialLayout()) Podium() // Creates the background that shows ranks. .overlay(alignment: .top) { layout { ForEach(pets) { pet in Avatar(pet: pet) .rank(rank(pet)) } } .animation(.default, value: pets) } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。