大多数浏览器和
Developer App 均支持流媒体播放。
-
出色 Mac Catalyst app 的质量
探索最佳实践、工具和技术,以帮助开发尽可能优秀的 Mac Catalyst app。我们将带您了解在您将自己的 iPad app 移植到 macOS 时应注意的关键事项,探讨优化您的界面和体验的详细代码示例,并向您展示如何向所有人分发您的 Mac app。为了充分了解本节内容,我们建议您要基本熟悉 Mac Catalyst。观看 WWDC21 的“Mac Catalyst 中的新功能”,全面了解用于将您的 iPad app 移植到 Mac 的最新功能。关于改进您的 macOS 体验的更多信息,请观看 WWDC20 的“优化 Mac Catalyst app 界面”。
资源
- Accessibility design for Mac Catalyst
- Adding Menus and Shortcuts to the Menu Bar and User Interface
- Bring an iPad App to the Mac with Mac Catalyst
- Building and Improving Your App with Mac Catalyst
- Human Interface Guidelines: Mac Catalyst
- Mac Catalyst
相关视频
WWDC21
- 使您的 iPad App 更上一层楼
- 带有 M1 的 Mac 上的出色 iPad 和 iPhone app 的质量
- 聚焦 iPad 键盘导航
- 认识 UIKit 按钮系统
- Mac Catalyst 中的新功能
- UIKit 中的新功能
WWDC20
-
下载
♪低音音乐播放♪ ♪ 大家好 欢迎来到 《出色的Mac Catalyst app的特质》 我叫欧文蒙斯马 是一名Cocoa工程师 稍后我的同事 来自UIKit的戴夫拉哈贾 也会加入谈话 今天我们要讨论制作出色的 Catalyst app的三个重要考量 首先 我们会介绍你转移到 Mac Catalyst app时 发生的一些高层次改变 接着 我们会深入讨论你可以做的 一些特定代码修改 以改善你在Mac上的app体验 最后我们会说明 关于app分发的信息 所以我们先从转移你的app 到Mac Catalyst说起吧 往出色的Catalyst app的第一步 是拥有一个出色的iPad app 而你的app已经 在使用M1的Mac上运行 无需其他更改 如果你有一台M1 Mac 可以马上尝试使用 Xcode中的“为iPad设计”运行目标 通过在iPad上采用这些功能 你的Mac app 就会有一个很棒的开始 如果你的app支持iPad上的多任务 你会自动获得Mac上的多窗口支持 而如果你使用UIMenuBuilder 你的菜单会在你的app的菜单栏中 自动选择 方法是通过上下文菜单 在画面上进行双击 我们也自动桥接系统行为 例如复制/粘贴和拖放 为了更加了解你的iPad app 如何像是在M1 Mac上运行 请查看我们的视频 《搭载M1的Mac上出色的iPad 与iPhone app的特质》 但你在这里是因为你想更深入理解 通过选择Mac复选框 你会获得分发到所有Mac的能力 并取得额外API 来进一步改进你的app 所以让我们用我们的app “旅行规划”做这件事吧! 在我们的Xcode计划设置中 我们在“部署信息”底下 选择Mac选项 请注意右边有个额外的弹窗出现 让我们在缩放的iPad界面 和Mac优化界面之间选择 我们稍后会进一步检视这个选择 现在 我们来点击Xcode工具栏的 建构与运行吧 我们的app就会建构和运行了! 如果你的app建构失败 有几件事需要调查 某些已弃用的框架和类别 不适用于Mac Catalyst 所以现在该是时候进行现代化了 这不只会让你的app在Mac上运行 也会改进你的iOS app 从OpenGLES转移到 Apple自己的Metal框架 会解放GPU的全部效能 Contacts框架取代了 已弃用的AddressBook 是一种处理联系人的具前瞻性 线程安全的方式 UIWebView已经被弃用 而且已经被WKWebView取代 另外 请务必检查你对第三方的依赖 如果这些框架是当作 XCFramework捆绑包来分发 请确定它们提供 要链接的Mac二进制文件 当你开始制作你的Mac app时 请在建构你的计划时注意编译器警告 并监视控制台日志来获取运行时消息 这些警告会告诉你如何修正代码 使代码能作为Mac Catalyst程序 运行良好 请记住 只能使用受支持的API 这样你的app才会继续 在将来的macOS版本中运行 另外也一定要注意你的app 在Mac上运行时 会收到的生命周期事件 如果你的app目前依赖 你的app代理上 调用的生命周期事件 你应该转而监视场景生命周期 这样你的app才会响应 特定于桌面上每个窗口内容的事件 请记住 Mac Catalyst app 不会像iPad app一样 那么频繁收到 sceneDidEnterBackground事件 当桌面窗口被缩小或关闭时 场景会进入背景状态 如果你的app使用 sceneDidEnterBackground 来进行一些常规作业 例如自动储存一份文件 改用计时器 会确保这个动作定时进行 最后 请记住你的Catalyst app 可能完全没有场景 却会持续在前景中运行 这种状态会在你的app的 所有窗口都被关闭时发生 但你的app名称会继续 显示在菜单栏中 现在我们来决定是否要优化 我们在Mac上的界面 当你首先开始 把你的app带来Mac时 这是最重要的决定之一 我们建议使用Mac idiom 使你的app在Mac上最能适应 但这的确需要一些额外的工作 在Mac idiom中 你的app 会以100%缩放大小来运行 为你提供完美像素的文字和图像 以及原生的AppKit控制权 如果你想要的话 可以把新的Mac特定资产 加入你的资产目录 以便利用这个额外的详细信息 提供1x和2x资产来支持 所有显示器分辨率 是一种很好的做法 请注意 你的许多控件的尺寸度量 会改变 所以一定要调整 你的app的布局 来适应 对于你的app里的自定义控件 你有一个额外的选择 你会自动获取Mac风格的控件 但如今你可以选择让你的按钮和滑块 弃用这种传统风格 转而使用Mac控件上 无法取得的自定义API 如果你使用任何自定义资产 例如设置UISlider上的缩略图 它们会比默认设置预期的更大 所以你可能需要缩放它们 或提供新资产 另外也请记住 Mac用户想要 AppKit风格的控件 所以应该谨慎使用自定义控件 想知道更多 关于Mac idiom的细节 请查看我们的视频 《优化你的 Mac Catalyst app的界面》 因为Mac idiom里的Catalyst app 使用AppKit控件风格 所以你的有些控件的 外观和行为会改变 在我们的视频 《关于Mac Catalyst的新信息》 我们介绍了新的弹窗按钮风格 这完善了我们的Mac按钮类型套件 让我们深入探索 使这些控件不同的因素 以及系统选择使用哪种控件的流程 了解这些控件 以及它们通常出现的位置 会帮助你周全地选择 它们在你的app中的用途 默认的UIButton类型是 UIButton type .system 使用这种按钮类型时 按钮会自动 呈现其上下文的预期外观 在Mac idiom中 这代表它成为一个带边框的按钮 下拉按钮是Mac原生控件 用于提供可能动作的列表 而且是用一个单箭头指示器下拉 有个好例子是打印对话中的 PDF下拉按钮 这会呈现“另存为PDF” 或“发送邮件” 要获取下拉按钮 请确保你已经通过按钮的菜单属性 分配一个UI菜单给按钮 并另外设置 showsMenuAsPrimaryAction为真 你的按钮会呈现下拉外观 并在单击后呈现菜单 使用macOS Monterey的 Catalyst的新功能 是弹出按钮 弹出按钮的外观类似下拉按钮 却有一个双箭头指示器 而且它们的功能稍有不同 下拉按钮会触发一个动作 而弹出按钮则用于从一组 相互独立的选项中选出一个 例如选择一周中的一天 接着按钮的标题会更新 来反映选中的选项 这是一种适合Mac的好选择 可以取代你的app里的 UIPickerView 获取这个控件类似下拉按钮 但属性 changesSelectionAsPrimaryAction 也必须是真 最后 复选框用于代表 非排他性二进制切换 而且比开关更适用于鼠标 结果证明 你不用做额外工作就能获取复选框! 只要确定开关有设定标题 也要记得标题属性 只有在Mac idiom受到支持 根据默认设置 开关 有自动的preferredStyle 而且你可以在运行时 利用只读风格属性来确认 它是开关还是复选框 现在 为了深入探索 一些特定代码变化 让我把镜头交给我的同事戴夫 大家好 我叫戴夫 我是UIKit团队的工程师 我们来谈谈你可以做的一些事 来让你的Mac Catalyst app 更加适应运作环境 Mac Catalyst app或许能存取 远远更多屏幕房地产 你的app窗口在Mac上 可以比在iPad上 调得更大 而且能以全屏幕显示 请花点时间调整你的app窗口大小 并注意它的布局 请确定你正在使用额外空间 来显示更多内容和控件 让你的app更容易使用 实时调整大小会测试你的app 布局表现 你的app应该在布局期间尽量 做最少的工作 使你的app窗口在调整大小时 依然有反馈 请特别注意你的app中 依赖模态演示和弹出框的交互 因为有较大的展示区 你可以通过将它们显示为子视图 使这些交互始终可用 现在 让我们谈谈指针输入设备吧 请记住 不是所有Mac都有触控板 而且有些Mac会连接 不支持滚动的输入设备 如果你的视图需要 捏合或旋转手势来作用 请确保所有功能都可以用鼠标 就能做到 不需要滚动输入 请把额外按钮或其他控件加入 你的Mac Catalyst app视图 来确保它的所有功能都能实现 另外 侦测键盘修改器 或点击或拖拽手势识别器 有时可以提供对视图功能的 更快访问 比如让Shift-拖拽发挥缩放功能 让我们来谈谈键盘快捷键与主菜单 Mac app的主菜单非常适合 探索你的app可用的所有动作 以及关联的键盘快捷键 如果你的app已经通过从响应者 返回键盘指令来支持键盘快捷键 就改用菜单建构器API 把这些指令加入主菜单 把你的所有键盘快捷键移到主菜单 使它们即使现在无法启动 也可以被探索 另外 利用MenuBuilder API 来组织Mac Catalyst上的快捷键 也会在iPad快捷键覆盖上组织它们 当你建构主菜单时 请务必加入所需的一切动作 以便跟你的app互动 以iPad上的手势来执行的动作 也应该可以通过 从主菜单选择项目来访问 把键盘快捷键加入你的菜单项目 会提供对这些动作的更快访问 因为菜单栏和键盘指令动作 是从第一响应者开始路由的 所以请确保 会是这些动作的目标的视图 能够成为第一响应者 而且可以接受焦点 要做到这件事 你可以让你的视图 为canBecomeFirstResponder 和canBecomeFocused属性返回真 由于Mac app必须 较少依赖视图的直接操纵 较多依赖用户选择视图 然后从主菜单选择动作 所以你的app的更多视图 成为第一响应者与焦点的能力 在Mac Catalyst也更加重要 想知道更多关于焦点 和第一响应者的信息 请查看视频 《iPad键盘导航的焦点》 当我们谈到响应者时 请务必在你的app中 保持未修改的响应者链 换句话说 请不要覆盖nextResponder 让响应者链保持未修改状态 能确保Mac Catalyst可以把你的动作 路由到适当的目标 如果你的app必须使用 不在响应者链中的对象 来处理某些动作 请使用target(for Action:, withSender:)函数 转而把这些动作 委托给适当对象 让我们来看看代码 在这个例子中 我们的视图 把setAsFavorite动作 委托给一个模型对象 同时允许其他动作 继续向上传播响应者链 现在 我们来谈谈场景 以及它们在Mac Catalyst app的 运作方式 Mac app可能同时开启 许多桌面窗口 在一个Mac Catalyst app中 每一个窗口 都配对一个UIWindowScene 你的app可能提供 具有不同功能的窗口 举例来说 它可能有一个文件窗口 一个详细信息查看器窗口 一个消息编辑器窗口 诸如此类的窗口 组织这些不同场景功能的最佳方法 是为每个类型的窗口 定义场景配置 为了定义场景配置 请把它们添加到 Application Scene Manifest 条目下的Info.plist 在Application Session Role数组下 为你的app支持的每种场景 创建一种配置 为每种配置命名 并选择场景类别、委托类别 以及场景创建时会被实例化的 情节提要 现在我们已经定义了我们的场景配置 让我们来讨论我们可以怎么使用它们 以便创建特定配置的新场景 在这个例子中 我们希望在双击视图时 创建一个新的详细信息查看器场景 我们做的第一件事是 定义一个新的用户活动类型 来请求一个详细信息查看器场景 我们会称它为 viewDetailActivityType 当我们创建这种新的用户活动时 我们希望传递我们想要 详细显示的项目的标识符 为了做到这点 我们定义一个itemIDKey 这个itemIDKey会在 用户信息字典中包含信息 接着 在我们的双击事件处理器里 我们创建适当类型的 一个新的NSUserActivity对象 将userInfo属性设置为 包含我们要显示的 itemID的字典 最后 我们调用UIApplication requestSceneSessionActivation函数 传入我们刚刚创建的用户活动 这会使系统创建我们的新场景 所以现在我们知道 如何为特定的用户活动类型 请求新场景了 现在我们来谈谈如何使用这个信息 来加载适当的场景配置 我们响应场景创建请求的方法是 在应用程序委托中执行应用程序 configurationForConnecting函数 在我们的执行中 我们检查了 传入的场景请求是否 包含任何用户活动 请求能包含多个用户活动 但在这个代码例子 我们只会检查第一个活动 如果有一个我们需要处理的活动 我们会检查 它的activityType 这里我们测试它是否等同 viewDetailActivityType 如果等同 我们会返回 名称是DetailViewer的 场景配置 这会使系统在我们的Info.plist 寻找具有该名称的配置 并加载适当的场景和场景委托类别 在新的桌面窗口 展示指定的情节提要 如果没有指定的场景配置应该被加载 我们就会返回默认配置 还有一件事要做 还记得我们储存了 要显示项目的itemID吗? 我们仍然需要在我们刚创建的场景的 视图控制器上设置那个值 我们在SceneDelegate类别中 做这件事 在场景即将在桌面上显示之前 调用场景willConnectTo会话函数 之前传入我们的 应用程序委托的用户活动也被传入 场景委托中的这个函数 我们现在能从它的userInfo字典中 抽取itemID 并在新的视图控制器上设置它 使用NSUserActivity 来配置新场景 也让你的app更容易支持状态回复 如果你的场景委托响应 stateRestorationActivity (for Scene:)回调 那么当你的app退出时 返回的用户活动 会被系统储存 如果“系统偏好”启用状态回复 下次你的app启动时 系统会重建你的场景 并把每个场景的用户活动对象传到 你的app委托的应用程序 configurationForConnecting SceneSession函数 这跟你的app创建新场景时 调用的函数 是一样的 就像之前介绍的一样 通过使用一致的活动类型组 当你的app创建新的桌面窗口 以及状态回复的时候 你可以使用相同代码 来选择合适的场景 你需要添加一样东西到你的场景委托 这样你的app才能用相同代码 处理新场景请求与状态回复 那就是在你的场景委托中 修改你的场景willConnect会话函数 这样如果场景链接选项的活动是nil 它会返回 stateRestorationActivity 现在你的app已经准备好 处理新场景请求 和状态回复 想知道更多关于状态回复的信息 请查看 《介绍iPad上的多窗口》视频 接着 我们来谈谈你的app工具栏 出色的Mac app会用窗口的工具栏 来呈现经常使用的动作 和其他导航选项 以便迅速访问 跟iOS上的工具栏不同的是 Mac Catalyst app 桌面窗口上的工具栏 不会随着视图控制器 在“分屏”控制器 或导航控制器中的出现和消失而改变 因为工具栏与场景有强烈关联 所以配置工具栏的最佳位置 就是在你的场景委托子类里 通常出现在工具栏上的一个重要项目 是“共享”按钮 把NSSharingServicePickerToolbarItem 添加到你的工具栏 让你的app能用Mac的标准共享菜单 来分享场景中显示的主要内容 在macOS Monterey 我们添加了 按钮自动使用场景共享的 活动项目配置的能力 请注意 这跟Siri的 新“共享此项目”功能 在iOS上使用的配置是一样的 为你的场景提供共享配置的 一个好方法是 从你的RootViewController的 activityItemsConfiguration属性 返回一个对象 在Mac Catalyst上 你的app工具栏中的 NSSharingServicePicker ToolbarItem 会自动使用这个属性 在iOS上 Siri会以相同属性 使用“共享此项目” 来共享数据 当然 工具栏并非你的app 能提供项目来分享的唯一位置 你往往会想要允许通过上下文菜单 来共享图像或其他项目 为了做到这点 需要从你的视图 返回activityItemsConfiguration对象 然后添加 contextMenuInteraction 这里显示的是Mac Catalyst 和iPad上的结果 在Mac Catalyst上 请注意“复制”动作和“共享”菜单 被自动添加了 而当你的app在iPad上运行时 “复制”和“共享”动作被添加了 点击“共享”动作 会自动呈现共享工作表 使用“活动项目配置”API 让你的app能宣布 它的视图能共享什么 这样系统就能在 各平台上展示合适的UI 既然我们已经讨论过 你的app可以如何共享数据 让我们来谈谈你的app 可以如何利用“连续性相机” 从iPhone或iPad导入图像 如果你的app使用UITextView 来展示丰富的文字 “连续性相机”支持 会在macOS Monterey 自动启用 在文字视图上单击鼠标右键 会显示一张上下文菜单 里面有个选项是 在iPhone或iPad上拍照 并自动将其当作附件来添加 想要把提供给“连续性相机”的支持 添加到任何视图 只需要从你的视图的 pasteConfiguration属性 返回一个UIPasteConfiguration对象 然后添加UI contextMenuInteraction 接着 执行paste(itemProviders:)函数 来加载及粘贴传入的对象 在这个例子指的就是图像 另外 从你的视图 返回粘贴配置 不仅会在配置接受图像时 启动“连续性相机” 也会自动启动上下文菜单中的 “粘贴”动作 并让你的视图 在Mac Catalyst和iPad上 接受传入的拖曳项目 所以这些是你可以做的特定操作 来帮助你的app成为 出色的Mac Catalyst app 现在 让我们把镜头还给欧文 来讨论分发 谢谢你 戴夫 当我们谈到发布你的app时 需要记住的重点是 Mac Catalyst app 是Mac app 可以通过 跟其他任何Mac app一样的 所有方法来分发 你可以在Mac App Store 发布你的app 里面有个选项是“通用购买” 这样你原有的iOS顾客 就会自动获取你的Mac app 你可以访问TestFlight 来发布app的beta版 并获得有关新版本的早期反馈 你也可以使用“App公证” 然后自行分发 如果你开发了一个框架 可以使用XCFramework 进行跨平台分发 将所有平台的二进制文件捆绑在一起 今天 我们已经讨论了 用Mac Catalyst建构适用Mac的 iOS app的流程 并突出显示在这个流程中 做出的一些重要决定和改变 现在该是时候考量你自己的计划了 要让你的app在Mac上运行是很容易的 只需要一点操作 你就能让你的app完全适用Mac 并将它提供给一群兴奋的新顾客 谢谢你们! ♪
-
-
6:50 - System button
let button = UIButton(type: .system) button.setTitle("Button", for: .normal)
-
7:06 - Pull-down button
button.menu = UIMenu(...) button.showsMenuAsPrimaryAction = true
-
7:44 - Pop-up button
button.menu = UIMenu(...) button.showsMenuAsPrimaryAction = true button.changesSelectionAsPrimaryAction = true
-
8:24 - Checkbox
let checkbox = UISwitch() if checkbox.style == .checkbox { checkbox.title = "Checkbox" }
-
13:20 - Delegating actions
final class MyView: UIView { override func target(forAction action: Selector, withSender sender: Any?) -> Any? { if action == #selector(Model.setAsFavorite(_:)) { return myModel } else { return super.target(forAction: action, withSender: sender) } } }
-
14:43 - Requesting a new scene
let viewDetailActivityType = "viewDetail" let itemIDKey = "itemID" final class MyView: UIView { @objc func viewDoubleClicked(_ sender: Any?) { let userActivity = NSUserActivity(activityType: viewDetailActivityType) userActivity.userInfo = [itemIDKey: selectedItem.itemID] UIApplication.shared.requestSceneSessionActivation(nil, userActivity: userActivity, options: nil, errorHandler: { error in //... }) } //... }
-
15:57 - Responding to a new scene request
let viewDetailActivityType = "viewDetail" final class AppDelegate: UIApplicationDelegate { func application(_ application: UIApplication, configurationForConnecting session: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { if let activity = options.userActivities.first { if activity.activityType == viewDetailActivityType { return UISceneConfiguration(name: "DetailViewer", sessionRole:session.role) } } return UISceneConfiguration(name: "Default Configuration", sessionRole: session.role) } //... }
-
17:13 - Setting item ID on new scene's root view controller
let itemIDKey = "itemID" final class SceneDelegate: UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options: UIScene.ConnectionOptions) { if let userActivity = connectionOptions.userActivities.first { if let itemId = userActivity.userInfo?[itemIDKey] as? ItemIDType { // Set item ID on new view controller } } //... } //...
-
17:47 - Saving state for later restoration
final class SceneDelegate: UIWindowSceneDelegate { func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { //... } }
-
17:57 - State restoration
final class AppDelegate: UIApplicationDelegate { func application(_ application: UIApplication, configurationForConnecting session: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { //... } }
-
18:42 - Handle both new scene requests and state restoration
let itemIDKey = "itemID" final class SceneDelegate: UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity { if let itemId = userActivity.userInfo?[itemIDKey] as? ItemIDType { // Set item ID on new view controller } } } }
-
20:20 - Provide sharing configuration for the scene
final class RootViewController: UIViewController { override var activityItemsConfiguration: UIActivityItemsConfigurationReading? { get { UIActivityItemsConfiguration(objects: [image]) } //... } }
-
20:56 - Support sharing through context menu
final class MyView: UIView { override var activityItemsConfiguration: UIActivityItemsConfigurationReading? { get { UIActivityItemsConfiguration(objects: images) } //... } func viewDidLoad() { let contextMenuInteraction = UIContextMenuInteraction(delegate: self) addInteraction(contextMenuInteraction) } }
-
22:08 - Supporting continuity camera
final class MyView: UIView { override var pasteConfiguration: UIPasteConfiguration? { get { UIPasteConfiguration(forAcceptingClass: UIImage.self) } //... } func willMove(toWindow: UIWindow) { addInteraction(contextMenuInteraction) } override func paste(itemProviders: [NSItemProvider]) { for itemProvider in itemProviders { if itemProvider.canLoadObject(ofClass: UIImage.self) { if let image = try? await itemProvider.loadObject(ofClass:UIImage.self) { insertImage(image) } //...
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。