大多数浏览器和
Developer App 均支持流媒体播放。
-
SwiftUI 的新功能
了解如何使用 SwiftUI 为任一 Apple 平台构建出色的 App。探索如何为 iPadOS 上的标签页和文稿带来全新的外观和使用感受。使用新的窗口 API 改进窗口管理,并更好地控制 visionOS App 中的沉浸式空间和空间容器。我们还将带你了解其他激动人心的改进功能,帮助你创建富有表现力的图表、自定文本和设置文本布局等。
章节
- 0:00 - Introduction
- 0:51 - Fresh apps
- 1:04 - Fresh apps: TabView
- 2:22 - Fresh apps: Presentation sizing
- 2:39 - Fresh apps: Zoom transition
- 3:02 - Fresh apps: Custom controls
- 3:38 - Fresh apps: Vectorized and function plots
- 4:10 - Fresh apps: TableColumnForEach
- 4:25 - Fresh apps: MeshGradient
- 4:51 - Fresh apps: Document launch experience
- 5:33 - Fresh apps: SF Symbols 6
- 6:37 - Harnessing the platform
- 6:52 - Harnessing the platform: Windowing
- 8:28 - Harnessing the platform: Input methods
- 10:45 - Harnessing the platform: Widgets and Live Activities
- 12:25 - Intermezzo
- 12:55 - Framework foundations
- 13:09 - Framework foundations: Custom containers
- 13:48 - Framework foundations: Ease of use
- 16:18 - Framework foundations: Scrolling enhancements
- 17:18 - Framework foundations: Swift 6 language mode
- 18:01 - Framework foundations: Improved interoperability
- 19:18 - Crafting experiences
- 19:43 - Crafting experiences: Volumes
- 20:27 - Crafting experiences: Immersive spaces
- 21:27 - Crafting experiences: TextRenderer
- 22:12 - Next steps
资源
相关视频
WWDC24
-
下载
大家好 感谢观看本次讲座! 我是 Sommer! 我是 Sam! 我们都是 SwiftUI 团队的工程师 我们很高兴为大家 介绍 SwiftUI 的新功能 我和 Sam 都喜欢 K 歌 目前在开发一款 App 用于策划我们团队 定期举办的 K 歌派对 我们的 App 用到了 SwiftUI 的许多改进 我们很高兴能够与大家 分享这些增强功能! 首先介绍能让 App 焕然一新 的多项出色功能 用于完善 App 的工具 可确保 App 适用于每个平台 对框架基本构建块的普遍性改进 还有一整套新工具 助你打造沉浸式体验 哇! 内容真不少 我们赶紧进入正题吧 SwiftUI 能让你的 App 焕然一新 从全新的标签视图、美观的网格渐变 到简洁的控件 统统令人惊艳! Sam 和我开发了一款 K 歌活动策划 App 这款 App 主要基于边栏构建而成 在 iOS 18.0 中 边栏变得更加灵活 只需轻点按钮 我们的主视图 就会变为标签页栏形式 为美观的 UI 元素显示更多相关内容 标签页栏现在悬浮在内容上方! 用户甚至可以根据个人喜好 完全自定使用体验 包括重新排列项目的顺序 以及隐藏不常用的选项 我非常轻松地重写了主视图 这样就能采用新的 TabView 现在 SwiftUI 中的 TabView 采用了一种类型安全语法 方便我们在构建时捕捉常见错误 随着内容增加 你可以 轻松提高标签视图的灵活性 我们只需应用新的 .sidebarAdaptable 标签视图样式 现在 K 歌策划者便可 随心切换标签页栏和边栏视图
如果要呈现的内容非常多 比如这里的各种歌单 那么 边栏视觉效果会非常棒 它甚至还支持重新排序 和移除标签页等自定行为 而且这种行为可以 完全通过编程方式进行控制 焕然一新的边栏在 Apple tvOS 上 视觉效果也很棒 你还可以在 macOS 上 设置标签视图样式 让它显示为边栏或者 工具栏中的分段控件 现在 表单呈现尺寸调整 实现了跨平台统一和简化 你可以使用 .presentationSizing 修饰符来创建尺寸最合适的 .form 或 .page 表单 甚至可以 选用自定尺寸调整 SwiftUI 还支持新的 缩放导航过渡效果 我可以利用这一效果 让派对相关信息 以炫酷的视觉效果展开
要进一步了解新的 TabView 请观看 “提升 iPad 上的标签页 和边栏使用体验” 要更深入地了解新的动画 请观看“提升 UI 动画和过渡效果”
现在你能创建可调整大小的 专属自定控件 例如在控制中心或锁定屏幕 添加按钮和切换开关 甚至能够通过操作按钮激活控件 控件是一种新型小组件 可以使用 App Intents 轻松构建! 只需使用几行代码 我就能创建 ControlWidgetButton 让 WWDC K 歌派对立刻开始! 要了解如何使用功能强大的 全新控件 API 对可配置按钮和切换开关进行自定 请观看: “从系统各处访问 App 控件” 我和 Sam 一直在努力吸引 更多人来参加我们的 K 歌派对 我认为人数呈指数级增长 似乎是非常合理的目标
借助 Swift Charts 中的 函数绘图功能 我可以轻松绘制出漂亮的图表 比如这里的 LinePlot 功能就很好用
接下来 我也要绘制 实际参加人数的图表 唉 离目标还差很远
要更深入地了解 Swift Charts 的改进 请观看“Swift Charts: 矢量图与函数图” 作为一个数据狂人 我还喜欢 统计追踪参加派对的人唱了多少首歌 现在我可以使用 TableColumnForEach 在可动态调整列数的表格中 显示我和 Sam 举办的派对 无论多少个派对都可以! 为了让更多人参加 我觉得我们应该 发出一些五彩缤纷的派对邀请函! SwiftUI 为彩色网格渐变效果 提供了一流支持 只需在多色网格的各点之间进行插值 就可以创建出漂亮的网格!
这个功能非常适合 帮我们制作时尚别致的 K 歌邀请函 完美展现我们派对的魅力!
现在 这款 K 歌策划 App 焕然一新 我和 Sam 想要增添一点乐趣 自定我们的歌词! 因此我们还构建了一款基于文稿的 App 用于为我们喜爱的歌曲编辑歌词 我精心制作了这个启动屏幕 来体现 App 的鲜明个性 同时使用新的“文稿启动场景”类型 来突出 App 的功能 我创建了粗体大字标题 自定了背景 还添加了一些有趣的补充视图 让启动体验令人眼前一亮! 要进一步了解你可以利用哪些功能 让基于文稿的 App 更出彩 比如自定文稿图标和模板 请观看“提升文稿启动体验” 现在我要对音符使用符号特效 为启动屏幕增添点睛之笔! 哇 这些符号真的动起来啦
现在有三种新的 SF Symbols 动画预设可供 App 采用 晃动效果能让符号 以任意方向或角度摆动 呈现引人注目的视觉效果 呼吸效果能让符号平滑地放大和缩小 从而表明活动正在进行中 旋转效果能让符号的某些部分 围绕指定的锚点旋转
现有的一些预设也随着 新功能的推出得到了提升 例如默认的替换动画现在会首选 新的 MagicReplace 行为 MagicReplace 能让符号 流畅地为徽章和斜线添加动画效果
以上只是 SF Symbols 6 众多增强功能中的一小部分 要进一步了解 请观看“SF Symbols 的新功能”
SwiftUI 带来了重大改进 让你的 App 在 Apple 任一平台上 都能呈现浑然天成的风格 借助改进的窗口功能、 增强的输入控制以及 大量一目了然的内容 你的 App 可以充分利用所处平台的优势 现在可以专为 macOS 量身定制窗口的样式和行为 在 macOS 上 我的歌词编辑器 App 有一个显示单行预览的窗口 我使用了新的纯色窗口样式 移除了默认窗口颜色 并将它设置为悬浮窗口层 让它显示在任何其他窗口之上 我使用 defaultWindowPlacement API 将这个窗口放置在屏幕顶部 这样就不会盖住其余的歌词 我综合考虑显示屏尺寸和内容大小 为窗口选定了最合适的位置 我还在预览的内容视图中添加了 WindowDragGesture 这样就能来回拖动窗口 从而调整它在屏幕上的位置
还有新的场景类型 比如实用工具窗口! 要进一步了解如何自定 窗口的样式和行为 请观看“利用 SwiftUI 量身定制 macOS 窗口!” 这款多窗口歌词编辑器 App 在 visionOS 上也有良好表现! 最近我的 K 歌伙伴 Andrew 向我展示了他如何借助 “Botanist”App 中新增的 pushWindow 操作 来突出显示最重要的内容! pushWindow 可用于打开 一个窗口并隐藏原始窗口 我当然也想在歌词编辑器中 实现同样的功能 所以打算使用 pushWindow 环境操作 来突出显示歌词预览 要进一步了解 请观看视频: “在 SwiftUI 中设计窗口” SwiftUI 提供了许多新工具 让你可以充分利用 各个平台独有的输入方法 在 visionOS 中 你可以让视图在用户注视、 将手指放在视图附近或是 将指针移到视图上方时做出响应 同时仍然保护用户隐私
在新的基于闭包的 hoverEffect 修饰符中 我可以控制视图在活动状态 和非活动状态之间转换时的视觉效果 要了解如何协调多个效果、 控制效果的呈现时机 以及对辅助功能设置做出响应 请观看 “在 visionOS 中打造 自定悬停效果” 优秀的 iPadOS、macOS 和 visionOS App 往往会提供出色的键盘支持
在 macOS 上的主菜单中 我有一个项目用于打开预览窗口
我添加了新的 modifierKeyAlternate 修饰符 在按下 Option 键时 可以显示一个次级项目 来提供全屏预览
对于低级别控件 任何视图都可以 对修饰符按键状态变化做出响应 我更新了歌词编辑器 以便使用 onModifierKeysChanged 这样一来 按住 Option 键时可以 看到一条额外的对齐参考线并进行调整 这条对齐参考线用于确定 弹跳小球在歌词上的落点位置 对许多设备而言 指针交互 是另一种重要的输入方式 借助 pointerStyle API 你可以自定系统指针的外观和可见性 由于我将歌词设置为尺寸可调 因此需要将相应的 frameResize 指针样式 应用于每个尺寸调整锚点 在 iPadOS 17.5 中 SwiftUI 新增了 对 Apple Pencil 和 Apple Pencil Pro 功能的支持 比如轻点两下和轻捏手势
借助 .onPencilSqueeze 我可以 从手势中收集信息 看看首选的操作是哪一个 在这个示例中 我要在笔尖悬停位置下方 显示歌词涂鸦调色盘 这样就可以用妙趣横生的 涂鸦来标记歌词 要进一步了解所有 新的 Apple Pencil API 请观看“充分利用 Apple Pencil 的强大功能” 小组件让信息一目了然 也能让 用户与你的 App 进行重要互动 现在 实时活动功能 也已登陆 watchOS 基于 iOS 的实时活动 会自动显示在 Apple Watch 上 你无需进行任何操作!
我和 Sam 之前就已经设置了 可即时显示歌词的实时活动 如今 实时活动会自动 显示在我的 Apple Watch 上! 为了实现惊艳的视觉效果 可以使用新的小型 .supplementalActivityFamily 来专门针对 watchOS 定制内容 一次显示更多歌词 很好!
为了让演唱者能够通过轻点两下 实现歌词快进 我可以应用 .handGestureShortcut 修饰符
此外 我还希望确保每场 K 歌活动中 轮到我上场时 我都能心中有数 于是我在 K 歌策划 App 中 添加了一个小组件 使用新的参考日期格式样式 来显示 K 歌上场倒计时
在文本方面 现在新增了多种格式 用于显示实时时间和日期 这些格式 在小组件和实时活动中效果很棒!
这些格式包括日期参考、 日期偏移和计时器 每种格式都可以深度自定 具体到各个组件 甚至还能根据容器的大小进行调整 小组件现在也变得更智能了 指定相关情境 就能让系统更智能地 在智能叠放等位置显示这些情境 这样一来 我的倒计时小组件 就能在指定时间或是 在演唱者即将到达计划 K 歌场所时 自动显示出来! 嘿 Sommer 歌词编辑器怎么样了? 我在构思我的下一首独唱曲目 《Cupertino Dreamin》的歌词 我得把想到的歌词写下来 以免事后忘记! 进展顺利!我刚刚用它 为《Smells Like Scene Spirit》 写了一些歌词 一定会很精彩 太棒了 我们可以 把这个插到歌单末尾 你提醒了我:歌单!WWDC K 歌派对有没有现成的歌单? 当然啦 Sommer 这些歌曲的主题传达的正是 SwiftUI 出色的全新框架基础功能 SwiftUI 新增了各种 API 让这个框架变得 比以往任何时候都更加易于使用 包括对 SwiftUI 核心组件的改进、 基础 API 的全新使用方式 以及增强功能的易用性
现在可以创建自己的 自定容器视图 ForEach 上的新 API subviewOf 可以对给定视图的子视图 进行迭代 比如在这个示例中 我会将每个子视图 包装在单独的卡片视图中 你可以用它来创建自定容器 自定容器的功能与 SwiftUI 的 列表和选择器等内置容器相同 包括将分别支持静态内容 和动态内容的部分加以混合 以及添加特定容器的修饰符 要进一步了解自定容器 及背后的 SwiftUI 基本原理 别忘了观看“解密 SwiftUI 容器” 由于在易用性方面有了新的改进 SwiftUI 变得比以往更易于使用 现在可以使用新的条目宏 来编写一个简单的属性 而不必完全遵从 EnvironmentKey 并编写环境值的扩展 最棒的是 它不仅仅适用于环境值 条目宏也可以与 FocusValues、 Transaction 以及新的 ContainerValue 配合使用 现在可以将其他信息附加到 SwiftUI 内置的辅助功能标签中 这意味着 你可以在控件中 添加额外的辅助功能信息 而无需覆盖 SwiftUI 框架提供的标签 别忘了观看 “了解 SwiftUI 中的辅助功能” 来探索 SwiftUI 新增的 各种令人惊叹的辅助功能 例如 对条件修饰符的支持 以及基于 App Intent 的 accessibilityActions
Xcode 预览采用全新的动态链接架构 你无需重新构建项目 即可在预览和“边构建边运行”模式 之间切换 从而加快迭代速度
现在也可以更轻松地设置预览 你现在可以利用可预览宏 直接在预览中使用状态 无需使用样板 将预览内容包装在视图中 现在可以通过新的方式 来处理文本和管理文本选择 SwiftUI 现在提供了 对文本编辑控件内 文本选择的编程访问和控制! 文本选择绑定的内容会进行更新 以匹配歌词栏位中的选定文本
现在 我可以读取文本选择 的属性 例如所选范围 我可以利用这个功能 在检查器中 显示所选单词的建议韵脚
借助 .searchFocused 你可以通过编程方式 操控搜索栏的焦点状态 这意味着你可以检查 焦点是不是在搜索栏上 还可通过编程方式 将焦点移入和移出搜索栏
现在可以在任意文本栏中 添加文本建议 我要使用这个功能来针对 如何写完一行歌词提供建议 这些建议会以下拉菜单的形式显示 当我选取某个选项后 文本栏会更新为所选的补全内容 SwiftUI 也推出了新的图形功能 现在可以将各种颜色 巧妙地混合在一起 新的颜色混合修饰符 可以按指定量混合两种颜色 我们还扩展了自定着色器功能 能够在首次使用之前 对着色器进行预编译 从而避免 因着色器编译延迟而导致丢帧 我们推出的许多新 API 可对滚动视图进行精细控制 现在可以通过 .onScrollGeometryChange 与 ScrollView 的状态 进行更深层次的整合 从而以高性能方式对内容偏移、 内容大小等方面的变化做出响应 比如这个“Back to invitation” 按钮 如果我在滚动浏览时 超过了滚动视图内容的顶部 就会显示这个按钮
现在当视图可见性 因滚动操作而发生变化时 你可以检测到这一情况! 因此你可以围绕移入 和移出屏幕的内容 打造出色的体验 比如这个自动播放视频
你不仅对滚动视图拥有 更多编程控制权 还可以 通过编程方式滚动浏览到更多位置 例如顶部边缘!
我们还提供了各种其他控制旋钮 你可以通过转动这些旋钮 来打造理想的滚动浏览体验 比如关闭沿给定轴 呈现的反弹效果、 以编程方式停止滚动、 对内容对齐方式进行精细控制等等 新的 Swift 6 语言模式 可为编译时数据争用安全性提供保障 SwiftUI 还改进了它的 API 从而方便你在 App 中 采用新的语言模型 对 SwiftUI 中视图的评估 一直以主要参与者为准 因此 视图协议现在会标有 主要参与者注解 以反映这一点 这意味着 所有遵从视图的类型 在默认情况下都与主要参与者 隐式隔离 如果你以前将视图显式标记为 主要参与者 那么现在可以 移除这个注解 而相关行为 不会发生任何变化 新的 Swift 6 语言模式为可选模式 因此你在准备就绪后 随时可以加以利用 要进一步了解编译时检查 请记得观看 “将 App 迁移到 Swift 6” SwiftUI 的设计不仅 适用于构建全新的 App 还适用于在使用 UIKit 和 AppKit 编写的现有 App 中 构建新功能 与这些框架之间的 出色互操作性至关重要 我们对手势与动画的整合 进行了重大改进 现在可以选择任意内置或自定 UIGestureRecognizer 将它移到你的 SwiftUI 视图层次结构中使用 这个功能甚至适用于并非由 UIKit 直接支持的 SwiftUI 视图 互操作性的改进还体现在另一个方面 UIKit 和 AppKit 现在可以 利用 SwiftUI 动画的强大功能 SwiftUI 在 UIView 和 NSAnimationContext 上定义了新的 animate 函数 这样就可以使用 进程内 SwiftUI 动画 为 UIKit 和 AppKit 的变化 添加动画效果 手势驱动的动画甚至会自动保留速度 就像在 SwiftUI 视图中一样 UI 和 NSViewRepresentable 上下文 提供了新的 API 能够将 SwiftUI 中开始的动画 桥接到 UIKit 和 AppKit 中 即使跨越不同框架 也能确保动画完美同步运行 要进一步了解 如何跨框架使用动画 请记得观看 “提升 UI 动画和过渡效果” 所有这些基础性改进 都让视觉效果焕然一新 现在我要做的就只剩下练习 K 歌 然后和 Sommer 一起举办派对了 利用 SwiftUI 提供的 新工具打造各种体验 不仅有助于构建出色的练习 App 还有助于为活动的成功举办奠定基础 借助与空间容器、沉浸式空间 和新的文本效果配合使用的新 API 我们可以获得生动的 K 歌体验 为了练就好声音 我构建了一款 visionOS 练习 App 它会在空间容器中显示麦克风 在 visionOS 2 中 空间容器可以显示底板 这有助于你感知空间容器的边界 并可引导你使用窗口控件 包括新的尺寸调整控制柄 我们的麦克风已经 带有麦克风支架底座 所以 我更喜欢不显示 系统自带底板的视觉效果 我要使用 .volumeBaseplateVisibility 修饰符来停用系统自带底板 很好!
我还使用新的 .onVolumeViewpointChange 修饰符旋转了麦克风 让它始终对着演唱者 只要我移动到空间容器中 没去过的一侧 就会调用这个操作 以便对观看方式的变化做出响应 搞定麦克风之后 现在 我需要找个地方来放置它 最佳 K 歌场所莫过于 氛围感十足的 K 歌包房 我已经有了一个美妙的沉浸式空间 现在可以控制设定的沉浸程度 沉浸度可以选择初始值 50% 最低值 40% 帮助演唱者适应 K 歌包房的环境
为了营造气氛 我现在可以 对渐进沉浸式空间周围的 透视视频应用一些效果 我可以使用 preferred-surroundings-effect 来调暗视频透视 或者为了 打造与众不同的 K 歌体验 我还可以使用 colorMultiply 实现炫酷的氛围感灯光!
哇 太棒了
要进一步了解 空间容器和沉浸式空间的改进 包括附加装饰和 指定支持视角的新方法 请记得观看 “深入探究空间容器和沉浸式空间”
舞台已经搭好了 但在开始派对之前 还需要可以带着我们跟唱的歌词!
现在可以利用 自定渲染效果和交互行为 来扩展 SwiftUI 文本视图 我可以使用这个功能 为 K 歌歌词构建文字高亮显示效果 我的 K 歌渲染器会在原始图样 后面创建文本副本 原始图样会变得模糊 并轻微染色 从而让文本看起来 像是发光的紫色! 将这种高亮显示效果 应用于特定字词并进行最终润色 就能制作出令人惊叹的效果 比如这样的 K 歌字词高亮显示 要全面了解如何制作 这些令人惊叹的文字效果 请记得观看“利用 SwiftUI 打造自定视觉效果” 好的 Sommer 我想我们已经 完成了派对的全部准备工作 一切都很完美! SwiftUI 的这些改进提升非常有用 让我和 Sommer 能够充分 发掘我们这款 App 的精彩 我们希望大家也能释放 你们的 App 的无穷潜力 如果你有一款基于边栏的 iPad 或 Apple tvOS App 你可以利用一些新的 标签视图 API 来提高灵活性 如果你有一款基于文稿的 App 你可以进行优化 让文稿启动体验再上新境界 为 macOS 和 visionOS App 添加新的窗口功能和输入功能 对 watchOS 上的体验进行微调 从而充分利用实时活动 充分利用 visionOS 上有关 空间容器和沉浸式空间的新功能 对 SwiftUI 开发者来说 今年似乎是大展身手的绝佳时机! 对 K 歌爱好者来说也是一样 好的 Sam 你准备好了吗? Sommer 我早就准备好了 嘿 Siri 让派对开始吧!
-
-
1:38 - TabView
import SwiftUI struct KaraokeTabView: View { @State var customization = TabViewCustomization() var body: some View { TabView { Tab("Parties", image: "party.popper") { PartiesView(parties: Party.all) } .customizationID("karaoke.tab.parties") Tab("Planning", image: "pencil.and.list.clipboard") { PlanningView() } .customizationID("karaoke.tab.planning") Tab("Attendance", image: "person.3") { AttendanceView() } .customizationID("karaoke.tab.attendance") Tab("Song List", image: "music.note.list") { SongListView() } .customizationID("karaoke.tab.songlist") } .tabViewStyle(.sidebarAdaptable) .tabViewCustomization($customization) } } struct PartiesView: View { var parties: [Party] var body: some View { Text("PartiesView") } } struct PlanningView: View { var body: some View { Text("PlanningView") } } struct AttendanceView: View { var body: some View { Text("AttendanceView") } } struct SongListView: View { var body: some View { Text("SongListView") } } struct Party { static var all: [Party] = [] } #Preview { KaraokeTabView() }
-
2:28 - Presentation sizing
import SwiftUI struct AllPartiesView: View { @State var showAddSheet: Bool = true var parties: [Party] = [] var body: some View { PartiesGridView(parties: parties, showAddSheet: $showAddSheet) .sheet(isPresented: $showAddSheet) { AddPartyView() .presentationSizing(.form) } } } struct PartiesGridView: View { var parties: [Party] @Binding var showAddSheet: Bool var body: some View { Text("PartiesGridView") } } struct AddPartyView: View { var body: some View { Text("AddPartyView") } } struct Party { static var all: [Party] = [] } #Preview { AllPartiesView() }
-
2:39 - Zoom transition
import SwiftUI struct PartyView: View { var party: Party @Namespace() var namespace var body: some View { NavigationLink { PartyDetailView(party: party) .navigationTransition(.zoom( sourceID: party.id, in: namespace)) } label: { Text("Party!") } .matchedTransitionSource(id: party.id, in: namespace) } } struct PartyDetailView: View { var party: Party var body: some View { Text("PartyDetailView") } } struct Party: Identifiable { var id = UUID() static var all: [Party] = [] } #Preview { @Previewable var party: Party = Party() NavigationStack { PartyView(party: party) } }
-
3:18 - Controls API
import WidgetKit import SwiftUI struct StartPartyControl: ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration( kind: "com.apple.karaoke_start_party" ) { ControlWidgetButton(action: StartPartyIntent()) { Label("Start the Party!", systemImage: "music.mic") Text(PartyManager.shared.nextParty.name) } } } } // Model code class PartyManager { static let shared = PartyManager() var nextParty: Party = Party(name: "WWDC Karaoke") } struct Party { var name: String } // AppIntent import AppIntents struct StartPartyIntent: AppIntent { static let title: LocalizedStringResource = "Start the Party" func perform() async throws -> some IntentResult { return .result() } }
-
3:49 - Function plotting
import SwiftUI import Charts struct AttendanceView: View { var body: some View { Chart { LinePlot(x: "Parties", y: "Guests") { x in pow(x, 2) } .foregroundStyle(.purple) } .chartXScale(domain: 1...10) .chartYScale(domain: 1...100) } } #Preview { AttendanceView() .padding(40) }
-
4:18 - Dynamic table columns
import SwiftUI struct SongCountsTable: View { var body: some View { Table(Self.guestData) { // A static column for the name TableColumn("Name", value: \.name) TableColumnForEach(Self.partyData) { party in TableColumn(party.name) { guest in Text(guest.songsSung[party.id] ?? 0, format: .number) } } } } private static func randSongsSung(low: Bool = false) -> [Int : Int] { var songs: [Int : Int] = [:] for party in partyData { songs[party.id] = low ? Int.random(in: 0...3) : Int.random(in: 3...12) } return songs } private static let guestData: [GuestData] = [ GuestData(name: "Sommer", songsSung: randSongsSung()), GuestData(name: "Sam", songsSung: randSongsSung()), GuestData(name: "Max", songsSung: randSongsSung()), GuestData(name: "Kyle", songsSung: randSongsSung(low: true)), GuestData(name: "Matt", songsSung: randSongsSung(low: true)), GuestData(name: "Apollo", songsSung: randSongsSung()), GuestData(name: "Anna", songsSung: randSongsSung()), GuestData(name: "Raj", songsSung: randSongsSung()), GuestData(name: "John", songsSung: randSongsSung(low: true)), GuestData(name: "Harry", songsSung: randSongsSung()), GuestData(name: "Luca", songsSung: randSongsSung()), GuestData(name: "Curt", songsSung: randSongsSung()), GuestData(name: "Betsy", songsSung: randSongsSung()) ] private static let partyData: [PartyData] = [ PartyData(partyNumber: 1, numberGuests: 5), PartyData(partyNumber: 2, numberGuests: 6), PartyData(partyNumber: 3, numberGuests: 7), PartyData(partyNumber: 4, numberGuests: 9), PartyData(partyNumber: 5, numberGuests: 9), PartyData(partyNumber: 6, numberGuests: 10), PartyData(partyNumber: 7, numberGuests: 11), PartyData(partyNumber: 8, numberGuests: 12), PartyData(partyNumber: 9, numberGuests: 11), PartyData(partyNumber: 10, numberGuests: 13), ] } struct GuestData: Identifiable { let name: String let songsSung: [Int : Int] let id = UUID() } struct PartyData: Identifiable { let partyNumber: Int let numberGuests: Int let symbolSize = 100 var id: Int { partyNumber } var name: String { "\(partyNumber)" } } #Preview { SongCountsTable() .padding(40) }
-
4:42 - Mesh gradients
import SwiftUI struct MyMesh: View { var body: some View { MeshGradient( width: 3, height: 3, points: [ .init(0, 0), .init(0.5, 0), .init(1, 0), .init(0, 0.5), .init(0.3, 0.5), .init(1, 0.5), .init(0, 1), .init(0.5, 1), .init(1, 1) ], colors: [ .red, .purple, .indigo, .orange, .cyan, .blue, .yellow, .green, .mint ] ) } } #Preview { MyMesh() .statusBarHidden() }
-
5:14 - Document launch scene
DocumentGroupLaunchScene("Your Lyrics") { NewDocumentButton() Button("New Parody from Existing Song") { // Do something! } } background: { PinkPurpleGradient() } backgroundAccessoryView: { geometry in MusicNotesAccessoryView(geometry: geometry) .symbolEffect(.wiggle(.rotational.continuous())) } overlayAccessoryView: { geometry in MicrophoneAccessoryView(geometry: geometry) }
-
7:04 - Window styling and default placement
Window("Lyric Preview", id: "lyricPreview") { LyricPreview() } .windowStyle(.plain) .windowLevel(.floating) .defaultWindowPlacement { content, context in let displayBounds = context.defaultDisplay.visibleRect let contentSize = content.sizeThatFits(.unspecified) return topPreviewPlacement(size: contentSize, bounds: displayBounds) } }
-
7:30 - Window Drag Gesture
Text(currentLyric) .background(.thinMaterial, in: .capsule) .gesture(WindowDragGesture())
-
8:18 - Push window environment action
struct EditorView: View { @Environment(\.pushWindow) private var pushWindow var body: some View { Button("Play", systemImage: "play.fill") { pushWindow(id: "lyric-preview") } } }
-
8:47 - Hover effects
struct ProfileButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .background(.thinMaterial) .hoverEffect(.highlight) .clipShape(.capsule) .hoverEffect { effect, isActive, _ in effect.scaleEffect(isActive ? 1.05 : 1.0) } } }
-
9:14 - Modifier key alternates
Button("Preview Lyrics in Window") { // show preview in window } .modifierKeyAlternate(.option) { Button("Preview Lyrics in Full Screen") { // show preview in full screen } } .keyboardShortcut("p", modifiers: [.shift, .command])
-
9:32 - Responding to modifier keys
var body: some View { LyricLine() .overlay(alignment: .top) { if showBouncingBallAlignment { // Show bouncing ball alignment guide } } .onModifierKeysChanged(mask: .option) { showBouncingBallAlignment = !$1.isEmpty } }
-
9:55 - Pointer customization
ForEach(resizeAnchors) { anchor in ResizeHandle(anchor: anchor) .pointerStyle(.frameResize(position: anchor.position)) }
-
10:23 - Pencil squeeze gesture
@Environment(\.preferredPencilSqueezeAction) var preferredAction var body: some View { LyricsEditorView() .onPencilSqueeze { phase in if preferredAction == .showContextualPalette, case let .ended(value) = phase { if let anchorPoint = value.hoverPose?.anchor { lyricDoodlePaletteAnchor = .point(anchorPoint) } lyricDoodlePalettePresented = true } }
-
13:13 - Custom containers
struct DisplayBoard<Content: View>: View { @ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } } } DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
13:35 - Custom containers with sectioning
DisplayBoard { Section("Matt's Favorites") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") .displayBoardCardRejected(true) } Section("Sam's Favorites") { ForEach(songsFromSam) { song in Text(song.title) } } }
-
13:52 - Entry macro
extension EnvironmentValues { @Entry var karaokePartyColor: Color = .purple } extension FocusValues { @Entry var lyricNote: String? = nil } extension Transaction { @Entry var animatePartyIcons: Bool = false } extension ContainerValues { @Entry var displayBoardCardStyle: DisplayBoardCardStyle = .bordered }
-
14:12 - Default accessibility label augmentation
SongView(song) .accessibilityElement(children: .combine) .accessibilityLabel { label in if let rating = song.rating { Text(rating) } label }
-
14:52 - Previewable
#Preview { @Previewable @State var showAllSongs = true Toggle("Show All songs", isOn: $showAllSongs) }
-
15:06 - Programatic text selection
struct LyricView: View { @State private var selection: TextSelection? var body: some View { TextField("Line \(line.number)", text: $line.text, selection: $selection) // ... } }
-
15:19 - Getting selected ranges
InspectorContent(text: line.text, ranges: selection?.ranges)
-
15:29 - Binding to search field focus state
// Binding to search field focus state struct SongSearchView: View { @FocusState private var isSearchFieldFocused: Bool @State private var searchText = "" @State private var isPresented = false var body: some View { NavigationSplitView { Text("Power Ballads") Text("Show Tunes") } detail: { // ... if !isSearchFieldFocused { Button("Find another song") { isSearchFieldFocused = true } } } .searchable(text: $searchText, isPresented: $isPresented) .searchFocused($isSearchFieldFocused) } }
-
15:41 - Text suggestions
TextField("Line \(line.number)", text: $line.text) .textInputSuggestions { ForEach(lyricCompletions) { Text($0.attributedCompletion) .textInputCompletion($0.text) } }
-
15:59 - Color mixing
Color.red.mix(with: .purple, by: 0.2) Color.red.mix(with: .purple, by: 0.5) Color.red.mix(with: .purple, by: 0.8)
-
16:13 - Custom shaders
ContentView() .task { let slimShader = ShaderLibrary.slim() try! await slimShader.compile(as: .layerEffect) }
-
16:23 - React to scroll geometry changes
struct ContentView: View { @State private var showBackButton = false ScrollView { // ... } .onScrollGeometryChange(for: Bool.self) { geometry in geometry.contentOffset.y < geometry.contentInsets.top } action: { wasScrolledToTop, isScrolledToTop in withAnimation { showBackButton = !isScrolledToTop } } }
-
16:42 - React to scroll visibility changes
struct AutoPlayingVideo: View { @State private var player: AVPlayer = makePlayer() var body: some View { VideoPlayer(player: player) .onScrollVisibilityChange(threshold: 0.2) { visible in if visible { player.play() } else { player.pause() } } } }
-
16:54 - New scroll positions
struct ContentView: View { @State private var position: ScrollPosition = .init(idType: Int.self) var body: some View { ScrollView { // ... } .scrollPosition($position) .overlay { FloatingButton("Back to Invitation") { position.scrollTo(edge: .top) } } } }
-
18:17 - Gesture interoperability
struct VideoThumbnailScrubGesture: UIGestureRecognizerRepresentable { @Binding var progress: Double func makeUIGestureRecognizer(context: Context) -> VideoThumbnailScrubGestureRecognizer { VideoThumbnailScrubGestureRecognizer() } func handleUIGestureRecognizerAction( _ recognizer: VideoThumbnailScrubGestureRecognizer, context: Context ) { progress = recognizer.progress } } struct VideoThumbnailTile: View { var body: some View { VideoThumbnail() .gesture(VideoThumbnailScrubGesture(progress: $progress)) } }
-
18:34 - SwiftUI animations in UIKit and AppKit
let animation = SwiftUI.Animation.spring(duration: 0.8) // UIKit UIView.animate(animation) { view.center = endOfBracelet } // AppKit NSAnimationContext.animate(animation) { view.center = endOfBracelet }
-
18:57 - Representable animation bridging
struct BeadBoxWrapper: UIViewRepresentable { @Binding var isOpen: Bool func updateUIView(_ box: BeadBox, context: Context) { context.animate { box.lid.center.y = isOpen ? -100 : 100 } } }
-
19:59 - Volume baseplate visibility
struct KaraokePracticeApp: App { var body: some Scene { WindowGroup { ContentView() } .windowStyle(.volumetric) .defaultWorldScaling(.trueScale) .volumeBaseplateVisibility(.hidden) } }
-
20:15 - React to volume viewpoint changes
struct MicrophoneView: View { @State var micRotation: Rotation3D = .identity var body: some View { Model3D(named: "microphone") .onVolumeViewpointChange { _, new in micRotation = rotateToFace(new) } .rotation3DEffect(micRotation) .animation(.easeInOut, value: micRotation) } }
-
20:38 - Control allowed immersion levels
struct KaraokeApp: App { @State private var immersion: ImmersionStyle = .progressive( 0.4...1.0, initialAmount: 0.5) var body: some Scene { ImmersiveSpace(id: "Karaoke") { LoungeView() } .immersionStyle(selection: $immersion, in: immersion) } }
-
21:00 - Preferred surrounding effects
struct LoungeView: View { var body: some View { StageView() .preferredSurroundingsEffect(.colorMultiply(.purple)) } }
-
21:33 - Custom text renderers
struct KaraokeRenderer: TextRenderer { func draw( layout: Text.Layout, in context: inout GraphicsContext ) { for line in layout { for run in line { var glow = context glow.addFilter(.blur(radius: 8)) glow.addFilter(purpleColorFilter) glow.draw(run) context.draw(run) } } } } struct LyricsView: View { var body: some View { Text("A Whole View World") .textRenderer(KaraokeRenderer()) } } #Preview { LyricsView() }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。