大多数浏览器和
Developer App 均支持流媒体播放。
-
增强你的 iPad 和 iPhone App 以实现共享空间
准备好为共享空间增强你的 iPad 和 iPhone App !我们将向你展示如何优化你的体验,使其在 VisionOS 上体验出色,并探索 iPad App 交互、视觉处理和媒体的专属设计。
章节
- 1:00 - Interaction
- 7:55 - Visuals
- 8:57 - Media
资源
相关视频
WWDC23
WWDC20
-
下载
♪ 悦耳的器乐嘻哈 ♪ ♪ 悦耳的器乐嘻哈 ♪ 大家好!欢迎大家观看讲座 “增强共享空间的 iPad 和 iPhone App” 我叫 John Marc 是一名平台兼容性工程师 得益于你的努力 大多数 iPad 和 iPhone App 不需要改变任何代码 就能在 Apple 的最新平台上运行 如果你刚刚起步 你可以观看讲座 “在共享空间运行 iPad 和 iPhone App” 先了解一下系统的内置行为 功能差异和测试设置 我将通过本视频为大家介绍 如何让你优秀的 iPad 和 iPhone App 在这个新平台上如鱼得水地运行 我会介绍你 App 可以期待一下的 新交互方式 视觉外观变化 媒体录制和播放功能 这个平台的交互方式很有趣 同时也让用户觉得熟悉 新的自然操控技术是 其中一个关键组件 轻点能让用户看向按钮 然后双指一起轻点按钮进行互动 用户可以轻点按钮 也可以通过轻点 来切换、按住并轻扫滑块
直接接触需要用户把手伸向 App 并用一根手指触碰里面的按钮 不管哪种交互方式 按钮都会提供持续的视觉反馈 来帮助提高互动准确度 这个视频里的光标 代表用户的视线 当看向一个按钮时 高亮悬停特效 会对控件进行着色 以此来突出焦点 请注意 我们 在浏览这个列表的时候 每个项目都会高亮显示 并且高亮会随着 光标的位置左右移动 这样我们就能 清楚地看到光标的位置 控件的悬停特效 表示用户在看什么地方的内容 非活跃状态控件则没有悬停特效 系统控件会为你处理所有的悬停特效 如果你只使用标准控件 就不需要在这儿做什么调整了 如果你正在构建自定义控件 你可能需要调整一下你的悬停特效 我们来看一个例子 这个 App 在 iPad 上的界面 采用了卡片式设计 每个卡片由图片、 标题、日期和菜单按钮组成 这是这个 App 在模拟器中运行的样子 因为菜单按钮是系统控件 所以它按照系统的设置来运行 然而 每个卡片都是带有 .onTap 修饰符的简易 VStack 所以不接收悬停特效 整个卡片是一个轻点目标 所以它需要悬停特效 来向用户反馈这里可交互 我们着重看一下 其中一个卡片来确定一下 按钮等系统控件 自动接收悬停特效 所以这里用作菜单的 按钮已经有了悬停特效 但是在这个例子中 用户要点按整个卡片 来查看更多细节 通过在 VStack 里添加 .hoverEffect 整个卡片变得能够交互更新 并且向用户反馈这里可点击 许多自定义视频播放器 都优化了点击目标 这样用户就不需要 非常精准地点击这些目标 才能互动 我们来看这个例子 这是一个 iPad 的 自定义视频播放器 它的轻点目标明显大于 前进和后退按钮符号 这两个边框表示轻点目标的大小 用户可以在这两个区域互动 我们再看一下这个播放器 在模拟器中的情况 悬停特效展示了 整个区域的可轻点区域 凸显了这些点击目标 同样的例子在共享空间里 展示了带有悬停特效的隐藏属性 这种外观的变化暴露了原理 但是感觉不正确
缩放这个播放器 在缩小的外观里 保留现有的轻点行为 我们在 .contentShape 修饰符里 增加一个自定义形状 通过使用自定义形状 App 可以在 .contentShape 修饰符里 添加比可点击区域小的源和尺寸 在这个视频中 直视按钮的用户 会看到一个悬停特效 有了这个变化 在悬停特效范围以外点击的体验 将符合用户 对 iPad 和 iPhone 体验的期待 再回到完整播放器的模拟器这里 有了这个自定义形状后 悬停特效只出现在了按钮上 但是外面的区域 也允许点击 棒极了! 大多数情况下 系统控件的悬停特效都很优秀; 但是 自定义悬停特效的功能 很强大 有了新的悬停特效 API App 能够创建 自定义按钮和形状的悬停特效 甚至能在必要的时候选择退出控件 我们来看看如何才能做到这一点 .buttonStyle 是一个很好的方法 能把一个自定义样式一致地 应用到 App 的所有按钮上 如果按钮采用自定义样式 它会关掉悬停特效 如果要重新启用自定义 .buttonStyle 按钮的悬停特效 我们要把 .hoverEffect() 修饰符 加到 App 元素里 这是一个简单的自定义按钮样式 看看这个自定义按钮样式 这里 我们需要 添加 .hoverEffect 修饰符 才能给自定义样式按钮 添加悬停特效 许多 App 的自定义界面都很有趣 比如说这个养蜂 App 它的按钮是蜂窝样式 每个蜂窝单元都是它的轻点目标 使用自定义形状按钮的 App 需要 向系统反馈如何呈现悬停特效 这里的框架宽度比这个 蜂窝形状所占的面积要大 所以系统提供的默认悬停特效 不受形状的限制 覆盖了整个按钮框架 在 .contentShape 修饰符里 添加一个自定义形状 悬停特效就会被裁剪成 这个按钮的形状 我们把它加到这里 现在就很完美了 当用户看向单个按钮的时候 悬停特效会被裁剪成按钮的几何形状 由于 App 状态而 自动禁用的系统控件 没有悬停特效 如果 App 不想一再 强调特定的界面元素 它们可以选择退出个别项目 用户希望整个系统的悬停特效 是明显和一致的 所以这些悬停特效 要尽可能少地关闭 系统最多同时接受两个输入值 因为每只手触感都不同 系统也支持自定义手势识别器 但是为了能够流畅地使用手势操控 你需要对其进行更新 游戏或者其他 需要快速、同步输入的 App 要能够支持游戏控制器 iPad 和 iPhone App 很早以前就支持游戏控制器 在这个平台上 这一点对新输入方法而言更为关键 在把 GCSupportsControllerUserInteraction 添加到 Info.plist 文件里 并增加了游戏控制器功能后 App 的产品页面 就增添了华丽的一笔 这就吸引了在 App Store 中 寻找游戏的用户 并提高了 游戏控制器在各个平台上的可用性 如果你想了解更多关于游戏控制器 和 App Store 游戏的相关信息 请观看视频“游戏控制器的改进” 和视频 “为空间计算构建优秀的游戏” 在这个平台上运行的 iPad 和 iPhone App 采用的是 iPad 的浅色外观 在大多数情况下 这种观感很棒 如果你使用的是系统标准控件 布局和颜色 就不需要做出什么改变了 系统使用动态内容缩放 对渲染进行了优化 所以从任何角度、任何距离看 所有的图像和文本始终都是清晰的 使用基于向量的内容创造最佳体验 iPad 和 iPhone 的提示 是以模式呈现的 所以必须要处理 这些提示才能进行下一步 这个新平台上的提示 不是以模式呈现的 申请位置权限 通过 Apple 账号 或 OAuth 登录等提示 不需要处理就能继续下一步 这些界面创造了自己的浏览器 和窗口化体验 App 应该注意和处理 出现提示的情况 但是可能不会立即 得到取消或成功的反馈 捕获、分享和发布内容 都是人们表达自己的好方法 在这个平台上 App 要注意几个差异 这个设备有多个 外置摄像头和内置摄像头 但是有很多摄像头 App 都不能使用 关键要使用发现会话来检测 哪些摄像头和麦克风可用 为了确保 App 有极佳的捕获体验 我们使用 AVCaptureDevice 发现会话 来确认硬件的可用性 此外 和其他平台类似 要申请权限才能使用这些硬件 最后 概括你的授权提示字符串 告知用户使用情况 而不提及具体的硬件或软件版本
在 App 请求 可用摄像头和麦克风的时候 返回与 iPad 和 iPhone 不同的值 查询麦克风的时候 App 会接收到 一个单一的 .front 麦克风 查询摄像头的时候 App 会查询到两个摄像头 .back 摄像头返回了一个 无摄像头字形的黑色摄像头框架 这是一个无功能摄像头 它是用来支持 假定后置摄像头可用的 App 查询前置摄像头的时候 App 就会查询到 一个单独的复合摄像头 如果设备没有空间形象 就没有相机帧返回给 App AVRoutePickerView 和画中画功能 在这个平台上不可用 这一点已经体现在了 系统提供的播放器里 使用自定义播放器的 App 在显示这些控件前 要先检查这两种功能的可用性 最后 这个平台一旦被移除就会锁定 使用背景音频的 App 应该考虑这一差异 因为设备一旦被锁定 它们就失去了这种 背景模式并会被完全挂起 导入媒体的 App 应该在捕获硬件不可用的时候 考虑替换资源 iCloud 和类似文件 及照片选取器的内容选取器 等选项都是极佳的替代资源 此外 使用 VisionKit 的 VNDocumentCameraViewController 的 App 会自动使用附近设备的 连续互通相机捕获内容 这些替换资源 给现有的 iPad 和 iPhone App 带来了更多的媒体导入选项 iPad 和 iPhone App 在这个新平台上运行地非常流畅 确保所有的交互式控件 都添加了悬停特效 游戏 App 要添加支持控制器 来确保玩家一如既往地 拥有极佳的游戏体验 最后 在使用摄像头和麦克风前 通过检查其可用性 来审查你对摄像头和麦克风的假设 现在你了解了如何优化这个新平台的 iPad 和 iPhone App 我很期待能在 共享空间中使用你的 App ♪
-
-
3:02 - Tappable VStack with hover effect
struct TappableCard: View { // Sample card var imageName = "BearsInWater" var headline = "Bear Fishing" var timeAgo = "42 Minutes ago" var body: some View { VStack { VStack(alignment: .leading) { Image(imageName) .resizable() .clipped() .aspectRatio(contentMode: .fill) .frame(width: 300, height: 250, alignment: .center) Text(headline) .padding([.leading]) .font(.title2) .foregroundColor(.black) } Divider() HStack { HStack { Text(timeAgo) .frame(alignment: .leading) .foregroundColor(.black) } .padding([.leading]) Spacer() VStack(alignment: .trailing) { Button { print("Present menu options") } label: { Image(systemName: "ellipsis") .foregroundColor(.black) } } } .padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)) } .frame(width: 300, height: 350, alignment: .top) .hoverEffect() .background(.white) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(Color(.sRGB, red: 150/255, green: 150/255, blue: 150/255, opacity: 0.1), lineWidth: 3.0) ) .cornerRadius(10) .onTapGesture { print("Present card detail") } } }
-
4:08 - Custom player with tap targets that are larger than the hover effect bounds
struct ContentView: View { var body: some View { VStack { // Video player HStack { Button { print("Going back 10 seconds") } label: { Image(systemName: "gobackward.10") .padding(.trailing) .contentShape(.hoverEffect, CustomizedRectShape(customRect: CGRect(x: -75, y: -40, width: 100, height: 100))) .foregroundStyle(.white) .frame(width: 500, height: 834, alignment: .trailing) } Button { print("Play") } label: { Image(systemName: "play.fill") .font(.title) .foregroundStyle(.white) .frame(width: 100, height: 100, alignment: .center) } .padding() Button { print("Going into the future 10 seconds") } label: { Image(systemName: "goforward.10") .padding(.leading) .contentShape(.hoverEffect, CustomizedRectShape(customRect: CGRect(x: 0, y: -40, width: 100, height: 100))) .foregroundStyle(.white) .frame(width: 500, height: 834, alignment: .leading) } } .frame( minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center ) } .frame( minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading ) .background(.black) } } struct CustomizedRectShape: Shape { var customRect: CGRect func path(in rect: CGRect) -> Path { var path = Path() path.move(to: CGPoint(x: customRect.minX, y: customRect.minY)) path.addLine(to: CGPoint(x: customRect.maxX, y: customRect.minY)) path.addLine(to: CGPoint(x: customRect.maxX, y: customRect.maxY)) path.addLine(to: CGPoint(x: customRect.minX, y: customRect.maxY)) path.addLine(to: CGPoint(x: customRect.minX, y: customRect.minY)) return path } }
-
5:14 - Button with custom buttonStyle, then adding a hover effect to the button
struct ContentView: View { var body: some View { VStack { Button("Howdy y'all") { print("🤠") } .buttonStyle(SixColorButton()) } .padding() } } struct SixColorButton: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .padding() .font(.title) .foregroundStyle(.white) .bold() .background { // Background color bands ZStack { Color.black HStack(spacing: 0) { // GREEN Rectangle() .foregroundStyle(Color(red: 125/255, green: 186/255, blue: 66/255)) .frame(width: 16) // YELLOW Rectangle() .foregroundStyle(Color(red: 240/255, green: 187/255, blue: 64/255)) .frame(width: 16) // ORANGE Rectangle() .foregroundStyle(Color(red: 225/255, green: 137/255, blue: 50/255)) .frame(width: 16) // RED Rectangle() .foregroundStyle(Color(red: 200/255, green: 73/255, blue: 65/255)) .frame(width: 16) // PURPLE Rectangle() .foregroundStyle(Color(red: 134/255, green: 64/255, blue: 151/255)) .frame(width: 16) // BLUE Rectangle() .foregroundStyle(Color(red: 75/255, green: 154/255, blue: 218/255)) .frame(width: 16, height: 500) } .opacity(0.7) .rotationEffect(.degrees(35)) } } .cornerRadius(10) .hoverEffect() } }
-
struct ContentView: View { var body: some View { VStack { Button { print("🐝") } label: { // Button label HoneyComb() .fill(.yellow) .frame(width: 300, height: 300) .contentShape(.hoverEffect, HoneyComb()) } } .frame(width: 400, height: 400, alignment: .center) .background(.black) .padding() } } } struct HoneyComb: Shape { func path(in rect: CGRect) -> Path { var path = Path() path.move(to: CGPoint(x: rect.minX + (rect.width * 0.25), y: rect.minY)) path.addLine(to: CGPoint(x: rect.maxX - (rect.maxX * 0.25), y: rect.minY)) path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY)) path.addLine(to: CGPoint(x: rect.maxX - (rect.maxX * 0.25), y: rect.maxY)) path.addLine(to: CGPoint(x: rect.minX + (rect.width * 0.25), y: rect.maxY)) path.addLine(to: CGPoint(x: rect.minX, y: rect.midY)) path.addLine(to: CGPoint(x: rect.minX + (rect.width * 0.25), y: rect.minY)) return path } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。