大多数浏览器和
Developer App 均支持流媒体播放。
-
AppKit 的新功能
探索 Mac App 开发方面的最新进展。简要了解 macOS Sequoia 的新功能,以及如何将这些功能应用到你的 App 中。探索将现有代码与 SwiftUI 整合在一起的多种新方式。了解工具栏、菜单、文本输入等各种 AppKit 控件的改进。
章节
- 0:00 - Introduction
- 0:49 - New macOS features
- 0:52 - Writing Tools, Genmoji, and Image Playground
- 3:31 - Window Tiling
- 6:21 - More SwiftUI integrations
- 6:41 - Build menus with SwiftUI
- 7:39 - Get animated with SwiftUI
- 8:20 - API refinements
- 8:44 - Context menu refinements
- 9:42 - Text highlighting
- 11:00 - SF Symbols
- 11:59 - Save Panel refinements
- 13:04 - Cursors refinements!
- 15:21 - Toolbar refinements
- 17:22 - Text entry suggestions
资源
相关视频
WWDC24
WWDC23
-
下载
大家好!我叫 Matt Zanchelli 是 AppKit 团队的一名工程师 本讲座的主题是 AppKit 的新功能 如果你和我一样喜欢 AppKit 和 Mac 这个讲座一定非常适合你 我将介绍 macOS Sequoia 的多项改进 并分享如何让你的 AppKit App 变得更加出色 这个视频将涵盖多个主题 我们先来了解一些系统范围的新功能 以及你的 App 如何采用这些功能 然后 我将介绍 AppKit 的 一些变化 这些变化 在你使用 SwiftUI 时可以提供帮助 最后 我将分享框架中多项 重要的全新 API 功能优化
我们先来了解一下 macOS 新功能 有了 Writing Tools 现在 macOS 不仅可以帮助你纠正拼写和语法问题 还能实现更复杂的写作概念 比如结构、条理清晰和语气 我们一直在努力 将这些写作工具应用到整个系统 让你的 App 能够自动获取这些智能功能 要在你的 App 中 提供卓越的写作体验 请思考它们带来的互动行为 如果你的 App 需要处理大量 文本输入 或者需要通过文本视图 执行高级操作 请务必观看 “开始使用 Writing Tools” 我创作了一些一直想要的表情符号 从中获得了很多乐趣 我现在可以表达非常具体的 情绪、物体和场景 然后在“信息”和“备忘录”等 App 中与朋友分享 我很高兴也能在你的 App 中创作 并使用这些新的表情符号 你创作的新表情符号是图像 而不仅仅是 Unicode 字符 所以 要在你的 App 中显示和存储 这些与文本内联的图像 可能需要进行一点调整
请观看 “借助 Genmoji 将表情引入 App” 了解如何在 App 中采用 自定表情符号
除了自定表情符号 用户现在还可以 在新推出的“Image Playground” App 中创作完整图像 你也可以采用这个全新的 “Image Playground”体验 在你的 App 中 启用这项神奇的图像创作功能 下面我将介绍如何将 “Image Playground”体验添加到 你的 App 中 首先 初始化“Image Playground” 视图控制器的实例 并为这个实例分配委托 委托将监听重要的生命周期事件 例如 图像创作完成或取消 如果你的 App 中有一些 特定的上下文 引导进入“Image Playground”体验 你的 App 可以选择性地 使用一些初始概念 和源图像来设置视图控制器 概念描述了输出图像的预期内容 而源图像将用作所创作图像的 图形参考 这两个属性可让查看表单的人员 快速开始创作他们的图像 他们仍然可以在图像创作表单中 选择不同的图像和概念 然后 将视图控制器 显示为表单以开始创作
创作图像后 视图控制器的委托会收到一个回调 其中包含对图像文件 URL 的引用 这个文件 URL 位于 App 经过沙盒化处理的临时目录中 使用这个文件 URL 将图像插入用户界面 然后关闭 Playground 表单 如果你的 App 允许 插入照片图库、“访达” 或连续互通相机中的图像 可以考虑添加“Image Playground” 作为其他图像来源 将“Image Playground”体验 整合到你的 App 中 就是这么简单 我已经迫不及待地想和大家分享 macOS Sequoia 中我最喜欢的 一项新功能 即 Window Tiling 下面我就来展示一下这项新功能
Window Tiling 可以让你快速 按某些常用的排列方式来排布窗口 我将开始移动这个窗口 并将光标放在屏幕的右侧边缘
将这个窗口放到这里 它将占据这一半屏幕
如果我将这个窗口拖出它的平铺位置 它就会恢复到平铺前的大小
如果我想让一个窗口突出显示一会儿 完成后恢复为常规大小 就非常适合使用这项功能 我甚至不需要将光标 一直移动到屏幕边缘就能平铺窗口 按住“Option”键同时拖移 会立即显示最近的平铺区域的预览 然后把它放到这个区域中就行了 我还可以在“Window”>“Move & Resize”菜单中 访问这些 Window Tiling 选项
在这里 我还能看到所有键盘快捷键 通过使用这些快捷键 我可以快速来回移动窗口
像这样 像这样 像这样
如果我想要这两个窗口并排显示 也有相应的 排列方式
除了“Window”菜单 我现在还可以在窗口标题栏中 便捷地使用这些选项
当两个窗口像这样并排平铺时 我可以同时调整这两个窗口的大小 从而获得我认为合适的比例
当打开新窗口时 它会以平铺前的大小显示
要排列所有这三个窗口 我可以选择三窗口排列方式 太酷了 Window Tiling 是一项非常棒的功能 适用于你在 macOS Sequoia 上的所有 App 要让你的 App 充分利用 Window Tiling 你需要考虑一些事项 窗口的最小尺寸和最大尺寸 使用 Window Tiling 用户可以让 窗口占据屏幕的 1/2 或 1/4 如果你的 App 窗口尺寸足够灵活 则可以避免窗口重叠 检查那些会促成窗口 最小尺寸的窗口布局约束
如果只能以固定的宽度和高度增量 调整窗口大小 请使用 resizeIncrements 属性 这可用于以单个字符的 宽度或高度为增量来调整窗口大小 就像在“终端”中一样 当打开新窗口时 思考它们相对于现有窗口的位置 使用新的 cascadingReferenceFrame 属性 来获取现有窗口的平铺前边框 相对于这个边框来层叠新打开的窗口 或者 如果你使用的是 NSWindowController 则这一行为在 macOS Sequoia 中 是一种默认行为 我喜欢在我的 Mac App 中使用 SwiftUI 因为这是构建用户界面的一种好方法 它从一开始就可以 与 AppKit 协同工作 便于逐步采用 多年来 我们一直在 深化两者之间的这种整合 并在 macOS Sequoia 中 进一步将其完善 就像你可以在 AppKit App 中通过 NSHostingView 使用 SwiftUI 视图 你现在也可以使用 SwiftUI 菜单了 这让你能够在那些使用 AppKit 的 App 部分和使用 SwiftUI 的 App 部分之间共享菜单定义 你可以在 AppKit 中通过 NSHostingMenu 使用 SwiftUI 菜单 NSHostingMenu 是一个 新的 NSMenu 子类 操作起来很简单
使用 SwiftUI View 创建菜单定义 在主体中 使用数据关系描述得最准确的 SwiftUI 视图 使用 Toggle 来打开和关闭值 使用 Picker 从列表中选择一个值 使用 Button 来执行操作
通过这个 SwiftUI 视图 初始化 NSHostingMenu 然后将它用于接受 NSMenu 的 任意 AppKit 上下文 例如包含下拉菜单参数的 新 NSPopUpButton 构造器 在 macOS Sequoia 中 AppKit 借助 SwiftUI 实现了动画效果! 你现在可以使用 SwiftUI 动画类型为 NSView 添加动画效果! 这样一来 你就能利用 全套强大的 SwiftUI 动画类型 包括 SwiftUI CustomAnimations!
要为 NSView 添加动画效果 使用 NSAnimationContext 传入 SwiftUI 动画类型 然后调整布局或绘图 SwiftUI 动画甚至 可中断且可重定目标
如需进一步深入了解 UIKit 和 AppKit 中的 SwiftUI 动画 请观看视频 “提升 UI 动画和过渡效果” 接下来 我将分享一些 出色的 AppKit API 功能优化
包括打开上下文菜单的新方式 文本系统和 SF Symbols 的新功能 保存文稿的新便捷方法 一些新光标 更好地控制工具栏 以及令人期待已久的用于协助文本输入的新 API macOS Sequoia 的一项新功能是 可以使用键盘 为当前获得焦点的 UI 元素 打开上下文菜单 用户可以使用这项功能更快、 更轻松地访问 App 的功能 这个快捷键 默认为 control-return 不过可以在“系统设置”中进行自定 但是 如果使用键盘而不是鼠标 显示上下文菜单 菜单将从哪里显示? 我将展示如何影响 这些上下文菜单的位置
如果视图的 menu 属性有一个值 菜单将自动位于视图边界的上方
如果视图绘制了自定选择 则实现新的 NSViewContentSelectionInfo 协议 以提供有关所选内容的几何信息 然后 视图的菜单将相应地 位于所选内容附近 可采用这种方式来控制 通过键盘显示的上下文菜单的位置 我要分享的下一个新 API 功能优化 是文本高亮显示
高亮显示可用于使用背景颜色 以及对比鲜明的前景颜色来强调文本 这适用于任何支持富文本的 NSTextView 首先 选中文本范围 然后 右键点按并使用“Font”菜单 导航到“Highlight”子菜单 你可以从多种 高亮显示配色方案中进行选择 也可以使用 App 的强调色 对于 TextEdit 强调色为蓝色 虽然系统针对富文本文本视图 自动支持这一操作 但如果 你的 App 具有自定文本属性控件 你可能希望实现这项新功能
文本高亮显示由 AttributedString 属性控制 新的 .textHighlight 属性 对应于文本高亮样式 将它设置为 .systemDefault 以指明应高亮显示的文本范围 使用的颜色基于 App 的强调色 要控制颜色 应使用新的 .textHighlightColorScheme 属性 并将它与系统提供的 某种配色方案相关联 比如粉红色 macOS Sequoia 随附了 SF Symbols 6 它不仅包含 800 多个新符号 (涵盖各种主题)
还包含更多效果 比如摆动、 旋转 和呼吸
此外 它还提供用于实现符号效果的 新播放选项 比如将某种效果重复特定的次数 或者连续循环播放动画
我最喜欢的新增功能是 替换符号的徽章或添加斜线 太神奇了 新的符号效果真是太酷了! 如需进一步了解这些内容 以及如何创作自定符号 请观看“SF Symbols 6 的新功能” 如需进一步了解如何使用 这些富有表现力的动画 请观看 “在 App 中为符号添加动画效果” 接下来 我将介绍 “保存面板”的增强功能
保存文稿时 选择文件格式通常很便捷 你如果想要保存 就能直接在“保存面板”中完成 在 macOS Sequoia 中 现在提供标准文件格式选择器 你无需为这个选择器 创建自定补充视图 使用标准文件格式选择器非常简单 只需将“保存面板”上的 showsContentTypes 属性设置为 true 选择器包含“保存面板”支持的 每种允许内容类型的选项 选项由现有的 allowedContentTypes 属性指定 默认情况下 每个菜单项都使用 内容类型的本地化描述 要提供自定显示名称 请实现 panel(_ displayNameFor type:) 委托函数 在这个函数中会返回 适合 App 上下文的内容类型名称
希望这项新的“保存面板”增强功能 能为你节省一些开发时间 现在 某项功能 在使用了鼠标多年之后 终于在 macOS SDK 中 首次公开推出 你应该已经猜到了!就是光标! 系统光标现已可在macOS Sequoia SDK 中使用 我们先了解一下 frameResize 光标
这些光标用于从元素的边缘和角落 调整元素的大小 它需要两个参数 第一个是“position” 这指的是要与光标交互的边缘或角落 第二个是“directions” 指的是 可以朝哪个方向调整元素的大小 处理以下情况 元素处于最小尺寸 或最大尺寸 frameResize 光标主要用于 调整单个元素的大小 如果你要在两个元素之间移动分隔符 可以使用 columnResize 和 rowResize 光标 垂直于光标图形中的箭头的条形 描述了要调整大小的分隔符 这些光标用于调整表格列的宽度 或电子表格中行的高度 指定分隔符可以移动的方向 以处理分隔符处于极限值的情况
对于缩放操作 可以使用新的 zoomIn 和 zoomOut 光标 当点按操作将使 App 的内容 放大或缩小时 可以使用这些光标 这些都是 AppKit 中 新推出的系统光标 使用系统光标可以为你提供标准外观 确保各个 App 保持一致 如果 App 的自定光标 无法传达系统光标不支持的内容 问问你自己是否真的需要自定光标 标准光标不仅更加易于使用 无需自行绘制 而且还支持更大的辅助功能尺寸 具体取决于“系统设置”中 “辅助功能”部分的设置 还有一些设置 用于为依赖更独特颜色的光标 调整指针颜色 以便高效地查看和找到光标 接下来 我将介绍 NSToolbar 的 三项增强功能 这些功能使你 能够更好地控制显示模式、 显示的项目以及项目的可见性 NSToolbar 支持使用或不使用 文本标签来显示项目 虽然 App 的 首选样式可能是纯图标 但有些用户觉得 如果他们还能够浏览文本标签 则可以更轻松地找到某个工具栏项目 所以最好为用户提供选择 在 macOS Sequoia 中 你现在可以提供这一样式选择 即使工具栏的内容无法自定也一样 方法就是使用 allowsDisplayModeCustomization 属性 默认情况下 它处于启用状态 确保你的工具栏具有标识符 以便 AppKit 能够保存样式偏好 还要仔细检查以保证 所有的工具栏项目都有合适的标签 为了方便管理 NSToolbar 的现有项目属性 你可以使用新的 itemIdentifiers 属性 设置工具栏的项目标识符 会自动为你实现最少的添加和删除操作 请注意 如果启用allowsUserCustomization 更改值将覆盖所有自定值 因此 请仅针对动态、不可自定的 工具栏使用这一属性 最好只是停用不适用于 当前选择的项目 如果窗口的模式发生变化 比如从文稿编辑模式更改为查看模式 仅以编程方式从工具栏删除项目
你还可以使用 新的 isHidden 属性 有条件地隐藏和显示工具栏项目 隐藏的项目仍会出现在自定设置中 所以用户可以选择当这些项目变得 可见时 希望这些项目在哪里显示 应在某个项目不适用时使用这个属性 无论窗口中的当前选择是什么
例如 App 可以隐藏某一显示下载的项目 直到首次下载开始
接下来 我想分享一个全新的 API即文本输入建议 当用户键入内容时 它能让你的 App 在系统标准建议菜单中 提供定制建议 这种模式很常见 许多 App 中都有 不过如今在 macOS Sequoia 的 AppKit 中进行了标准化 它适用于任何 NSTextField 包括 NSSearchField 等子类 首先 针对文本字段 设置 suggestionsDelegate 属性 当键入文本时 委托需要提供建议 并能够同步和异步 返回结果 它还可以选择性地 通过高亮显示和进行选择 来自定文本补全 在使用文本输入建议时 有一些设计技巧 确保在任何给定时间点 显示的建议都与键入的文本相关 因为用户期望界面能够与他们的键入速度保持同步 提供一致且可预测的建议 以便保持肌肉记忆 并建立对结果的信任 当提供异步建议时 将这些建议放在 已提供的即时建议之后 保持简单 只提供最重要的结果和详细信息 以便用户能够 轻松快速地找到所需结果 这些只是 macOS Sequoia 中的部分 AppKit 增强功能 还有更多精彩功能待你发掘 你可以在 developer.apple.com/cn/ 上的发布说明中查看所有增强功能
开始使用新的智能功能 并采用 API 将这些功能无缝整合到你的 App 中
利用 Window Tiling 摆脱窗口面板 确保你的 App 窗口可供清理和平铺 通过新的菜单和动画 API 继续以增量方式采用 SwiftUI 采用新的系统标准组件 比如内容类型选择器、 光标和文本输入建议 借助通过键盘显示的上下文菜单 让用户快速浏览 App 并确保工具栏支持所有显示模式
非常感谢大家观看 也非常感谢大家 为 Mac 开发出色的 App 如果将这些 API 比作颜料 那么 Xcode 就是你的画笔 而 Mac 就是你的画布! 继续加油 向世界展示你的创作能力吧!
-
-
2:09 - Adding the Image Playground experience
extension DocumentCanvasViewController { @IBAction func importFromImagePlayground(_ sender: Any?) { // Initialize the playground, get set up to be notified of lifecycle events. let playground = ImagePlaygroundViewController() playground.delegate = self // Seed the playground with concepts and source imagery. (Optional) playground.concepts = [.text("birthday card")] playground.sourceImage = NSImage(named: "balloons") presentAsSheet(playground) } } extension DocumentCanvasViewController: ImagePlaygroundViewController.Delegate { func imagePlaygroundViewController( _ imagePlaygroundViewController: ImagePlaygroundViewController, didCreateImageAt resultingImageURL: URL ) { if let image = NSImage(contentsOf: resultingImageURL) { imageView.image = image } else { logger.error("Could not read image at \(resultingImageURL)") } dismiss(imagePlaygroundViewController) } }
-
5:50 - Using window resize increments
window.resizeIncrements = NSSize(width: characterWidth, height: characterHeight)
-
7:05 - Build menus with SwiftUI
struct ActionMenu: View { var body: some View { Toggle("Use Groups", isOn: $useGroups) Picker("Sort By", selection: $sortOrder) { ForEach(SortOrder.allCases) { Text($0.title) } }.pickerStyle(.inline) Button("Customize View…") { <#Action#> } } } let menu = NSHostingMenu(rootView: ActionMenu()) let pullDown = NSPopUpButton(image: image, pullDownMenu: menu)
-
7:43 - Get animated with SwiftUI
NSAnimationContext.animate(with: .spring(duration: 0.3)) { drawer.isExpanded.toggle() }
-
7:55 - Get animated with SwiftUI
class PaletteView: NSView { @Invalidating(.layout) var isExpanded: Bool = false private func onHover(_ isHovered: Bool) { NSAnimationContext.animate(with: .spring) { isExpanded = isHovered layoutSubtreeIfNeeded() } } }
-
10:31 - Text highlighting
let attributes: [NSAttributedString.Key: Any] = [ .textHighlight: NSAttributedString.TextHighlightStyle.systemDefault, .textHighlightColorScheme: NSAttributedString.TextHighlightColorScheme.pink, ]
-
11:11 - SF Symbols effects
imageView.addSymbolEffect(.wiggle) imageView.addSymbolEffect(.rotate) imageView.addSymbolEffect(.breathe)
-
11:24 - SF Symbols playback (periodic)
imageView.addSymbolEffect(.wiggle, options: .repeat(.periodic(3, delay: 0.5)))
-
11:30 - SF Symbols playback (continuous)
imageView.addSymbolEffect(.wiggle, options: .repeat(.continuous))
-
11:37 - SF Symbols magic replace
imageView.setSymbolImage(badgedSymbolImage, contentTransition: .replace)
-
12:19 - Save panel content types
extension ImageViewController: NSOpenSavePanelDelegate { @MainActor @IBAction internal func saveDocument(_ sender: Any?) { Task { let savePanel = NSSavePanel() savePanel.delegate = self savePanel.identifier = NSUserInterfaceItemIdentifier("ImageExport") savePanel.showsContentTypes = true savePanel.allowedContentTypes = [.png, .jpeg] let result = await savePanel.beginSheetModal(for: window) switch result { case .OK: let url = savePanel.url // Save the document to 'url'. It already has the appropriate extension. case .cancel: break default: break } } } func panel(_ panel: Any, displayNameFor type: UTType) -> String? { switch type { case .png: NSLocalizedString("PNG (Greater Quality)", comment: <#Comment#>) case .jpeg: NSLocalizedString("JPG (Smaller File Size)", comment: <#Comment#>) default: nil } } }
-
13:34 - Frame-resize cursors
let cursor = NSCursor.frameResize(position: .bottomRight, directions: .all)
-
14:20 - Column and row resize cursors
let cursor = NSCursor.columnResize(directions: .left) let cursor = NSCursor.rowResize(directions: .up)
-
14:29 - Zoom in and out cursors
let cursor = NSCusor.zoomIn let cursor = NSCusor.zoomOut
-
15:57 - Display mode customizable toolbar
let toolbar = NSToolbar(identifier: NSToolbar.Identifier("ViewerWindow")) toolbar.allowsDisplayModeCustomization // Defaults to `true`.
-
16:57 - Hidden toolbar items
let downloadsToolbarItem: NSToolbarItem downloadsToolbarItem.isHidden = downloadsManager.downloads.isEmpty
-
17:49 - Text entry suggestions
class MYViewController: NSViewController { let museumTextField = NSTextField(string: "") let museumTextSuggestionsController = MuseumTextSuggestionsController() override func viewDidLoad() { super.viewDidLoad() self.museumTextField.suggestionsDelegate = self.museumTextSuggestionsController } } class MuseumTextSuggestionsController: NSTextSuggestionsDelegate { typealias SuggestionItemType = Museum func textField( _ textField: NSTextField, provideUpdatedSuggestions responseHandler: @escaping ((ItemResponse) -> Void) ) { let searchString = textField.stringValue func museumItem(_ museum: Museum) -> Item { var item = NSSuggestionItem(representedValue: museum, title: museum.name) item.secondaryTitle = museum.address return item } let favoriteMuseums = Museum.favorites.filter({ $0.matches(searchString) }) let favorites = NSSuggestionItemSection( title: NSLocalizedString("Favorites", comment: "The title of suggestion results section containing favorite museums."), items: favoriteMuseums.map(museumItem(_:)) ) var response = NSSuggestionItemResponse(itemSections: [favorites]) response.phase = .intermediate responseHandler(response) Task { let otherMuseums = await Museum.allMatching(searchString) let nonFavorites = NSSuggestionItemSection(items: otherMuseums.map(museumItem(_:))) var response = NSSuggestionItemResponse(itemSections: [ favorites, nonFavorites, ]) response.phase = .final responseHandler(response) } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。