大多数浏览器和
Developer App 均支持流媒体播放。
-
在 visionOS 中打造自定悬停效果
了解如何开发自定悬停效果,让用户在注视视图时实现视图更新。探索如何构建将透明度、缩放和裁剪效果组合在一起的按钮展开效果。探究相应的推荐做法,打造能够满足用户辅助功能需求的舒适效果。
章节
- 0:00 - Introduction
- 2:35 - Content effects
- 7:46 - Effect groups
- 9:40 - Delayed effects
- 12:09 - Accessibility
资源
相关视频
WWDC24
WWDC23
-
下载
大家好 欢迎观看“在 visionOS 中 打造自定悬停效果” 我叫 Christian 是 SwiftUI 团队的一名工程师 通过这个视频 你将了解如何借助 全新的 Custom Hover Effect API 让 SwiftUI 视图 在用户注视时做出响应
在 visionOS 上 交互式区域 在用户注视时会高亮显示 这些高亮显示 是通过悬停效果来应用的 悬停效果能让用户觉得 你的 App 响应灵敏 同时在用户轻捏指尖时 提供关于将要触发 哪个元素的反馈
悬停效果会自动添加到标准控件 并可通过 hoverEffect 视图修饰符 添加到自定控件 如需进一步了解高亮显示等标准效果 请观看“将你的窗口 App 提升至空间计算领域”
标准高亮显示效果 在大多数情况下都能达到理想的效果 但也有一些视图 可以从自定效果中获益
滑块显示旋钮以吸引用户进行互动 “返回”按钮尺寸变大 以显示上一个页面的名称 标签栏弹出打开以显示标签
“Safari 浏览器”导航栏展开 以显示浏览器标签页
利用 visionOS 2 中的 全新 Custom Hover Effect API 你可以构建类似的悬停效果
自定悬停效果可以应用于 App 中任意位置的 SwiftUI 视图 包括装饰元素和 RealityView 附件 当用户注视视图时 伸出手指与它交互时 或是将鼠标光标移到它上方时 就会应用这些效果
自定悬停效果从设计之初 就充分考虑了隐私保护 它们由系统 在 App 进程之外应用 不需要额外的授权或扩展
RealityKit 还新增了一个 API 可将悬停效果应用于 3D 内容 如需了解详情 请观看 “RealityKit 新功能” 我很高兴能够向大家介绍这个 API 首先 我将说明如何使用内容效果 来更改视图的外观 然后 我将介绍如何利用效果组 将多个效果配合应用 接下来 我将介绍 如何利用延迟效果 来控制效果的时序
最后 我将使用全新的 CustomHoverEffect 协议 来根据用户的辅助功能偏好 创建可重复使用的效果 我们首先谈谈内容效果!
内容效果是指 可以改变视图外观的基本效果 它们可以更改视图的不透明度、 转换视图的几何结构 或应用裁剪形状 这些效果只会更改视图的外观 不会影响附近视图的布局 效果会在两种状态之间转换 在用户没有注视视图时 效果会应用非活动状态
在用户注视视图时 效果会转换为活动状态并更新视图 像这个缩放效果一样的几何效果 可修改视图的几何结构
裁剪效果可显示视图的隐藏部分 不透明度效果 可让内容淡入或淡出 当用户将视线从视图上移开时 效果就会转换回非活动状态 可以将多种效果合成在一起 创建细节丰富的转换效果 比如这种展开效果 我正在开发一款视频播放 App 希望为它添加这种效果 在这个视频剩余的时间里 我将逐步构建这种效果 直到让它完美呈现为止
这是“Destination Video”App 它在模拟器中运行 在左上角 我添加了一个 用于切换个人资料的按钮 当我注视这个按钮时 它会高亮显示 作为构建完整展开效果的第一步 我要让这个按钮 在我注视时放大 我们来编写一些代码
我已经将图标和详细信息文本 放置在一个自定按钮中 并且使用了自定按钮样式 来控制按钮的外观 这里就是我要添加缩放效果的位置
在 ButtonStyle 中 我使用 hoverEffect 修饰符 添加了标准高亮显示
为应用缩放效果 我将添加新的 基于代码块的 hoverEffect 修饰符
在代码块中 我可以使用 scaleEffect 等修饰符 来更改视图在活动和非活动状态之间 转换时的外观
系统会使用 isActive 值 true 调用代码块 以获取效果的活动状态 并使用 isActive 值 false 来获取效果的非活动状态 由于效果是由系统应用的 这些调用会提前发生 而不是在悬停实际发生时发生
由于我想让这个按钮 在我注视时放大 因此要在活动状态下应用 5% 的放大 在非活动状态下不放大 我们在模拟器中试试看
很好 在我注视按钮时 高亮显示效果 和缩放效果同时应用
接下来 我要使用裁剪效果 来隐藏和显示按钮的详细信息文本 裁剪效果会更改视图中的可见部分 并且可用于在非活动状态下 隐藏附加内容 当效果变为活动状态时 裁剪效果可以展开 以显示之前隐藏的内容
回到自定按钮样式 我要将现有的 clipShape 修饰符 移到 hoverEffect 代码块中 这样就能在效果变为活动 或非活动状态时更改 clipShape
为了更改裁剪形状的大小 我要为形状添加尺寸修饰符 并使用提供给 hoverEffect 代码块的几何结构代理 来计算活动和非活动状态下 裁剪形状的大小
当效果处于活动状态时 clipShape 应该横跨整个按钮 以便显示整个按钮 但是当效果处于非活动状态时 按钮应该是圆形的 并且应该仅显示图标 所以要让 clipShape 的 宽度和高度相匹配 从而生成圆形 clipShape
最后 我要使用新的锚点参数 将裁剪形状 与按钮的前边缘对齐 确保在效果处于非活动状态时 显示图标 我们在模拟器中试试看
太棒了! 当我注视按钮时 它会展开 当我将视线移开时 它会自动收起 我要打磨一下这个效果 让详细信息文本在按钮展开时淡入
由于只有文本应该淡入 因此我要 为详细信息文本添加 hoverEffect 并应用不透明效果 让文本从 0 到 1 淡入
已经很接近理想效果了 但还是不太对劲 我们再来看一下
我想让详细信息文本 在按钮展开时淡入 但只有在我注视详细信息文本 应该出现的那个空间时 它才会淡入 我们来讨论一下原因
当用户注视附加了悬停效果的视图时 悬停效果就会变为活动状态 我对整个按钮应用了缩放效果 和 clipShape 效果 所以当我注视按钮中的任意位置时 这些效果会变为活动状态 但是我对详细信息文本 应用了不透明度效果 所以只有当我注视文本时 这个效果才会变为活动状态 我需要换一种方法 以便同时激活这些效果 为此我会使用效果组 只要组中的任意效果变为活动状态 分组在一起的效果就会一起应用 如果来自不同视图的效果 分组在一起 那么当我注视其中任意视图时 这个组就会变为活动状态 这意味着效果组可以控制 App 的哪些区域会激活某个效果
你可以通过两种方式将效果分组: 显式和隐式 我将首先以显式方式 对这些效果进行分组
为此 我要创建 HoverEffectGroup 来表示这个组 我要使用一个 Namespace 来为这个组提供唯一 ID
现在 我已经确定了组 接下来我要 以显式方式将每个效果添加到这个组 首先是不透明效果 我要将这个组提供给 ButtonStyle 并将其余效果添加到组中 现在 所有效果都在同一个组中 它们会作为单个效果一起激活
太棒了! 所有效果配合应用、一起激活 文本会随着按钮展开而淡入 也会随着按钮收缩而淡出
像这样以显式方式对效果进行分组 可提供最大限度的灵活性和控制权 但如果我不需要那么大的控制权 也可用隐式方式对效果进行分组
不必为每个效果 提供 hoverEffectGroup 只需为视图添加 一个 hoverEffectGroup 修饰符即可 这会以隐式方式将这个视图 和它子视图上的每个效果添加到组中 因此 我无需将组添加到每个效果 如果不向修饰符提供组 系统也会以隐式方式为我创建组 非常方便 对吧? 个人资料按钮的开发进展顺利 我使用了内容效果和效果组 来应用所需的所有视觉变化 但是我一注视这个按钮 它就会 立即展开 这可能会分散注意力 如果它能稍等一下再展开 那就更好了 我可以使用延迟效果 来控制按钮展开的时间 默认情况下 悬停效果会立即应用 这种方式非常适合 吸引用户进行互动的微妙效果 但几乎所有效果都可以从延迟中获益 哪怕是短暂的延迟 也是如此 这可以防止效果 在用户快速浏览 App 时短暂激活
用户也永远不会完全静止不动 使用延迟可让效果短暂保持活动状态 以适应这种本能的动作 从而避免出现闪烁
最后 显示附加内容的效果 应该采用更长的延迟 这些效果很容易分散注意力 应在用户的视线 聚焦于 App 的特定元素时 保留一会儿 适合每个效果的延迟时间各不相同 因此请务必戴上 Apple Vision Pro 尝试各种效果 看看合适的效果是什么样的
由于个人资料按钮会显示内容 因此我会添加时间更长的延迟
为了应用延迟 我会将这个效果 包装在动画修饰符中 从而提供延迟动画效果
我使用了默认动画 在效果变为活动状态时 使用时间较长的延迟 而在效果变为非活跃状态时 使用时间较短的延迟
不过 我不会让 scaleEffect 延迟 这个效果可提供即时反馈 因此也应该立即应用
由于文本会随着按钮展开而淡入 因此我要将同一动画效果 应用于不透明度效果 以便让各个效果保持同步 我们再尝试运行一下这个效果 现在它有了延迟
很好 按钮仍可通过缩放效果 和高亮显示效果 提供即时反馈 但会稍等片刻才展开
在继续介绍后面的内容之前 我们再 多了解一下如何为效果添加动画效果
如果没有指定动画效果 则效果会使用 SwiftUI 的默认动画
悬停效果支持大家非常熟悉的 线性、缓出和弹簧等动画效果 以及带有自定时间曲线的动画效果
但 CustomAnimation 类型不受支持 因为这些类型 无法应用到 App 的进程之外
我觉得这个按钮展开效果很棒 但是对动态效果敏感的人 可能会感到不适 在制作各种效果时 应该始终考虑到辅助功能 并在需要时提供替代效果 在这个视频余下的部分中 我要更新个人资料按钮 从而在“减弱动态效果”设置 处于启用状态时 使用交叉淡入淡出效果
在我注视这个按钮时 它仍会展开再收起 但会改为使用淡入淡出效果 现在 我已经编写了一个 淡入淡出效果来让详细信息文本淡入 我不会创建一模一样的效果 而是会使用全新的 CustomHoverEffect 协议 来创建适用于两个位置的效果
为了创建可复用的效果 我要拷贝 之前编写的 hoverEffect 修饰符 将它放在遵从 CustomHoverEffect 协议的全新 FadeEffect 类型中
在这个效果的 body 方法中 我可以使用在视图中 用过的 hoverEffect 修饰符 这样便于从简单处着手 然后根据需要进行重构 现在 这个效果自己构成了一个类型 我可以通过允许自定 非活动和活动状态下的不透明值 来让这个效果变得更加实用
现在 我有了可重复使用的 淡入淡出效果 我要返回并在按钮视图中 使用这个效果 只需移除 hoverEffect 代码块 并将它替换为 新的 FadeEffect()
这样一来 我的代码既可以重复使用 又变得更加简洁 我非常喜欢这一点 我还要将展开效果移到 CustomHoverEffect 类型中 和之前一样 我要将 hoverEffect 代码块从视图中移出 将它放在新的 CustomHoverEffect 类型中 称为 ExpandEffect 类型
回到按钮样式 我将移除基于代码块的 hoverEffect
并将它替换为 新的 ExpandEffect() 漂亮!
现在 我的所有效果 都能重复使用了 我可以更新个人资料按钮 从而在启用了“减弱动态效果”时 使用交叉淡入淡出效果
我要首先添加 一个 @Environment 属性 以访问 reduceMotion 设置
在 reduceMotion 处于启用状态时 不应该应用 ExpandEffect() 所以我会改为应用空白效果 空白效果就是不起任何作用的效果 为了在不同效果之间动态切换 需要将每个效果包装在 HoverEffect 类型中 以抹除它们各自的类型 现在还没有完成 不过我要 在模拟器中查看一下进度
背景形状还不正确 但按钮不再展开 而详细信息文本仍会正常淡入 我要将背景更新为 与文本同步交叉淡入淡出 为此 我要将现有背景 替换为两个独立的背景视图 第一个视图是横跨按钮的 Capsule() 应该在我注视按钮时显示出来 我要在 reduceMotion 状态为启用时 将 FadeEffect 应用于这个视图
第二个视图是圆形背景 应该仅在我没有注视按钮时显示 我要在这里应用 FadeEffect 以及自定不透明度值 让这个视图在另一个视图淡入时淡出
效果很棒! 现在 当我注视按钮时 背景和详细信息文本会一起淡入 这个展开式按钮现在大功告成了! 我通过将几个简单的特效 合并在一起 创建了一个精细的效果 通过选择恰当的延迟时间 并尊重用户对辅助功能的偏好 我确保了这个效果能够 为所有用户带来很棒的使用感受 现在 是时候发挥创意 构建你自己的自定悬停效果了! 强烈建议大家从简单处着手 逐步构建效果 但也要让视图的某些部分保持静态 比如我创建的个人资料按钮中的图标 在效果转换过程中 这些锚定元素有助于保持连贯性 当然 你需要对效果进行全面测试 模拟器是快速迭代的理想之选 但是要想了解效果的使用感受 唯一 方法就是戴上 Apple Vision Pro 测试一下
关于如何创建令人惊叹的悬停效果 如需了解更多技巧 请查看最新版《人机界面指南》 感谢大家观看!
-
-
4:06 - Button with Scale Effect
struct ProfileButtonView: View { var action: () -> Void = { } var body: some View { Button(action: action) { HStack(spacing: 2) { ProfileIconView() ProfileDetailView() } } .buttonStyle(ProfileButtonStyle()) } 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) } } } struct ProfileIconView: View { var body: some View { Image(systemName: "person.crop.circle") .resizable() .scaledToFit() .frame(width: 44, height: 44) .padding(6) } } struct ProfileDetailView: View { var body: some View { VStack(alignment: .leading) { Text("Peter McCullough") .font(.body) .foregroundStyle(.primary) Text("Switch profiles") .font(.footnote) .foregroundStyle(.tertiary) } .padding(.trailing, 24) } } }
-
5:37 - Button with Clip and Scale Effects
struct ProfileButtonView: View { var action: () -> Void = { } var body: some View { Button(action: action) { HStack(spacing: 2) { ProfileIconView() ProfileDetailView() } } .buttonStyle(ProfileButtonStyle()) } struct ProfileButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .background(.thinMaterial) .hoverEffect(.highlight) .hoverEffect { effect, isActive, proxy in effect.clipShape(.capsule.size( width: isActive ? proxy.size.width : proxy.size.height, height: proxy.size.height, anchor: .leading )) .scaleEffect(isActive ? 1.05 : 1.0) } } } struct ProfileIconView: View { var body: some View { Image(systemName: "person.crop.circle") .resizable() .scaledToFit() .frame( width: 44, height: 44 ) .padding(6) } } struct ProfileDetailView: View { var body: some View { VStack(alignment: .leading) { Text("Peter McCullough") .font(.body) .foregroundStyle(.primary) Text("Switch profiles") .font(.footnote) .foregroundStyle(.tertiary) } .padding(.trailing, 24) } } }
-
6:50 - Expanding Button with Ungrouped Fade
struct ProfileButtonView: View { var action: () -> Void = { } var body: some View { Button(action: action) { HStack(spacing: 2) { ProfileIconView() ProfileDetailView() .hoverEffect { effect, isActive, _ in effect.opacity(isActive ? 1 : 0) } } } .buttonStyle(ProfileButtonStyle()) } struct ProfileButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .background(.thinMaterial) .hoverEffect(.highlight) .hoverEffect { effect, isActive, proxy in effect.clipShape(.capsule.size( width: isActive ? proxy.size.width : proxy.size.height, height: proxy.size.height, anchor: .leading )) .scaleEffect(isActive ? 1.05 : 1.0) } } } struct ProfileIconView: View { var body: some View { Image(systemName: "person.crop.circle") .resizable() .scaledToFit() .frame(width: 44, height: 44) .padding(6) } } struct ProfileDetailView: View { var body: some View { VStack(alignment: .leading) { Text("Peter McCullough") .font(.body) .foregroundStyle(.primary) Text("Switch profiles") .font(.footnote) .foregroundStyle(.tertiary) } .padding(.trailing, 24) } } }
-
8:19 - Expanding Button with Explicit Group
struct ProfileButtonView: View { var action: () -> Void = { } @Namespace var hoverNamespace var hoverGroup: HoverEffectGroup { HoverEffectGroup(hoverNamespace) } var body: some View { Button(action: action) { HStack(spacing: 2) { ProfileIconView() ProfileDetailView() .hoverEffect(in: hoverGroup) { effect, isActive, _ in effect.opacity(isActive ? 1 : 0) } } } .buttonStyle(ProfileButtonStyle(hoverGroup: hoverGroup)) } struct ProfileIconView: View { var body: some View { Image(systemName: "person.crop.circle") .resizable() .scaledToFit() .frame(width: 44, height: 44) .padding(6) } } struct ProfileDetailView: View { var body: some View { VStack(alignment: .leading) { Text("Peter McCullough") .font(.body) .foregroundStyle(.primary) Text("Switch profiles") .font(.footnote) .foregroundStyle(.tertiary) } .padding(.trailing, 24) } } } struct ProfileButtonStyle: ButtonStyle { var hoverGroup: HoverEffectGroup? func makeBody(configuration: Configuration) -> some View { configuration.label .background(.thinMaterial) .hoverEffect(.highlight, in: hoverGroup) .hoverEffect(in: hoverGroup) { effect, isActive, proxy in effect.clipShape(.capsule.size( width: isActive ? proxy.size.width : proxy.size.height, height: proxy.size.height, anchor: .leading )) .scaleEffect(isActive ? 1.05 : 1.0) } } }
-
9:13 - Expanding Button with Implicit Group
struct ProfileButtonView: View { var action: () -> Void = { } var body: some View { Button(action: action) { HStack(spacing: 2) { ProfileIconView() ProfileDetailView() .hoverEffect { effect, isActive, _ in effect.opacity(isActive ? 1 : 0) } } } .buttonStyle(ProfileButtonStyle()) .hoverEffectGroup() } struct ProfileButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .background(.thinMaterial) .hoverEffect(.highlight) .hoverEffect { effect, isActive, proxy in effect.clipShape(.capsule.size( width: isActive ? proxy.size.width : proxy.size.height, height: proxy.size.height, anchor: .leading )) .scaleEffect(isActive ? 1.05 : 1.0) } } } struct ProfileIconView: View { var body: some View { Image(systemName: "person.crop.circle") .resizable() .scaledToFit() .frame( width: 44, height: 44 ) .padding(6) } } struct ProfileDetailView: View { var body: some View { VStack(alignment: .leading) { Text("Peter McCullough") .font(.body) .foregroundStyle(.primary) Text("Switch profiles") .font(.footnote) .foregroundStyle(.tertiary) } .padding(.trailing, 24) } } }
-
10:51 - Expanding Button with Delayed Effect
struct ProfileButtonView: View { var action: () -> Void = { } var body: some View { Button(action: action) { HStack(spacing: 2) { ProfileIconView() ProfileDetailView() .hoverEffect { effect, isActive, _ in effect.animation(.default.delay(isActive ? 0.8 : 0.2)) { $0.opacity(isActive ? 1 : 0) } } } } .buttonStyle(ProfileButtonStyle()) .hoverEffectGroup() } struct ProfileButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .background(.thinMaterial) .hoverEffect(.highlight) .hoverEffect { effect, isActive, proxy in effect.animation(.default.delay(isActive ? 0.8 : 0.2)) { $0.clipShape(.capsule.size( width: isActive ? proxy.size.width : proxy.size.height, height: proxy.size.height, anchor: .leading )) }.scaleEffect(isActive ? 1.05 : 1.0) } } } struct ProfileIconView: View { var body: some View { Image(systemName: "person.crop.circle") .resizable() .scaledToFit() .frame( width: 44, height: 44 ) .padding(6) } } struct ProfileDetailView: View { var body: some View { VStack(alignment: .leading) { Text("Peter McCullough") .font(.body) .foregroundStyle(.primary) Text("Switch profiles") .font(.footnote) .foregroundStyle(.tertiary) } .padding(.trailing, 24) } } }
-
12:50 - Expanding Button with Reusable Effects
struct ProfileButtonView: View { var action: () -> Void = { } var body: some View { Button(action: action) { HStack(spacing: 2) { ProfileIconView() ProfileDetailView() .hoverEffect(FadeEffect()) } } .buttonStyle(ProfileButtonStyle()) .hoverEffectGroup() } struct ProfileButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .background(.thinMaterial) .hoverEffect(.highlight) .hoverEffect(ExpandEffect()) } } struct ExpandEffect: CustomHoverEffect { func body(content: Content) -> some CustomHoverEffect { content.hoverEffect { effect, isActive, proxy in effect.animation(.default.delay(isActive ? 0.8 : 0.2)) { $0.clipShape(.capsule.size( width: isActive ? proxy.size.width : proxy.size.height, height: proxy.size.height, anchor: .leading )) }.scaleEffect(isActive ? 1.05 : 1.0) } } } struct FadeEffect: CustomHoverEffect { var from: Double = 0 var to: Double = 1 func body(content: Content) -> some CustomHoverEffect { content.hoverEffect { effect, isActive, _ in effect.animation(.default.delay(isActive ? 0.8 : 0.2)) { $0.opacity(isActive ? to : from) } } } } struct ProfileIconView: View { var body: some View { Image(systemName: "person.crop.circle") .resizable() .scaledToFit() .frame( width: 44, height: 44 ) .padding(6) } } struct ProfileDetailView: View { var body: some View { VStack(alignment: .leading) { Text("Peter McCullough") .font(.body) .foregroundStyle(.primary) Text("Switch profiles") .font(.footnote) .foregroundStyle(.tertiary) } .padding(.trailing, 24) } } }
-
14:14 - Final Expanding Button with Accessibility Support
struct ProfileButtonView: View { var action: () -> Void = { } var body: some View { Button(action: action) { HStack(spacing: 2) { ProfileIconView() ProfileDetailView() .hoverEffect(FadeEffect()) } } .buttonStyle(ProfileButtonStyle()) .hoverEffectGroup() } struct ProfileButtonStyle: ButtonStyle { @Environment(\.accessibilityReduceMotion) var reduceMotion func makeBody(configuration: Configuration) -> some View { configuration.label .background { ZStack(alignment: .leading) { Capsule() .fill(.thinMaterial) .hoverEffect(.highlight) .hoverEffect( reduceMotion ? HoverEffect(FadeEffect()) : HoverEffect(.empty)) if reduceMotion { Circle() .fill(.thinMaterial) .hoverEffect(.highlight) .hoverEffect(FadeEffect(from: 1, to: 0)) } } } .hoverEffect( reduceMotion ? HoverEffect(.empty) : HoverEffect(ExpandEffect()) ) } } struct ExpandEffect: CustomHoverEffect { func body(content: Content) -> some CustomHoverEffect { content.hoverEffect { effect, isActive, proxy in effect.animation(.default.delay(isActive ? 0.8 : 0.2)) { $0.clipShape(.capsule.size( width: isActive ? proxy.size.width : proxy.size.height, height: proxy.size.height, anchor: .leading )) }.scaleEffect(isActive ? 1.05 : 1.0) } } } struct FadeEffect: CustomHoverEffect { var from: Double = 0 var to: Double = 1 func body(content: Content) -> some CustomHoverEffect { content.hoverEffect { effect, isActive, _ in effect.animation(.default.delay(isActive ? 0.8 : 0.2)) { $0.opacity(isActive ? to : from) } } } } struct ProfileIconView: View { var body: some View { Image(systemName: "person.crop.circle") .resizable() .scaledToFit() .frame( width: 44, height: 44 ) .padding(6) } } struct ProfileDetailView: View { var body: some View { VStack(alignment: .leading) { Text("Peter McCullough") .font(.body) .foregroundStyle(.primary) Text("Switch profiles") .font(.footnote) .foregroundStyle(.tertiary) } .padding(.trailing, 24) } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。