大多数浏览器和
Developer App 均支持流媒体播放。
-
SwiftUI 的新功能
和我们一起了解 Swift 的更新。我们将向你展示 API 如何通过参数包和宏等功能变得更具扩展性和表现力。我们还将带你了解互操作性的改进,并分享我们如何将 Swift 的性能和安全优势扩展到从 Foundation 到服务器上的大型分布式程序等各个地方。
章节
- 1:05 - SwiftUI in more places
- 10:21 - Simplified data flow
- 18:46 - Extraordinary animations
- 27:18 - Enhanced interactions
资源
相关视频
WWDC23
- 使用 SwiftData 构建 App
- 使用 Xcode 预览构建编程 UI
- 在 App 中为符号添加动画效果
- 在 SwiftUI 中轻松完成高级动画
- 在 SwiftUI 和 UIKit 中创建易于访问的 App
- 在 watchOS 10 平台上进行 App 设计和开发
- 探索 Swift Charts 中的饼图及交互性
- 探索 SwiftUI 中的观察
- 探索 SwiftUI 动画
- 滚动视图进阶
- 用弹簧制作动画
- 认识 StoreKit 与 SwiftUI
- 认识 SwiftData
- 认识 SwiftUI 版 MapKit
- 认识用于空间计算的 SwiftUI
- Swift 的新功能
- SwiftUI 中的检查器:探索细节之美
- SwiftUI 聚焦的秘诀
-
下载
♪ 悦耳的器乐嘻哈 ♪ ♪ Curt Clifton: 大家好 感谢加入我们 我是 Curt SwiftUI 团队的工程师 Jeff Robertson:我是 Jeff 也是 SwiftUI 团队的工程师 很高兴跟大家分享 SwiftUI 的新功能 Curt:你现在可以在更多地方 使用 SwiftUI 了 包括在一个全新的平台上使用! 新数据流类型 极大地简化了域名建模 比以往更增添助力 检查器和表格的改进 用更好的方式展示你的数据 团队还增强了我们的动画 API 让你能为使用你 App 的顾客 打造更多美好体验 在整个框架中 我们通过强化滚动视图、 改善专注和键盘输入 深化如按钮、菜单等控件 的个性化 来提升你提供互动的能力 很高兴跟你分享一些 使用 SwiftUI 的新平台 从头戴式耳机、watchOS 10 到小组件和跨框架集成 SwiftUI 可帮助你 为使用你 App 的人 提供更好的体验 空间计算将 SwiftUI 带入了一个大胆的新未来 如空间容器模式等 全新 3D 功能 丰富的沉浸式空间体验 全新 3D 手势、效果、排版 以及与 RealityKit 的深度融合 从控制中心的主视图等核心部分 到熟悉的 App 如 TV、 Safari 浏览器, 和 Freeform; 再到所有新环境 如 Keynote 讲演中的沉浸式彩排 SwiftUI 都是 这些用户体验的核心 在这个新平台上 使用如窗口组等 熟悉的场景类型来构建窗口组 窗口组的场景呈现为 2D 窗口 加上令人愉悦的高感应 3D 控制 在窗口内选择一个 常规的 SwiftUI 包装器 比如 NavigationSplitView 或选项卡视图 在这些包装器中 你可以像在其他平台一样使用所有的 常规 SwiftUI 控件 想要更深层的体验 可以在你的场景中应用容积样式 空间容器可展示 3D 体验 如在有界空间中的桌游 或建筑模型 可与其他 App 一同显示 人们可在 Notes 中快速记下想法 或在 Keynote 讲演中 更新幻灯片时使用这些内容 使用 Model3D 静态模型 来填充容积 对于带有灯光等效果的 动态、互动式模型 可使用新的 RealityView 如想要全方位沉浸式体验 请将 ImmersiveSpaces 添加到你的 App 中 全新 ImmersiveSpace 场景类型 让你来制定沉浸式空间体验 不论是嵌入到你的周围环境 或是完全沉浸到你的环境中 该系统可迅速消除其他 App 让用户潜入你所创造的世界中 使用混合式沉浸风格的 ImmersiveSpace 将你的 App 与现实世界相连接 将你的内容 与人们周围的环境相结合 将你的 App 元素 锚定在表格和桌面上 用虚拟物体和效果 来延伸并充实现实世界 完全沉浸式风格则更进一步 你的 App 独揽大权 使用空间容器模式中同样的 Model3D 和 RealityView 来构建这种连接 和沉浸式体验 这个新平台上的 SwiftUI 能让你构建神奇的体验 观看 “认识用于空间计算的 SwiftUI” 继续探索这一伟大组合 SwiftUI 可在家构建 房屋填充式体验 但它同样也可以 为 Apple 最便捷的显示屏 构建体验 watchOS 10 提供了全新用户体验 可实时显示消息 快速浏览重点内容 并突出显示器形状和保真度 我们已经更新了整个平台上的 App 来充分发挥全屏色彩和画面的优势 这些设计的根基是几个现有的 SwiftUI 视图 为 watchOS 10 提供新授权 NavigationSplitView 和 NavigationStack 都得到了良好的过渡 TabView 通过数码表冠 转换为新纵向分页风格 SwiftUI 引入了一些新的 API 可帮你将这种全彩风格带入 你的 Apple Watch App 中 全新 containerBackground 修改器 可让你配置这些微妙的背景效果 使推送内容时具有动画效果 你也可将 watchOS 的背景配置为 选项卡视图 而新的多平台工具栏排版 topBarLeading 和 topBarTrailing 以及现有的 bottomBar 让你在 Apple Watch App 中 完美安置这些小细节视图 除了这些新增加的特性 我们也很高兴将一些现有的 API 首次引入 watchOS 包括 DatePicker 和列表中的选项 现在是用这些新功能打磨你的 Apple Watch App 的最好时机 如果你目前还没有 Apple Watch App 现在正当其时 你可在 “为 watchOS 10 设计和构建 App”中 了解设计和工程 如何共同打造这些体验 然后观看 “为 watchOS 10 更新 App” 将这些想法应用于你的工作 watchOS 10 上的智能叠放小组件 可以让使用你的 App 的人 随时查看其信息 SwiftUI 是小组件的核心 无论在哪里都是如此 就像这些新平台一样 iPadOS 17 锁屏上的小组件 是主屏幕小组件的一个很好的补充 大而醒目的小组件在具有待机模式的 iPhone 息屏显示上 表现出众 而 macOS Sonoma 上 的桌面小组件 可让人们在日常工作中 随时了解最新情况 这些小组件已经找到了 适应新环境的方式 而团队还赋予了他们另一个小技巧 我也很高兴地告诉大家 小组件现在可支持互动式控制 小组件中的 Toggle 和 Button 现在可通过 App Intents 激活你的 App 包中设定的代码 你可以通过 SwiftUI 过渡 和动画修改器 对你的小组件进行动画处理 想开始使用这些伟大的新功能 请观看“妥善处理小组件” 和“让小组件栩栩如生” 通过开发和完善你的 新互动式动画小组件 你会喜欢上 Xcode 预览器的 强大功能 预览器通过 Swift 5.9 中的宏 来提供一种优雅的新句法 声明和配置预览、添加小组件类型 并标明测试时间线 Xcode 预览器显示了 当前的小组件状态和时间线 你可看到状态之间的动画 当然 新预览也适用于 常规的 SwiftUI 视图 和 App 现在你可以在 Xcode 内 与 Mac App 的预览 进行互动 观看 “使用 Xcode 预览器 构建程序化 UI” 了解如何利用这些奇妙的新工具 来加速你的 App 和小组件的开发 除了为预览提供动力的宏 Swift 5.9 还带来了 许多其他进步 通过观看“Swift 的全新功能” 了解 Swift 的所有最新动态 SwiftUI 适应新环境的另一种方式是 通过 SwiftUI 对其他 Apple 框架的 特定扩展 有些框架可提供新的或改进的支持 这里我也想强调几个 我认为最值得关注的框架 MapKit 有一个巨大的进步 能使你在 SwiftUI 代码中 获得感知 Apple 奇妙的地图框架能力 只需导入 SwiftUI 和 MapKit 即可使用强大功能 打开地图 添加自定义标记、折线 以及用户位置 配置可用控件 想了解关于在你的 SwiftUI App 中 添加神奇地图的相关信息 请观看 “认识用于 SwiftUI 的 MapKit” 在第二季中 Swift Charts 改善良多 包括滚动图表 选项的内置支持 以及大家一直期待的东西: 全新 SectorMark 的 空心饼图和饼图 想要进一步了解特性 请观看 “探索饼图和 Swift 图表的交互” 为构造吸引顾客 和维持忠诚顾客的体验 你将会喜欢新的应用内购买 和订阅商店功能上的便捷和强大 通过定制营销内容 呈现订阅商店视图 通过配置全出血背景 来配合你的品牌推广 并从各种控件中做相应选择 观看 “认识用于 SwiftUI 的 StoreKit” 来提升你应用内营销的能力 从新平台和小组件 到跨框架集成 到 watchOS 的魅力 SwiftUI 持续推动着 Apple 开发者的经验 Jeff 看到这些所有使用 SwiftUI 的新平台 真是令人激动! Jeff:确实! 我们还有很多改进 在我们所有的平台上都发挥着作用 Curt:你说得对! Jeff:我们应该努力开发一个 涵盖这些改进的 App Curt:我同意! 你对我的想法有什么意见吗? Jeff:关于狗的那个吗? Curt:对! 就像观鸟一样 但是用来观察狗 Jeff:你觉得人们想要一个 观察狗的 App 吗? Curt:肯定想要! 投标书几乎都自己写好了 Jeff:有了我们 百万美元的想法 现在是时候开始 扩建我们的 App 了 因为每个优秀的 App 都始于 优秀的数据模型 那我们先看看 SwiftUI 在处理我们的 App 数据时 都有哪些强大的新功能吧 SwiftUI 最吸引我的一点是 它能让我 把我的 UI 设定为 我的 App 状态函数 很高兴跟大家分享关于 如何用 SwiftUI 设定你的模型类型的最大升级: 全新 Observable 宏 Observable 模型可让你 在数据流中使用 熟悉的 SwiftUI 模式 同时能让你的代码 更简洁、更有性能 这是我建立的模型类 用来存储我外出时遇到的狗的数据 为了让该模型类成为 Observable 我将在其中加入宏 我只需要做这些 不同于 ObservableObject 不需要将属性标记为 Published Observable 模型可轻松融入 现有的 SwiftUI 数据流机制中 我们用 DogCard 视图举个例子 在视图中使用 Observable 时 SwiftUI 将自动建立你读取的属性的 依赖关系 读取时也无需使用属性包装器 所以你的视图代码更加简洁 而该视图正在读取 isFavorite 属性 所以当它变化时 将会重新计算 只有在已读取的属性上会无效 所以无需点击任何不必要的更新 你就可以通过中间视图 传递你的模型 SwiftUI 包括几个 用于设定你的状态 和该状态与你的视图的关系的工具 其中有几个工具是为了能和 ObservableObject 一起使用 当使用 Observable 时 就更简单了 因为它的设计就是能和 State 和 Environment 动态属性一同工作 除了为只读值建模外 Observables 也很适合 用来表示可变状态 比如这个新寻狗表格 该模型是用 State 动态属性设定的 我将其属性的绑定 传递给负责编辑该属性的表单元 最后 Observable 属性与环境 无缝融合 由于我们希望所有 App 的视图都能有一个 获取当前用户的方法 于是我已经把它添加到 我的根视图环境中 然后用户配置文件视图 使用 Environment 动态属性读取该值 我在这里使用模型类作为环境键 但这里也支持自定义环境键 可观看 “使用 SwiftUI 发现 Observation” 学习如何使用 这个强大的新工具的更多内容 我喜欢 Observable 让我写出清晰简洁的代码 它为我的 App 提供了良好开端 但我也想确保数据模型的 任何变化都有持久性 这样我就不会失去对所有喜爱小狗 的跟踪 SwiftData 是一个用于数据建模 和管理的全新框架 它速度快、可扩展 很好地适配 SwiftUI SwiftData 模型 完全由其代码表示 使其可自然适用于 任意 SwiftUI App 为了给 SwiftData 设置我的小狗模型类 我将从使用 Observable 切换到 Model 宏 这是我唯一需要做的改动 除了 SwiftData 提供的持久性之外 模型还能获得使用 Observable 的所有好处 功能非常强大 观察狗狗的 App 的主屏幕上 用滚动堆栈展示了最近遇到的狗 我们来看看 在这里使用 SwiftData 需要做哪些改变 首先 我将在 App 的设定中 添加一个模型容器 并为它提供我的模型类型 然后 在我的视图代码中 我将切换我的狗数组 来使用新的 Query 动态属性 使用 Query 将使 SwiftData 从底层数据库中获取模型值 当数据发生变化时 比如我遇见了一只新的狗 我的视图将会无效 Query 对于大型数据集来说 是非常高效的 且它允许自定义数据的返回方式 例如改变排序顺序 使用我发现狗的日期 这能让 App 的体验更好 SwiftData 对于 在 macOS 和 iOS 上 存储文档数据也非常有效 我决定我想要一个快速的方法 来为我们的 App 制作一些狗牌的视觉处理原型 所以我建立了 这个基于文档的小型 App 用于与 Curt 和其他设计师合作 基于文档的 App 可通过新的初始化定式 使用 swiftData 的 所有新功能 然后 SwiftUI 将使用 SwiftData 对每个文档进行底层存储 并自动设置一个模型包装器 进一步了解关于 SwiftData 及 SwiftData 如何与 SwiftUI 相结合 请观看“认识 SwiftData” 和“使用 SwiftData 构建 App” 除了 SwiftData 的支持 在 iOS 17 或 iPadOS 17 上运行时 DocumentGroup 也有一些新的平台功能 如自动共享和文档重命名支持 以及工具栏中的撤销控制 Inspector 是新的修改器 用于显示关于当前选择 或上下文的细节 这在你的界面中以单独部分呈现 在 macOS 中 Inspector 以尾部侧边栏的形式呈现 在 iPadOS 中为常规尺寸类别 在紧凑的尺寸类别中 将以工作表形式呈现 如需了解关于 Inspector 的所有细节 请观看 “SwiftUI 中的 Inspectors:探索细节” 对话框在 iOS 17 和 macOS Sonoma 中 也有了几个新的自定义 API 我正在用一些新修改器 给我的图像导出对话框 提供些实用信息 比如调整确认按钮的标签 增强的严重度有助于引起 对重要确认对话框的注意 包括已开启的 Toggle 表明 该对话框不应出现在 随后的互动中 最后 添加 HelpLink 可以引导人们进一步了解对话目的 列表和表格是 大多数 App 的关键部分 SwiftUI 在 iOS 17 和 macOS Sonoma 中 通过微调这些带来了 一些新功能和 API 表格支持自定义其纵行顺序 和可见性 当与 SceneStorage 动态属性结合时 这些偏好可在你的 App 运行中 被持久化 你为该表格提供一个 代表自定义状态的值 并给每一列一个独特的稳定标识符 表格现在也有 OutlineGroup 的所有功能 这对于那些适合分层结构的 大型数据集来说 非常好 比如这个数据集将 我最喜欢的狗狗 和它们自豪的父母进行分类 只需使用新的 DisclosureTableRow 表示包含其他行的行 然后用常规方式 建立你的表格其他部分即可 列表或表格内的各部分 已得到了程序化扩展的支持 我在我的 App 的侧边栏中 使用了这个 用来显示最初折叠的位置 但同时仍然允许扩展 新的初始化器需要绑定一个值 这个值反映了该部分的 当前扩展状态 对于较小的数据集 表格也有些新风格可见性 例如如何显示行的背景和列的标题 最后 像我的星级评价这样的 自定义控件也将 从新背景突出环境属性中受益 当背景很突出时 使用不那么突出的前景风格 可以让我的自定义控件 在列表中更适配 除了这些和其他 API 可让你 对列表和表格的外观和特质 进行微调外 我们还对性能做了很大的改进 尤其适用于处理大型数据集 进一步了解关于优化 你自己的 SwiftUI 视图方法 请观看“揭秘 SwiftUI 性能” 从 Observable 到 SwiftData 再到 Inspector 和表格个性化 在你的 App 中处理数据 都将是一种全新体验 Curt:有了 Jeff 建立的数据模型 和表格 我们就有了一个 优秀 App 的骨架 我想用非同寻常的新动画 API 来增加一些炫目效果 如果有一个 Apple TV App 用来观看狗的照片库就太好了 这是我一直在做的一个动画 用来选择当前浏览器 我用新的 KeyframeAnimator API 构建了这个 Keyframe 动画制作软件 能让我同时对多个属性 制造动画 我给该动画制作软件 提供一个包含可动画属性的值 和 Equatable 状态 状态变化会触发我的动画 在第一个闭包中 我建立了一个视图 这个视图由我的可动画属性修改 比如标志的垂直偏移 在第二个闭包中 我设定了这些属性如何随时间变化 例如 第一条轨道设定了我的 verticalTranslation 属性的动画 我使用弹性动画将我的标志 在第一个四分之一秒处 拉低了 30 点 然后我用一个三次曲线 让我的比格犬起跳降落 最后我用弹性动画让狗回到原点 我为我的其他动画属性 设定了其他轨道 所有轨道都平行运行 以打造酷炫的动画效果 了解如何在你的 App 中 充分利用关键帧动画 请观看 “在 SwiftUI 中浏览高级动画” 我还一直在开发一个 Apple Watch App 用来记录我外出跑步时看到的狗 到目前为止都很简单 只需要一个快乐小狗图标 和一个按钮就能记录我看到的狗 我想要点击按钮时 该图标有动画效果 这对于阶段性动画来说很方便 阶段性动画比关键帧动画更简单 它不需要平行轨道 而是通过单一阶段序列逐步执行 这让我能在上一个动画结束后 启动一个新动画 我给这个动画设计软件 一个阶段性的序列 并告诉它 每当我的 sightingCount 变化时 就启动我的动画 然后在第一个闭包中 我根据当前阶段 设置快乐小狗的转动和比例 第二个闭包告诉 SwiftUI 如何在每个阶段 制作动画 我在这使用了一些很酷的弹性动画 我很喜欢这些名字 谁不想要活泼又有活力的动画呢? 在发展阶段 我用的是一个全定制化的弹性动画 该弹性还设置了持续时间和弹力值 更易于描述 你可以在所有需要 SwiftUI 动画的平台 使用所有这些新弹性 弹性动画效果更细致更自然 弹性动画与之前所有动画速度 相匹配 并以逼真的摩擦力设置最终值 现已成为 iOS 17 和相关版本中 目前已有或之后发布的 App 的默认动画 我现在对我的动画很满意 但当我在外跑步时 如果能有些触觉反馈就好了 触觉反馈提供了一种触觉反应 如轻拍 用来吸引注意力并强化行动和事件 我觉得轻拍手腕能让我更有信心 不会错过小狗 有了新感官反馈 API 触觉反馈轻而易得 要使用触觉反馈 我只需附加 sensoryFeedback 修改器 指定我想要的反馈方式及时间 sensoryFeedback 修改器适用于所有 支持触觉反馈的平台 不同平台支持不同的反馈 所以请查看“人机界面指南” 了解你的 App 中 最适用的反馈方式 我还用新的视觉效果修改器 为欢迎屏幕制作动画 视觉效果修改器 让我根据这些狗的位置 更新它们的照片 而且我不需要 GeometryReader 来做这件事 我有一个小型仿造物 可以在屏幕上移动焦点 这个红点就是我说的焦点 我将坐标空间和 显示所有狗的网格连起来 然后在我的 DogCircle 视图中 添加一个视觉效果 这个闭包得到了我要修改的内容 和代理几何体 我将代理几何体传递给辅助方法 来计算比例 我可以使用代理几何体 获得我的网格视图大小 和相对于我的网格视图的 单个狗圈框架 这让我可以计算出任何狗 离模拟焦点的距离 这样我就可以放大聚焦的狗了 有了这些视觉效果 不需要用 GeometryReader 就能完成所有工作 而且它能自动适应不同尺寸
我再和大家分享一个例子 我一直在玩一个功能 就是给我遇到的狗主人 发夸奖小狗的信息 我觉得如果能给小狗命名 让它脱颖而出 十分有趣 现在这很容易了 我可以在另一个文本视图中 直接插入具有前景样式的文本 请注意! 我可以用滑块来调整样式 这是如何做到的呢? 我是这样设定样式的 我将 stripeSpacing 和角度 以及资源目录中的颜色 传给自定义 Metal 着色器 使用 SwiftUI 的 新 ShaderLibrary 我可以将 Metal 着色器函数 直接转化为 SwiftUI 的形状样式 比如这个渲染的 Furdinand 名字中的条纹样式
如果你想旋转 Metal 着色器 只需在你的项目中添加 一个新的 Metal 文件 并在 SwiftUI 中使用 ShaderLibrary 调用你的着色器功能 这个案例中还有个 我想强调的事情 请注意 当我将滑块拉到轨道末端时 符号会有个恰到好处的弹跳 这种效果在 macOS 和 iOS 上的 Slider 中是内置的 你也可以用新的符号效果修改器 将它添加到你自己的符号中 只需运用这个修改器 就能让 SF Symbol 或视图层次中的所有符号产生动画效果 符号支持多种效果 包括带律动和可变色的连续动画 状态可随比例而变化 可呈现可消失 可替换 事件通知可通过弹跳推送 观看 “在 App 中制作动画符号” 进一步了解通过符号效果 投其所好的最佳做法 在结束这个案例前 我想强调最后一个特性 请注意这里的文本单位 以前我可能会用小型大写字母 来达到这种效果 但现在我可以通过对单位 应用新的 textScale 修改器来达到这种效果 如果我和 Jeff 要把 该 App 引入中国市场 即使中文排版中 没有小型大写字母的概念 这些单位也可改变 我们还有另一个工具来 帮助 App 在多语言区域 良好运转 有些语言的字母形式较高 如泰语 当这些语言的文本 被嵌入到 字母形式较短的语言(如英语) 的本地化文本中时 较高的文本可能会 排版拥挤或被剪掉 当我们意识到这个问题时 例如 如果小狗名字 来源于全球众包数据 我们可以应用 typesettingLanguage 修改器 这让 SwiftUI 知道 文本可能需要更多的空间 这些新 API 给我带来很多乐趣 但更重要的是动画要精美 而不是让人们措手不及 进一步了解 SwiftUI 的动画基础 请观看“探索 SwiftUI 动画” 在“使用弹簧制作动画”中 Jacob 将帮你 在自己的设备上建造 良好体验的动画 SwiftUI 的新动画 API 广度令人叹为观止 这里我只介绍了一些皮毛 从动画完成处理程序 到建造全自定义动画 还有更多内容有待发现 我希望你能像我一样 享受这些 API Jeff:我很喜欢所有这些 新动画和效果 将这些 App 融入生活 接下来我们看看 新的交互 API 以进行最后润色 交互是所有 App 的核心 这些只是即将在 iOS 17 和对应版本中 更新的部分 API 我的屏幕上最近遇到的小狗 可以再加点花样 作为最后的润色 我想给我的狗牌添加一些视觉效果 因为狗牌会在我的滚动视图中 来回过渡 滚动过渡修改器与之前 Curt 用于欢迎屏幕的 视觉效果修改器 非常相似 它可以在你滚动视图的项目 中应用效果 只需要几行额外代码 使用缩放和不透明度效果 就能达到我想要的那一点润色效果 我还想在屏幕上添加一个 我最喜欢的狗公园的侧滚动列表 SwiftUI 已增加一些 强大的功能让我添加该列表 在小狗的垂直栈上面 我将放一个狗公园的水平栈 我正在使用新的 containerRelativeFrame 修改器 来确定这些公园卡相对于 水平滚动视图的可见尺寸 计数指的是将屏幕 分成的数量块 跨度指每个视图应该用的小块数量 这简直太棒了 但我想让这些公园卡嵌入这里 新 scrollTargetLayout 修改器 让这一切变得容易 我将把它添加到 LazyHStack 中 并修改滚动视图 使其与目标布局中的视图对齐 除了视图对齐 也可将滚动视图设定为 分页性能 为获得真正的自定义体验 你可用 scrollTargetBehavior 协议 设定你自己的行为 我还认为 当我的混血小狗 在滚动视图的顶端时 应该得到一些表扬 新的 scrollPosition 修改器 与最上面的项目 ID 绑定 可在我滚屏时更新 这样我就知道谁是顶级小狗了 进一步了解关于滚动视图的 重点改善 请观看“超越滚动视图” 图像现在支持高动态范围 的渲染内容 通过应用 allowedDynamicRange 修改器 App 的图片库屏幕中的美丽图片 可以完全保真地显示出来 不过最好少用这种方法 该方法通常适用于图像独立运行时 SwiftUI 编写的应用程序在启用后 就能很好地使用无障碍功能 你也可以用我们正在引入的一些 新的无障碍 API 让它们变得更好 这张照片中勇于冒险的小狗 距离有点远 看不清 所以我应用了 一个放大的手势来允许放大 我还要在我的视图中 添加新的 accessibilityZoomAction 修改器 这能让像旁白这样的辅助技术可以在 不使用手势的情况下访问相同功能 我只需根据行动方向就 更新缩放级别 就能看到她目前在做什么恶作剧了 旁白:缩放图像视图 图像 Jeff:进一步了解 关于 Apple 各平台的 所有新的无障碍功能 请查看 “使用 SwiftUI 和 UIKit 构建可访问的 App” 颜色现在支持使用静态成员句法 来查找你 App 的资源目录中 设定的自定义颜色 这样在使用时就有了编译安全性 永远不会因为打错字而浪费时间 对于我之前展示的文档 App 我在工具栏上添加了一个 包含几个有用行为 的菜单 菜单的顶部是个带新 compactMenu 风格的 ControlGroup 它将其项目显示为水平栈图标 标签颜色选择器被设定为 新调色板风格的选择器 将这种风格与符号图像配合使用 在菜单中会有很好的视觉表现 特别是像这样的菜单 我可以用标签的色调 进行区分 最后 paletteSelectionEffect 修改器 能让我使用符号变体表示挑选器中的 选定项目 有了该菜单 Buddy 就可以为狗牌 选择最喜欢的颜色 即网球黄 现在可以用新的内置形状 来设定有边框的按钮 如圆形和圆角矩形 这些新边框形状样式 适用于 iOS、watchOS 和 macOS macOS 和 iOS 上的按钮 现在可以对拖动动作做出反应 比如我的编辑器中的这个按钮 可以打开一个弹出窗口 新的 springLoadingBehavior 修改器 表明 当一个按钮在其上暂停拖动 或 在 macOS 上强制点击它时 应该触发其动作 tvOS 上的按钮可以很好地利用 新的高亮悬停效果 我把它用在图片库里的图片上 并且只应用于按钮标签的图片部分 来打造一种在平台上 恰到好处的效果 这些按钮也使用了无边框风格 现在在 tvOS 上可以使用 硬件键盘对于促进你 App 中的 常规互动 非常有效 有硬件键盘支持的平台上的 可聚焦视图 可使用 onKeyPress 修改器 直接对任何键盘输入做出反应 修改器取一组键和对应的行动 来匹配该事件 想更多了解相关功能诀窍 请观看 “SwiftUI 焦点标准” 从滚动过渡和行为 到按钮样式和焦点互动 这些新 API 可以帮助你 建造具有丰富功能 和出色风格的 App Curt:我觉得我们的 App 已经取得了很大的进展! Jeff:真的很了不起 Curt:使用这些 API 十分有趣 Jeff:确实如此 Curt:对于 SwiftUI 来说 这是个振奋人心的时刻 有着全新的平台! Jeff:Observable 和 SwiftData 的优雅程度 很好地适配于 SwiftUI Curt:动画的进步 也让人惊叹不已 Jeff:别忘了滚动视图! Curt:看到开发者社区 如何利用这些新 API 总是很振奋人心 Jeff:感谢大家的观看 替我像你的小狗问好! Curt:我们会再接再厉! ♪
-
-
4:49 - watchOS 10
import SwiftUI #if os(watchOS) struct ContainerBackground_Snippet: View { @State private var selection: Int? @State var date = Date() var body: some View { NavigationSplitView { List(selection: $selection) { NavigationLink("Dates", value: -1) NavigationLink("Zero", value: 0) NavigationLink("One", value: 1) NavigationLink("Two", value: 2) } .containerBackground( Color.green.gradient, for: .navigation) } detail: { switch selection { case -1: DatePicker( "Time", selection: $date, displayedComponents: .hourMinuteAndSecond) .containerBackground( Color.yellow.gradient, for: .navigation) case let value?: DetailView(value: value) .containerBackground( Color.blue.gradient, for: .navigation) default: Text("Choose a link") } } } struct DetailView: View { var value: Int var body: some View { Text("\(value)") .font(.largeTitle) } } } #Preview { ContainerBackground_Snippet() } #endif
-
7:01 - Widget Previews
#Preview(as: .systemSmall) { CaffeineTrackerWidget() } timeline: { CaffeineLogEntry.log1 CaffeineLogEntry.log2 CaffeineLogEntry.log3 CaffeineLogEntry.log4 }
-
7:28 - SwiftUI Preview
#Preview("good dog") { ZStack(alignment: .bottom) { Rectangle() .fill(Color.blue.gradient) Text("Riley") .font(.largeTitle) .padding() .background(.thinMaterial, in: .capsule) .padding() } .ignoresSafeArea() }
-
7:33 - Mac Preview
import SwiftUI struct MacPreview_Snippet: View { @State private var drinks = Drink.sampleData @State private var selection: Drink? var body: some View { NavigationSplitView { List(drinks, selection: $selection) { drink in NavigationLink(drink.name, value: drink) } } detail: { if let selection { DrinkCard(drink: selection) } else { ContentUnavailableView( "Select a drink", systemImage: "cup.and.saucer.fill") } } } } struct DrinkCard: View { var drink: Drink var body: some View { ZStack(alignment: .top) { Rectangle() .fill(Color.blue.gradient) Text(drink.name) .padding([.leading, .trailing], 16) .padding([.top, .bottom], 4) .background(.thinMaterial, in: .capsule) .padding() } } } struct Drink: Identifiable, Hashable { let id = UUID() var name: String static let sampleData: [Drink] = [ Drink(name: "Cappuccino"), Drink(name: "Coffee"), Drink(name: "Espresso"), Drink(name: "Latte"), Drink(name: "Macchiato"), ] } #Preview { MacPreview_Snippet() }
-
8:18 - MapKit
import SwiftUI import MapKit struct Maps_Snippet: View { private let location = CLLocationCoordinate2D( latitude: CLLocationDegrees(floatLiteral: 37.3353), longitude: CLLocationDegrees(floatLiteral: -122.0097)) var body: some View { Map { Marker("Pond", coordinate: location) UserAnnotation() } .mapControls { MapUserLocationButton() MapCompass() } } } #Preview { Maps_Snippet() }
-
8:46 - Scrolling Charts
import SwiftUI import Charts struct ScrollingChart_Snippet: View { @State private var scrollPosition = SalesData.last365Days.first! @State private var selection: SalesData? var body: some View { VStack(alignment: .leading) { VStack(alignment: .leading) { Text(""" Scrolled to: \ \(scrollPosition.day, format: .dateTime.day().month().year()) """) Text(""" Selected: \ \(selection?.day ?? .now, format: .dateTime.day().month().year()) """) .opacity(selection != nil ? 1.0 : 0.0) } .padding([.leading, .trailing]) Chart { ForEach(SalesData.last365Days, id: \.day) { BarMark( x: .value("Day", $0.day, unit: .day), y: .value("Sales", $0.sales)) } .foregroundStyle(.blue) } .chartScrollableAxes(.horizontal) .chartXVisibleDomain(length: 3600 * 24 * 30) .chartScrollPosition(x: $scrollPosition) .chartXSelection(value: $selection) } } } struct SalesData: Plottable { var day: Date var sales: Int var primitivePlottable: Date { day } init?(primitivePlottable: Date) { self.day = primitivePlottable self.sales = 0 } init(day: Date, sales: Int) { self.day = day self.sales = sales } static let last365Days: [SalesData] = buildSalesData() private static func buildSalesData() -> [SalesData] { var result: [SalesData] = [] var date = Date.now for _ in 0..<365 { result.append(SalesData(day: date, sales: Int.random(in: 150...250))) date = Calendar.current.date( byAdding: .day, value: -1, to: date)! } return result.reversed() } } #Preview { ScrollingChart_Snippet() }
-
9:00 - Donut and Pie Charts
import SwiftUI import Charts struct DonutChart_Snippet: View { var sales = Bagel.salesData var body: some View { NavigationStack { Chart(sales, id: \.name) { element in SectorMark( angle: .value("Sales", element.sales), innerRadius: .ratio(0.6), angularInset: 1.5) .cornerRadius(5) .foregroundStyle(by: .value("Name", element.name)) } .padding() .navigationTitle("Bagel Sales") .toolbarTitleDisplayMode(.inlineLarge) } } } struct Bagel { var name: String var sales: Int static var salesData: [Bagel] = buildSalesData() static func buildSalesData() -> [Bagel] { [ Bagel(name: "Blueberry", sales: 60), Bagel(name: "Everything", sales: 120), Bagel(name: "Choc. Chip", sales: 40), Bagel(name: "Cin. Raisin", sales: 100), Bagel(name: "Plain", sales: 140), Bagel(name: "Onion", sales: 70), Bagel(name: "Sesame Seed", sales: 110), ] } } #Preview { DonutChart_Snippet() }
-
9:31 - StoreKit
import SwiftUI import StoreKit struct SubscriptionStore_Snippet { var body: some View { SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .lightMarketingContentStyle() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } } .backgroundStyle(.clear) .subscriptionStoreButtonLabel(.multiline) .subscriptionStorePickerItemBackground(.thinMaterial) .storeButton(.visible, for: .redeemCode) } }
-
10:56 - Observable Model
import Foundation import SwiftUI @Observable class Dog: Identifiable { var id = UUID() var name = "" var age = 1 var breed = DogBreed.mutt var owner: Person? = nil } class Person: Identifiable { var id = UUID() var name = "" } enum DogBreed { case mutt }
-
11:22 - Observable View Integration
import Foundation import SwiftUI struct DogCard: View { var dog: Dog var body: some View { DogImage(dog: dog) .overlay(alignment: .bottom) { HStack { Text(dog.name) Spacer() Image(systemName: "heart") .symbolVariant(dog.isFavorite ? .fill : .none) } .font(.headline) .padding(.horizontal, 22) .padding(.vertical, 12) .background(.thinMaterial) } .clipShape(.rect(cornerRadius: 16)) } struct DogImage: View { var dog: Dog var body: some View { Rectangle() .fill(Color.green) .frame(width: 400, height: 400) } } @Observable class Dog: Identifiable { var id = UUID() var name = "" var isFavorite = false } }
-
12:22 - Observable State Integration
import Foundation import SwiftUI struct AddSightingView: View { @State private var model = DogDetails() var body: some View { Form { Section { TextField("Name", text: $model.dogName) DogBreedPicker(selection: $model.dogBreed) } Section { TextField("Location", text: $model.location) } } } struct DogBreedPicker: View { @Binding var selection: DogBreed var body: some View { Picker("Breed", selection: $selection) { ForEach(DogBreed.allCases) { Text($0.rawValue.capitalized) .tag($0.id) } } } } @Observable class DogDetails { var dogName = "" var dogBreed = DogBreed.mutt var location = "" } enum DogBreed: String, CaseIterable, Identifiable { case mutt case husky case beagle var id: Self { self } } } #Preview { AddSightingView() }
-
12:33 - Observable Environment Integration
import SwiftUI @main private struct WhatsNew2023: App { @State private var currentUser: User? var body: some Scene { WindowGroup { ContentView() .environment(currentUser) } } struct ContentView: View { var body: some View { Color.clear } } struct ProfileView: View { @Environment(User.self) private var currentUser: User? var body: some View { if let currentUser { UserDetails(user: currentUser) } else { Button("Log In") { } } } } struct UserDetails: View { var user: User var body: some View { Text("Hello, \(user.name)") } } @Observable class User: Identifiable { var id = UUID() var name = "" } }
-
13:59 - SwiftData Model Container
import Foundation import SwiftUI import SwiftData @main private struct WhatsNew2023: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Dog.self) } struct ContentView: View { var body: some View { Color.clear } } @Model class Dog { var name = "" var age = 1 } }
-
14:05 - SwiftData Query
import Foundation import SwiftUI import SwiftData struct RecentDogsView: View { @Query(sort: \.dateSpotted) private var dogs: [Dog] var body: some View { ScrollView(.vertical) { LazyVStack { ForEach(dogs) { dog in DogCard(dog: dog) } } } } struct DogCard: View { var dog: Dog var body: some View { DogImage(dog: dog) .overlay(alignment: .bottom) { HStack { Text(dog.name) Spacer() Image(systemName: "heart") .symbolVariant(dog.isFavorite ? .fill : .none) } .font(.headline) .padding(.horizontal, 22) .padding(.vertical, 12) .background(.thinMaterial) } .clipShape(.rect(cornerRadius: 16)) } } struct DogImage: View { var dog: Dog var body: some View { Rectangle() .fill(Color.green) .frame(width: 400, height: 400) } } @Model class Dog: Identifiable { var name = "" var isFavorite = false var dateSpotted = Date.now } } #Preview { RecentDogsView() }
-
14:52 - SwiftData DocumentGroup
import SwiftUI import SwiftData import UniformTypeIdentifiers @main private struct WhatsNew2023: App { var body: some Scene { DocumentGroup(editing: DogTag.self, contentType: .dogTag) { ContentView() } } struct ContentView: View { var body: some View { Color.clear } } @Model class DogTag { var text = "" } } extension UTType { static var dogTag: UTType { UTType(exportedAs: "com.apple.SwiftUI.dogTag") } }
-
15:33 - Inspector
import SwiftUI struct InspectorContentView: View { @State private var inspectorPresented = true var body: some View { DogTagEditor() .inspector(isPresented: $inspectorPresented) { DogTagInspector() } } struct DogTagEditor: View { var body: some View { Color.clear } } struct DogTagInspector: View { @State private var fontName = FontName.sfHello @State private var fontColor: Color = .white var body: some View { Form { Section("Text Formatting") { Picker("Font", selection: $fontName) { ForEach(FontName.allCases) { Text($0.name).tag($0) } } ColorPicker("Font Color", selection: $fontColor) } } } } enum FontName: Identifiable, CaseIterable { case sfHello case arial case helvetica var id: Self { self } var name: String { switch self { case .sfHello: return "SF Hello" case .arial: return "Arial" case .helvetica: return "Helvetica" } } } } #Preview { InspectorContentView() }
-
16:10 - File Export Dialog Customization
import Foundation import SwiftUI import UniformTypeIdentifiers struct ExportDialogCustomization: View { @State private var isExporterPresented = true @State private var selectedItem = "" var body: some View { Color.clear .fileExporter( isPresented: $isExporterPresented, item: selectedItem, contentTypes: [.plainText], defaultFilename: "ExportedData.txt") { result in handleDataExport(result: result) } .fileExporterFilenameLabel("Export Data") .fileDialogConfirmationLabel("Export Data") } func handleDataExport(result: Result<URL, Error>) { } struct Data: Codable, Transferable { static var transferRepresentation: some TransferRepresentation { CodableRepresentation(contentType: .plainText) } var text = "Exported Data" } }
-
16:19 - Confirmation Dialog Customization
import Foundation import SwiftUI import UniformTypeIdentifiers struct ConfirmationDialogCustomization: View { @State private var showDeleteDialog = false @AppStorage("dialogIsSuppressed") private var dialogIsSuppressed = false var body: some View { Button("Show Dialog") { if !dialogIsSuppressed { showDeleteDialog = true } } .confirmationDialog( "Are you sure you want to delete the selected dog tag?", isPresented: $showDeleteDialog) { Button("Delete dog tag", role: .destructive) { } HelpLink { } } .dialogSeverity(.critical) .dialogSuppressionToggle(isSuppressed: $dialogIsSuppressed) } }
-
17:01 - Table Column Customization
import SwiftUI struct DogSightingsTable: View { private var dogSightings: [DogSighting] = (1..<50).map { .init( name: "Sighting \($0)", date: .now + Double((Int.random(in: -5..<5) * 86400))) } @SceneStorage("columnCustomization") private var columnCustomization: TableColumnCustomization<DogSighting> @State private var selectedSighting: DogSighting.ID? var body: some View { Table( dogSightings, selection: $selectedSighting, columnCustomization: $columnCustomization) { TableColumn("Dog Name", value: \.name) .customizationID("name") TableColumn("Date") { Text($0.date, style: .date) } .customizationID("date") } } struct DogSighting: Identifiable { var id = UUID() var name: String var date: Date } }
-
17:22 - DisclosureTableRow
import SwiftUI struct DogGenealogyTable: View { private static let dogToys = ["🦴", "🧸", "👟", "🎾", "🥏"] private var dogs: [DogGenealogy] = (1..<10).map { .init( name: "Parent \($0)", age: Int.random(in: 8..<12) * 7, favoriteToy: dogToys[Int.random(in: 0..<5)], children: (1..<10).map { .init( name: "Child \($0)", age: Int.random(in: 1..<5) * 7, favoriteToy: dogToys[Int.random(in: 0..<5)]) } ) } var body: some View { Table(of: DogGenealogy.self) { TableColumn("Dog Name", value: \.name) TableColumn("Age (Dog Years)") { Text($0.age, format: .number) } TableColumn("Favorite Toy", value: \.favoriteToy) } rows: { ForEach(dogs) { dog in DisclosureTableRow(dog) { ForEach(dog.children) { child in TableRow(child) } } } } } struct DogGenealogy: Identifiable { var id = UUID() var name: String var age: Int var favoriteToy: String var children: [DogGenealogy] = [] } }
-
17:45 - Programmatic Section Expansion
import SwiftUI struct ExpandableSectionsView: View { @State private var selection: Int? var body: some View { NavigationSplitView { Sidebar(selection: $selection) } detail: { Detail(selection: selection) } } struct Sidebar: View { @Binding var selection: Int? @State private var isSection1Expanded = true @State private var isSection2Expanded = false var body: some View { List(selection: $selection) { Section("First Section", isExpanded: $isSection1Expanded) { ForEach(1..<6, id: \.self) { Text("Item \($0)") } } Section("Second Section", isExpanded: $isSection2Expanded) { ForEach(6..<11, id: \.self) { Text("Item \($0)") } } } } } struct Detail: View { var selection: Int? var body: some View { Text(selection.map { "Selection: \($0)" } ?? "No Selection") } } }
-
17:54 - Table Display Customization And Background Prominence
import SwiftUI struct TableDisplayCustomizationView: View { private var dogSightings: [DogSighting] = (1..<10).map { .init( name: "Dog Breed \($0)", sightings: Int.random(in: 1..<5), rating: Int.random(in: 1..<6)) } @State private var selection: DogSighting.ID? var body: some View { Table(dogSightings, selection: $selection) { TableColumn("Name", value: \.name) TableColumn("Sightings") { Text($0.sightings, format: .number) } TableColumn("Rating") { StarRating(rating: $0.rating) .foregroundStyle(.starRatingForeground) } } .alternatingRowBackgrounds(.disabled) .tableColumnHeaders(.hidden) } struct StarRating: View { var rating: Int var body: some View { HStack(spacing: 1) { ForEach(1...5, id: \.self) { n in Image(systemName: "star") .symbolVariant(n <= rating ? .fill : .none) } } .imageScale(.small) } } struct StarRatingForegroundStyle: ShapeStyle { func resolve(in environment: EnvironmentValues) -> some ShapeStyle { if environment.backgroundProminence == .increased { return AnyShapeStyle(.secondary) } else { return AnyShapeStyle(.yellow) } } } struct DogSighting: Identifiable { var id = UUID() var name: String var sightings: Int var rating: Int } } extension ShapeStyle where Self == TableDisplayCustomizationView.StarRatingForegroundStyle { static var starRatingForeground: TableDisplayCustomizationView.StarRatingForegroundStyle { .init() } }
-
19:19 - Keyframe Animator
import SwiftUI struct KeyframeAnimator_Snippet: View { var body: some View { Logo(color: .blue) Text("Tap the shape") } } struct Logo: View { var color: Color @State private var runPlan = 0 var body: some View { VStack(spacing: 100) { KeyframeAnimator( initialValue: AnimationValues(), trigger: runPlan ) { values in LogoField(color: color) .scaleEffect(values.scale) .rotationEffect(values.rotation, anchor: .bottom) .offset(y: values.verticalTranslation) .frame(width: 240, height: 240) } keyframes: { _ in KeyframeTrack(\.verticalTranslation) { SpringKeyframe(30, duration: 0.25, spring: .smooth) CubicKeyframe(-120, duration: 0.3) CubicKeyframe(-120, duration: 0.5) CubicKeyframe(10, duration: 0.3) SpringKeyframe(0, spring: .bouncy) } KeyframeTrack(\.scale) { SpringKeyframe(0.98, duration: 0.25, spring: .smooth) SpringKeyframe(1.2, duration: 0.5, spring: .smooth) SpringKeyframe(1.0, spring: .bouncy) } KeyframeTrack(\.rotation) { LinearKeyframe(Angle(degrees:0), duration: 0.45) CubicKeyframe(Angle(degrees: 0), duration: 0.1) CubicKeyframe(Angle(degrees: -15), duration: 0.1) CubicKeyframe(Angle(degrees: 15), duration: 0.1) CubicKeyframe(Angle(degrees: -15), duration: 0.1) SpringKeyframe(Angle(degrees: 0), spring: .bouncy) } } .onTapGesture { runPlan += 1 } } } struct AnimationValues { var scale = 1.0 var verticalTranslation = 0.0 var rotation = Angle(degrees: 0.0) } struct LogoField: View { var color: Color var body: some View { ZStack(alignment: .bottom) { RoundedRectangle(cornerRadius: 48) .fill(.shadow(.drop(radius: 5))) .fill(color.gradient) } } } } #Preview { KeyframeAnimator_Snippet() }
-
20:35 - Phase Animator
import SwiftUI struct PhaseAnimator_Snippet: View { @State private var sightingCount = 0 var body: some View { VStack { Spacer() HappyDog() .phaseAnimator( SightingPhases.allCases, trigger: sightingCount ) { content, phase in content .rotationEffect(phase.rotation) .scaleEffect(phase.scale) } animation: { phase in switch phase { case .shrink: .snappy(duration: 0.1) case .spin: .bouncy case .grow: .spring( duration: 0.2, bounce: 0.1, blendDuration: 0.1) case .reset: .linear(duration: 0.0) } } .sensoryFeedback(.increase, trigger: sightingCount) Spacer() Button("There’s One!", action: recordSighting) .zIndex(-1.0) } } func recordSighting() { sightingCount += 1 } enum SightingPhases: CaseIterable { case reset case shrink case spin case grow var rotation: Angle { switch self { case .spin, .grow: Angle(degrees: 360) default: Angle(degrees: 0) } } var scale: Double { switch self { case .reset: 1.0 case .shrink: 0.75 case .spin: 0.85 case .grow: 1.0 } } } } struct HappyDog: View { var body: some View { ZStack(alignment: .center) { Rectangle() .fill(.blue.gradient) Text("🐶") .font(.system(size: 58)) } .clipShape(.rect(cornerRadius: 12)) .frame(width: 96, height: 96) } } #Preview { PhaseAnimator_Snippet() }
-
22:27 - Haptic Feedback
https://developer.apple.com/design/human-interface-guidelines/playing-haptics
-
22:35 - Visual Effects
import SwiftUI struct VisualEffects_Snippet: View { @State private var dogs: [Dog] = manySampleDogs @StateObject private var simulation = Simulation() @State private var showFocalPoint = false var body: some View { ScrollView { LazyVGrid(columns: columns, spacing: itemSpacing) { ForEach(dogs) { dog in DogCircle(dog: dog, focalPoint: simulation.point) } } .opacity(showFocalPoint ? 0.3 : 1.0) .overlay(alignment: .topLeading) { DebugDot(focalPoint: simulation.point) .opacity(showFocalPoint ? 1.0 : 0.0) } .compositingGroup() } .coordinateSpace(.dogGrid) .onTapGesture { withAnimation { showFocalPoint.toggle() } } } var columns: [GridItem] { [GridItem( .adaptive( minimum: imageLength, maximum: imageLength ), spacing: itemSpacing )] } struct DebugDot: View { var focalPoint: CGPoint var body: some View { Circle() .fill(.red) .frame(width: 10, height: 10) .visualEffect { content, proxy in content.offset(position(in: proxy)) } } func position(in proxy: GeometryProxy) -> CGSize { guard let backgroundSize = proxy.bounds(of: .dogGrid)?.size else { return .zero } let frame = proxy.frame(in: .dogGrid) let center = CGPoint( x: (frame.minX + frame.maxX) / 2.0, y: (frame.minY + frame.maxY) / 2.0 ) let xOffset = focalPoint.x * backgroundSize.width - center.x let yOffset = focalPoint.y * backgroundSize.height - center.y return CGSize(width: xOffset, height: yOffset) } } /// A self-updating simulation of a point bouncing inside a unit square. @MainActor class Simulation: ObservableObject { @Published var point = CGPoint( x: Double.random(in: 0.001..<1.0), y: Double.random(in: 0.001..<1.0) ) private var velocity = CGVector(dx: 0.0048, dy: 0.0028) private var updateTask: Task<Void, Never>? private var isUpdating = true init() { updateTask = Task.detached { do { while true { try await Task.sleep(for: .milliseconds(16)) await self.updateLocation() } } catch { // fallthrough and exit } } } func toggle() { isUpdating.toggle() } private func updateLocation() { guard isUpdating else { return } point.x += velocity.dx point.y += velocity.dy if point.x < 0 || point.x >= 1.0 { velocity.dx *= -1 point.x += 2 * velocity.dx } if point.y < 0 || point.y >= 1.0 { velocity.dy *= -1 point.y += 2 * velocity.dy } } } } extension CoordinateSpaceProtocol where Self == NamedCoordinateSpace { fileprivate static var dogGrid: Self { .named("dogGrid") } } private func magnitude(dx: Double, dy: Double) -> Double { sqrt(dx * dx + dy * dy) } private struct DogCircle: View { var dog: Dog var focalPoint: CGPoint var body: some View { ZStack { DogImage(dog: dog) .visualEffect { content, geometry in content .scaleEffect(contentScale(in: geometry)) .saturation(contentSaturation(in: geometry)) .opacity(contentOpacity(in: geometry)) } } } } private struct DogImage: View { var dog: Dog var body: some View { Circle() .fill(.shadow(.drop( color: .black.opacity(0.4), radius: 4, x: 0, y: 2))) .fill(dog.color) .strokeBorder(.secondary, lineWidth: 3) .frame(width: imageLength, height: imageLength) } } extension DogCircle { func contentScale(in geometry: GeometryProxy) -> Double { guard let gridSize = geometry.bounds(of: .dogGrid)?.size else { return 0 } let frame = geometry.frame(in: .dogGrid) let center = CGPoint(x: (frame.minX + frame.maxX) / 2.0, y: (frame.minY + frame.maxY) / 2.0) let xOffset = focalPoint.x * gridSize.width - center.x let yOffset = focalPoint.y * gridSize.height - center.y let unitMagnitude = magnitude(dx: xOffset, dy: yOffset) / magnitude(dx: gridSize.width, dy: gridSize.height) if unitMagnitude < 0.2 { let d = 3 * (unitMagnitude - 0.2) return 1.0 + 1.2 * d * d * (1 + d) } else { return 1.0 } } func contentOpacity(in geometry: GeometryProxy) -> Double { opacity(for: displacement(in: geometry)) } func contentSaturation(in geometry: GeometryProxy) -> Double { opacity(for: displacement(in: geometry)) } func opacity(for displacement: Double) -> Double { if displacement < 0.3 { return 1.0 } else { return 1.0 - (displacement - 0.3) * 1.43 } } func displacement(in proxy: GeometryProxy) -> Double { guard let backgroundSize = proxy.bounds(of: .dogGrid)?.size else { return 0 } let frame = proxy.frame(in: .dogGrid) let center = CGPoint( x: (frame.minX + frame.maxX) / 2.0, y: (frame.minY + frame.maxY) / 2.0 ) let xOffset = focalPoint.x * backgroundSize.width - center.x let yOffset = focalPoint.y * backgroundSize.height - center.y return magnitude(dx: xOffset, dy: yOffset) / magnitude( dx: backgroundSize.width, dy: backgroundSize.height) } } private struct Dog: Identifiable { let id = UUID() var color: Color } private let imageLength = 100.0 private let itemSpacing = 20.0 private let possibleColors: [Color] = [.red, .orange, .yellow, .green, .blue, .indigo, .purple] private let manySampleDogs: [Dog] = (0..<100).map { Dog(color: possibleColors[$0 % possibleColors.count]) } #Preview { VisualEffects_Snippet() }
-
23:39 - Metal Shader
import SwiftUI struct ShaderUse_Snippet: View { @State private var stripeSpacing: Float = 10.0 @State private var stripeAngle: Float = 0.0 var body: some View { VStack { Text( """ \( Text("Furdinand") .foregroundStyle(stripes) .fontWidth(.expanded) ) \ is a good dog! """ ) .font(.system(size: 56, weight: .heavy).width(.condensed)) .lineLimit(...4) .multilineTextAlignment(.center) Spacer() controls Spacer() } .padding() } var stripes: Shader { ShaderLibrary.angledFill( .float(stripeSpacing), .float(stripeAngle), .color(.blue) ) } @ViewBuilder var controls: some View { Grid(alignment: .trailing) { GridRow { spacingSlider ZStack(alignment: .trailing) { Text("50.0 PX").hidden() // maintains size Text(""" \(stripeSpacing, format: .number.precision(.fractionLength(1))) \ \(Text("PX").textScale(.secondary)) """) .foregroundStyle(.secondary) } } GridRow { angleSlider ZStack(alignment: .trailing) { Text("-0.09π RAD").hidden() // maintains size Text(""" \(stripeAngle / .pi, format: .number.precision(.fractionLength(2)))π \ \(Text("RAD").textScale(.secondary)) """) .foregroundStyle(.secondary) } } } .labelsHidden() } @ViewBuilder var spacingSlider: some View { Slider( value: $stripeSpacing, in: Float(10.0)...50.0) { Text("Spacing") } minimumValueLabel: { Image( systemName: "arrow.down.forward.and.arrow.up.backward") } maximumValueLabel: { Image( systemName: "arrow.up.backward.and.arrow.down.forward") } } @ViewBuilder var angleSlider: some View { Slider( value: $stripeAngle, in: (-.pi / 2)...(.pi / 2)) { Text("Angle") } minimumValueLabel: { Image( systemName: "arrow.clockwise") } maximumValueLabel: { Image( systemName: "arrow.counterclockwise") } } } // NOTE: create a .metal file in your project and add the following to it: /* #include <metal_stdlib> using namespace metal; [[ stitchable ]] half4 angledFill(float2 position, float width, float angle, half4 color) { float pMagnitude = sqrt(position.x * position.x + position.y * position.y); float pAngle = angle + (position.x == 0.0f ? (M_PI_F / 2.0f) : atan(position.y / position.x)); float rotatedX = pMagnitude * cos(pAngle); float rotatedY = pMagnitude * sin(pAngle); return (color + color * fmod(abs(rotatedX + rotatedY), width) / width) / 2; } */ #Preview { ShaderUse_Snippet() }
-
25:01 - Symbol Effect
import SwiftUI struct SymbolEffect_Snippet: View { @State private var downloadCount = -2 @State private var isPaused = false var scaleUpActive: Bool { (downloadCount % 2) == 0 } var isHidden: Bool { scaleUpActive } var isShown: Bool { scaleUpActive } var isPlaying: Bool { scaleUpActive } var body: some View { ScrollView { VStack(spacing: 48) { Image(systemName: "rectangle.inset.filled.and.person.filled") .symbolEffect(.pulse) .frame(maxWidth: .infinity) Image(systemName: "arrow.down.circle") .symbolEffect(.bounce, value: downloadCount) Image(systemName: "wifi") .symbolEffect(.variableColor.iterative.reversing) Image(systemName: "bubble.left.and.bubble.right.fill") .symbolEffect(.scale.up, isActive: scaleUpActive) Image(systemName: "cloud.sun.rain.fill") .symbolEffect(.disappear, isActive: isHidden) Image(systemName: isPlaying ? "play.fill" : "pause.fill") .contentTransition(.symbolEffect(.replace.downUp)) } .padding() } .font(.system(size: 64)) .frame(maxWidth: .infinity) .symbolRenderingMode(.multicolor) .preferredColorScheme(.dark) .task { do { while true { try await Task.sleep(for: .milliseconds(1500)) if !isPaused { downloadCount += 1 } } } catch { print("exiting") } } } } #Preview { SymbolEffect_Snippet() }
-
25:35 - Metal Shader (cont.)
import SwiftUI struct ShaderUse_Snippet: View { @State private var stripeSpacing: Float = 10.0 @State private var stripeAngle: Float = 0.0 var body: some View { VStack { Text( """ \( Text("Furdinand") .foregroundStyle(stripes) .fontWidth(.expanded) ) \ is a good dog! """ ) .font(.system(size: 56, weight: .heavy).width(.condensed)) .lineLimit(...4) .multilineTextAlignment(.center) Spacer() controls Spacer() } .padding() } var stripes: Shader { ShaderLibrary.angledFill( .float(stripeSpacing), .float(stripeAngle), .color(.blue) ) } @ViewBuilder var controls: some View { Grid(alignment: .trailing) { GridRow { spacingSlider ZStack(alignment: .trailing) { Text("50.0 PX").hidden() // maintains size Text(""" \(stripeSpacing, format: .number.precision(.fractionLength(1))) \ \(Text("PX").textScale(.secondary)) """) .foregroundStyle(.secondary) } } GridRow { angleSlider ZStack(alignment: .trailing) { Text("-0.09π RAD").hidden() // maintains size Text(""" \(stripeAngle / .pi, format: .number.precision(.fractionLength(2)))π \ \(Text("RAD").textScale(.secondary)) """) .foregroundStyle(.secondary) } } } .labelsHidden() } @ViewBuilder var spacingSlider: some View { Slider( value: $stripeSpacing, in: Float(10.0)...50.0) { Text("Spacing") } minimumValueLabel: { Image( systemName: "arrow.down.forward.and.arrow.up.backward") } maximumValueLabel: { Image( systemName: "arrow.up.backward.and.arrow.down.forward") } } @ViewBuilder var angleSlider: some View { Slider( value: $stripeAngle, in: (-.pi / 2)...(.pi / 2)) { Text("Angle") } minimumValueLabel: { Image( systemName: "arrow.clockwise") } maximumValueLabel: { Image( systemName: "arrow.counterclockwise") } } } // NOTE: create a .metal file in your project and add the following to it: /* #include <metal_stdlib> using namespace metal; [[ stitchable ]] half4 angledFill(float2 position, float width, float angle, half4 color) { float pMagnitude = sqrt(position.x * position.x + position.y * position.y); float pAngle = angle + (position.x == 0.0f ? (M_PI_F / 2.0f) : atan(position.y / position.x)); float rotatedX = pMagnitude * cos(pAngle); float rotatedY = pMagnitude * sin(pAngle); return (color + color * fmod(abs(rotatedX + rotatedY), width) / width) / 2; } */ #Preview { ShaderUse_Snippet() }
-
26:11 - Typesetting Language
import SwiftUI struct TypesettingLanguage_Snippet: View { var dog = Dog( name: "ไมโล", language: .init(languageCode: .thai), imageName: "Puppy_Pitbull") func phrase(for name: Text) -> Text { Text( "Who's a good dog, \(name)?" ) } var body: some View { HStack(spacing: 54) { VStack { phrase(for: Text("Milo")) } VStack { phrase(for: Text(dog.name)) } VStack { phrase(for: dog.nameText) } } .font(.title) .lineLimit(...5) .multilineTextAlignment(.leading) .padding() } struct Dog { var name: String var language: Locale.Language var imageName: String var nameText: Text { Text(name).typesettingLanguage(language) } } } #Preview { TypesettingLanguage_Snippet() }
-
27:46 - ScrollView Transitions And Behaviors
import SwiftUI struct ScrollingRecentDogsView: View { private static let colors: [Color] = [.red, .blue, .brown, .yellow, .purple] private var dogs: [Dog] = (1..<10).map { .init( name: "Dog \($0)", color: colors[Int.random(in: 0..<5)], isFavorite: false) } private var parks: [Park] = (1..<10).map { .init(name: "Park \($0)") } @State private var scrolledID: Dog.ID? var body: some View { ScrollView { LazyVStack { ForEach(dogs) { dog in DogCard(dog: dog, isTop: scrolledID == dog.id) .scrollTransition { content, phase in content .scaleEffect(phase.isIdentity ? 1 : 0.8) .opacity(phase.isIdentity ? 1 : 0) } } } } .scrollPosition(id: $scrolledID) .safeAreaInset(edge: .top) { ScrollView(.horizontal) { LazyHStack { ForEach(parks) { park in ParkCard(park: park) .aspectRatio(3.0 / 2.0, contentMode: .fill) .containerRelativeFrame( .horizontal, count: 5, span: 2, spacing: 8) } } .scrollTargetLayout() } .scrollTargetBehavior(.viewAligned) .padding(.vertical, 8) .fixedSize(horizontal: false, vertical: true) .background(.thinMaterial) } .safeAreaPadding(.horizontal, 16.0) } struct DogCard: View { var dog: Dog var isTop: Bool var body: some View { DogImage(dog: dog) .overlay(alignment: .bottom) { HStack { Text(dog.name) Spacer() if isTop { TopDog() } Spacer() Image(systemName: "heart") .symbolVariant(dog.isFavorite ? .fill : .none) } .font(.headline) .padding(.horizontal, 22) .padding(.vertical, 12) .background(.thinMaterial) } .clipShape(.rect(cornerRadius: 16)) } } struct DogImage: View { var dog: Dog var body: some View { Rectangle() .fill(dog.color.gradient) .frame(height: 400) } } struct TopDog: View { var body: some View { HStack { Image(systemName: "trophy.fill") Text("Top Dog") Image(systemName: "trophy.fill") } } } struct ParkCard: View { var park: Park var body: some View { RoundedRectangle(cornerRadius: 8) .fill(.green.gradient) .overlay { Text(park.name) .padding() } } } struct Dog: Identifiable { var id = UUID() var name: String var color: Color var isFavorite: Bool } struct Park: Identifiable { var id = UUID() var name: String } }
-
31:12 - Menu Enhancements
import SwiftUI struct DogTagEditMenu: View { @State private var selectedColor = TagColor.blue var body: some View { Menu { ControlGroup { Button { } label: { Label("Cut", systemImage: "scissors") } Button { } label: { Label("Copy", systemImage: "doc.on.doc") } Button { } label: { Label("Paste", systemImage: "doc.on.clipboard.fill") } Button { } label: { Label("Duplicate", systemImage: "plus.square.on.square") } } .controlGroupStyle(.compactMenu) Picker("Tag Color", selection: $selectedColor) { ForEach(TagColor.allCases) { Label($0.rawValue.capitalized, systemImage: "tag") .tint($0.color) .tag($0) } } .paletteSelectionEffect(.symbolVariant(.fill)) .pickerStyle(.palette) } label: { Label("Edit", systemImage: "ellipsis.circle") } .menuStyle(.button) } enum TagColor: String, CaseIterable, Identifiable { case blue case brown case green case yellow var id: Self { self } var color: Color { switch self { case .blue: return .blue case .brown: return .brown case .green: return .green case .yellow: return .yellow } } } }
-
32:30 - Highlight Hover Effect
import SwiftUI struct DogGalleryCard: View { @FocusState private var isFocused: Bool var body: some View { Button { } label: { VStack { RoundedRectangle(cornerRadius: 8) .fill(.blue) .frame(width: 888, height: 500) .hoverEffect(.highlight) Text("Name") .opacity(isFocused ? 1 : 0) } } .buttonStyle(.borderless) .focused($isFocused) } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。