ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
AppKitの新機能
Macアプリ開発における最新の進化を確認しましょう。macOS Sequoiaの新機能の概要、およびそれらをアプリに導入する方法をご紹介します。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
-
ダウンロード
こんにちは AppKitチームのエンジニアの Matt Zanchelliです 「AppKitの新機能」へようこそ 私と同じくらいAppKitとMacが好きな方に 最適なセッションです 今日は皆さんに macOS Sequoiaの様々な改善点と 素晴らしいAppKitアプリを 開発する方法をご紹介します このセッションでは幅広いトピックを 取り上げます まず システム全体の新機能と これらの機能をアプリで実装する方法 次に SwiftUIを使う際に役立つ AppKitの変更点 最後に フレームワークにおける 新しいAPIの改良点を紹介します
macOSの新機能から見ていきましょう Writing Toolsにより macOSがスペルや文法だけではなく 構成や明確さ トーンなどの より高度な作文の概念に対応しました Writing Toolsは システム全体に組み込まれていて インテリジェンス機能を アプリで自動的に使えます 最高の作文体験を実現するには アプリで行われる操作を考えましょう テキスト入力が多い場合や テキスト表示で高度な操作を行う場合は 「Get started with Writing Tools」を ご覧ください 私は ずっと欲しかった絵文字を いろいろ作って楽しんでいます 具体的な感情や物 場面を表現して メッセージやメモなどのアプリで 友だちと共有できます 新しく作成した絵文字はアプリでも使えます 新しい絵文字はUnicode文字ではなく 画像で表されるので 導入が簡単で テキストと一緒にインラインで表示して アプリに保存できます
カスタム絵文字を アプリに導入する方法については 「Bring expression to your app with Genmoji」をご覧ください
カスタム絵文字に加え 新しいImage Playgroundアプリで 完全な画像を作成できます 新しいImage Playgroundを導入することで アプリで魔法のように画像を作成できます Image Playgroundを アプリに追加する方法を説明します まず Image Playgroundの ビューコントローラインスタンスを初期化して デリゲートに割り当てます デリゲートは画像作成の完了や キャンセルなど 重要なライフサイクルイベントの 情報を取得します アプリ内で特定のコンテキストで Image Playgroundを開始する場合は ビューコントローラに最初のコンセプトと ソース画像を設定することもできます conceptsで出力画像の内容を説明します sourceImageは作成した画像を グラフィックで表したものです この2つのプロパティを使うことで シートを見れば すぐに画像を作成できます 画像作成シートで異なる画像や コンセプトを選択することもできます 次に ビューコントローラを 作成対象のシートとして提示します
画像が作成されると ビューコントローラの デリゲートがコールバックを受けて 画像のファイルURLを受け取ります ファイルURLはサンドボックス化された アプリの一時ディレクトリに入っています このファイルURLを使って 画像をユーザーインターフェイスに挿入したら Playgroundシートをdismissします フォトライブラリやFinder または連係カメラから画像をアプリに 挿入できる場合は Image Playgroundからも 画像を追加できるようにするとよいでしょう このようにImage Playgroundは 簡単にアプリに統合できます 次にご紹介するのは macOS Sequoiaの新機能の中でも 私が特に気に入っている 「ウインドウのタイル表示」です 早速ご覧いただきましょう
ウインドウを素早く動かして 一般的なレイアウトにできます まずこのウインドウを動かしてみましょう カーソルを画面の右端に置いて
この位置でウインドウをドロップすると 片側いっぱいに表示されます
タイル表示していた場所から ウインドウを動かすと 元のサイズに戻ります
ウインドウを大きくして 作業したいときに便利です そして作業が終わったら 元のサイズに戻せます 画面の端にカーソルを移動しなくても タイル表示にできます Optionを押しながらドラッグし 一番近いタイルのプレビューを表示して ドロップできます タイル表示のオプションはメニューの にもあります
キーボードショートカットもあります キーボードショートカットを使えば ウインドウを素早く動かせます
ほら さっと この通り
2つのウインドウを並べて配置したい場合も レイアウトを活用できます
メニューからも ウインドウのタイトルバーからも アクセスできるので便利です
ご覧のように2つのウインドウを 左右に並べているときは 両方の大きさを同時に変更して ちょうどよいサイズにすることができます
この状態でウインドウをもう1つ開くと 元のサイズで表示されます
この3つをすべてタイル表示にするには 3つのウインドウ用のレイアウトを選択します 素晴らしいでしょう? macOS Sequoiaでは あらゆるアプリの ウインドウをタイル表示にできます アプリを「ウインドウのタイル表示」対応に するには いくつかのポイントがあります まずは ウインドウの最小/最大サイズです ウインドウは画面の半分 または4分の1のサイズで表示できます ウインドウのサイズを 自由に変更できるアプリなら 複数のウインドウが重なることはありません 最小サイズが制限される場合があるため ウインドウレイアウトの制約に注意しましょう
ウインドウを固定の幅と高さの増分でのみ 変更できるようにするには resizeIncrementsプロパティを使います このプロパティを使うとターミナルと同様に ウインドウの幅や高さを 1文字分ずつ変更できます 新しいウインドウを開いたときに すでに開いているウインドウに対して どう配置されるかを考えましょう 新しいcascadingReferenceFrame プロパティを使って タイル表示されていない 既存ウインドウのフレームを確認し 新しいウインドウのカスケード位置を このフレームを基準に決めます NSWindowControllerなら macOS Sequoiaでは デフォルトで処理されます SwiftUIを使えば Mac用のアプリで 優れたユーザーインターフェイスを 簡単に構築できます AppKitとスムーズに連係するように 最初から設計されていて 段階的に導入できます 連係の度合いは年々深まり macOS Sequoiaでは より強固になっています AppKit内でSwiftUIビューを使えるだけでなく NSHostingViewにより SwiftUIメニューも使えるようになりました アプリ内でAppKitを使う部分と SwiftUIを使う部分で メニューの定義を共有できるということです AppKitのコンテキストでSwiftUIメニューを 使う場合は NSHostingMenuを利用します これはNSMenuの新しいサブクラスです 操作は簡単です
SwiftUIビューを使って メニューの定義を作成します 本文ではデータの関係を最も適切に 示している SwiftUIビューを使います 値のオンとオフを切り替える場合はToggle リストから値を選択する場合はPicker アクションにはButtonを選択します
このSwiftUIビューで NSHostingMenuを初期化して NSMenuに対応する AppKitの任意のコンテキストで使います 例えばプルダウンメニューパラメータがある NSPopUpButtonイニシャライザです macOS Sequoiaでは SwiftUIでAppKitをアニメーション化できます SwiftUIアニメーションを使って NSViewsをアニメーション化できます 強力なSwiftUIアニメーションを すべて使えます SwiftUI CustomAnimationsもその1つです
NSViewsをアニメーション化するには NSAnimationContextを使用し SwiftUIアニメーションタイプに渡して レイアウトや描画を調整します SwiftUIアニメーションは 中断可能かつリターゲット可能です
UIKitやAppKitを使った SwiftUIアニメーションの詳細については 「Enhance your UI animations and transitions」をご覧ください 次にAppKit APIの改善点を いくつかご紹介しましょう
コンテキストメニューの新しい表示方法 テキストシステムとSF Symbolsの新機能 ドキュメントの便利な保存方法 新しいカーソルや ツールバーの細かいコントロール 新しいAPIによる テキスト入力支援などです macOS Sequoiaの新機能として 選択中のUI要素のコンテキストメニューを キーボードを使って 表示できるようになりました この機能を使えば もっと素早く 快適に アプリの機能にアクセスできます このショートカットはデフォルトで Control + Returnキーですが システム設定でカスタマイズ可能です コンテキストメニューをマウスではなく キーボードで表示する場合 メニューはどこに表示されるのでしょうか? コンテキストメニューの 位置の決め方を説明します
メニュープロパティの値を ビューで指定してある場合 メニューは自動的に そのビューの範囲内に配置されます
独自の選択方法をビューに表示するには 新しいNSViewContentSelectionInfo プロトコルを実装して 選択インジケータの 配置情報を設定すると インジケータのそばに ビューのメニューが配置されます このように コンテキストメニューの位置は キーボードでコントロールできます 次に紹介するAPIの改善点は テキストのハイライトです
ハイライトでテキストを強調すると 背景色と前景色にコントラストが生まれます ハイライトはリッチテキストに対応する あらゆるNSTextViewで使えます まず 一定範囲のテキストを選択したら 右クリックしてメニューで サブメニューのを選択します ハイライト用の様々な配色から選ぶことも アプリのアクセントカラーを 使うこともできます TextEditの場合は青です リッチテキストのビューでは 自動的にハイライトが使えますが アプリで独自のテキスト属性コントロールが ある場合は この新機能を 実装することをおすすめします
テキストのハイライトは 属性付きの文字列属性で制御します 新しい.textHighlight属性で ハイライトのスタイルを指定します これを.systemDefaultに設定すると 一定範囲のテキストをハイライトできます ハイライトの色は アプリのアクセントカラーに基づきます 色を制御するには 新しい.textHighlightColorScheme属性で システムに用意されている ピンクなどの色を指定します macOS SequoiaのSF Symbols 6には 広範な題材を網羅した 新しいシンボルが800以上そろっています
エフェクトも増えて 小刻みな揺れ 回転 呼吸が追加されました
シンボルのエフェクトを再生する 新しいオプションも追加され 特定の回数 エフェクトを繰り返したり アニメーションを連続ループで 再生したりできます
私のお気に入りは シンボルのバッジやスラッシュを 魔法のように置き換えられることです 新しいシンボルエフェクトは 素晴らしいですね これらの詳細や カスタムシンボルの オーサリング方法については 「What’s new in SF Symbols 6」を ご覧ください 「Animate symbols in your app」では 表現力豊かなアニメーションを使う方法を 詳しく紹介しています 次はSaveパネルの機能強化についてです
ドキュメントを保存する際に 保存したいファイルフォーマットを Saveパネルで選択できたら便利ですよね macOS Sequoiaには ファイルフォーマットの ピッカーが標準装備され 独自のアクセサリビューを 作成する必要がありません 標準のファイルフォーマットピッカーは とても使いやすく SaveパネルのshowsContentTypesプロパティを trueに設定するだけです ピッカーにはSaveパネルで選択可能な コンテンツタイプの オプションが表示されます これはallowedContentTypes プロパティで指定します デフォルトではローカライズされた コンテンツタイプの説明が表示されますが メニューの項目名を 独自の表示名にするには panel(_ displayNameFor type:) デリゲート関数を実装します この関数は アプリのコンテキストに応じて 適切なコンテンツタイプの 名前を返します
Saveパネルの機能強化で 開発時間が短縮されれば幸いです 次は macOS SDKに 初登場のAPIを紹介します 長年にわたって マウスの後をずっと追いかけてきた そう カーソルです システムカーソルがmacOS Sequoia SDKで 利用できるようになりました まず frameResizeカーソルを見てみましょう
要素の縁や隅に置いて サイズを変更する際に使います パラメータは2つ必要です 1つ目はpositionです 縁や隅で カーソルが反応するようにします 2つ目はdirectionsは サイズ変更の方向です 要素は最小サイズにも 最大サイズにもできます frameResizeカーソルで サイズを変更できるのは1つの要素です 2つの要素間の セパレータを移動するには columnResizeカーソルと rowResizeカーソルを使います カーソルアートワークの矢印に対して 垂直に描かれているバーが サイズを変更する セパレータを示しています この種類のカーソルは スプレッドシートで表の列幅や 行の高さを変更する際に便利です セパレータをこれ以上 移動できない場合は 移動可能な方向が示されます
拡大と縮小は 新しいzoomInカーソルと zoomOutカーソルで行います これらのカーソルを使うと アプリのコンテンツをクリックしたときに 拡大または縮小できます AppKitではご覧のシステムカーソルが 新たに使えるようになりました システムカーソルを使うことで アプリ全体で統一感のある 標準的な外観になります アプリ用のカスタムカーソルで うまくできない操作が システムカーソルでもできない場合は カスタムカーソルが本当に必要か よく考えてみましょう 標準のカーソルを使った方が簡単で 自分で描く必要もありません システム設定の セクションで アクセシビリティ向けに 大きなサイズに変更できます また ポインタの色を調整して ユニークな色にすれば カーソルの位置が見つけやすくなります 次に NSToolbarの機能強化は 表示モード 表示項目 項目の表示/非表示の3点です NSToolbarでは項目を表示する際に テキストラベルの有無を選べます アプリの推奨スタイルとして アイコンのみを採用していても ユーザーによっては テキストラベルがあった方が 項目を見つけやすい場合があるため このような選択肢を提供するとよいでしょう macOS Sequoiaではツールバーの内容を ほかの方法でカスタマイズできない場合でも スタイルの選択肢を 提供することができるようになりました allowsDisplayModeCustomization プロパティを使用します これはデフォルトで有効になっています ツールバーに識別子が 割り当てられていて AppKitにスタイル設定を 保存できるようになっているか すべての項目に適切なラベルが付いているか よく確認しましょう NSToolbarの従来の項目プロパティよりも さらに便利なプロパティとして 新しいitemIdentifiersプロパティが 用意されています ツールバーのitemIdentifiersを設定すると 最小限の追加や削除が 自動的に実行されます allowsUserCustomizationが有効の場合 この部分で変更した値によって すべてのカスタム設定が 上書きされるので注意してください これはカスタマイズできない ダイナミックツールバーでのみ使用してください 現在の選択に無関係な項目は 無効にしておき ツールバーからプログラムで 項目を削除するのは ドキュメントの編集モードと 表示モードの変更など ウインドウのモードが 変更された場合に限ります
新しいisHiddenプロパティでは 条件に基づいてツールバーの項目を 非表示または表示することもできます 非表示になっている項目も 引き続きカスタマイズに表示されるので このような項目を表示する場所や 条件を選択できます これを使うのは項目が無関係な場合で ウインドウに表示されている 選択インジケータは問いません
例えばダウンロードを示す項目は 最初のダウンロードが始まるまで アプリで非表示にしておくことができます
次に紹介するのは まったく新しい APIであるテキスト入力候補です アプリで入力し始めると システム標準の候補メニューに 独自の入力候補を表示できます このパターンは多くのアプリで よく目にしますが macOS Sequoiaでは AppKitの標準になっています NSSearchFieldのようなサブクラスを含め どのNSTextFieldでも使えます まず テキストフィールドで suggestionsDelegateプロパティを設定します テキストの入力が始まると 変換候補を表示するよう デリゲートが依頼を受け 同期または非同期で 結果を返します 候補をハイライトまたは選択したときに すべてのテキストが入力されるよう カスタマイズすることもできます テキスト入力候補を使う際の デザイン上のヒントを紹介します ユーザーによるテキストの入力に合わせて 適切な変換候補がタイミングよく 表示されるようにしましょう ユーザーは入力速度に合わせてスムーズに インターフェイスが反応することを期待します 一貫性があり予測可能な変換候補が 表示されるようにしましょう そうすることでマッスルメモリが鍛えられ 表示される結果を信頼してもらえます 変換候補を非同期で表示する場合は 即時的な候補を先に表示しましょう シンプルさを保ち 最も重要な情報と詳細以外は 表示しないようにします そうすることで 正しい結果が素早く見つかります これらはmacOS Sequoiaの AppKitの機能強化のほんの一部です ほかにもたくさんあります 詳しくは developer.apple.comで リリースノートをご覧ください
新しいインテリジェンス機能を活用し アプリにAPIを導入して シームレスに統合してください
ウインドウのパネルをいくつも表示せずに タイル表示にしましょう アプリのウインドウをすっきりと 整理するための準備をしましょう 新しいメニューとアニメーションのAPIで SwiftUIを段階的に実装しましょう コンテンツタイプのピッカーやカーソル テキスト入力候補などの新しいシステム標準 コンポーネントを実装してください コンテキストメニューをキーボード対応にして アプリをスムーズに操作できるようにし ツールバーがすべての表示モードに 対応しているか確認してください
Mac向けの素晴らしいアプリを 開発してくださる皆さんに感謝します 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) } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。