cockscomblog?

cockscomb on hatena blog

XCFrameworkをSwift PackageとしてGitHubでリリースする

以前、XCFrameworkをバイナリターゲットとしてSwift Packageに埋め込んだライブラリを作った。

これを容易に利用するためには、ビルド済みのXCFrameworkをどこかにアップロードして、それを参照するSwift Packageを公開するとよい。XCFrameworkはGitHubリリースに成果物として置いておくことにする。

ということで、前回は概ねこれを手作業でやったのだけど、手作業したくないのでGitHub Actionsにした。

リリースが自動化されているソフトウェアは信頼性が高い。

GitHub Actionsでバイナリターゲットを含むSwift Packageをリリース

自動化にあたって手順を書き出してみると、次のようになる。ややこしい。

  1. XCFrameworkをビルドしてアーカイブする
  2. XCFrameworkのチェックサムを計算
  3. バージョン番号を決める
  4. Package.swiftを書き換える
  5. Package.swiftをコミット
  6. タグを打つ
  7. GitHubでリリースを作成

実際のActionはこうなる。

順を追ってやっていく。

XCFrameworkをビルドしてアーカイブする

これは普通にやったらいい。今回はgomobileを使っているのでちょっと変な感じではある。Makefileに書いてある。

.PHONY: install-tools
install-tools:
    go install golang.org/x/mobile/cmd/gomobile@latest
    gomobile init

.PHONY: xcframework zip
xcframework: install-tools
    gomobile bind -target=ios,iossimulator,macos,maccatalyst -iosversion 14 -prefix GOJQ -o Frameworks/GOJQBinding.xcframework github.com/cockscomb/swift-gojq/binding

zip: xcframework
    zip -X -r Frameworks/GOJQBinding.xcframework.zip Frameworks/GOJQBinding.xcframework/

XCFrameworkのチェックサムを計算

Swift Packageのバイナリターゲットにはチェックサムが必要なので計算する。swift package compute-checksum するだけ。

- name: Compute checksum
  id: checksum
  run: |
    echo "checksum=$(swift package compute-checksum Frameworks/GOJQBinding.xcframework.zip)" >> $GITHUB_OUTPUT

バージョン番号を決める

mathieudutour/github-tag-action を使った。major とか minor とか patch とかで一つ前のバージョン番号から決めることにして、workflow_dispatchinput で選べる感じにしている。

on:
  workflow_dispatch:
    inputs:
      bump:
        description: 'Bump version'
        required: true
        default: 'patch'
        type: choice
        options:
        - major
        - minor
        - patch
- name: Calculate next version
  id: next_version
  uses: mathieudutour/github-tag-action@v6.1
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    default_bump: ${{ github.event.inputs.bump }}
    tag_prefix: ''
    dry_run: true

バージョンを先に決めておかないとリリースのURLが決まらないので、先にバージョンを決める。dry_run があって便利。

Package.swiftを書き換える

ここがいちばんトリッキー。リリースのURLとチェックサムPackage.swift に書き込まないといけない。Package.swift のテンプレートを用意してレンダリングするか、すでにある Package.swift を書き換えるかだが、テンプレートを管理するのが面倒なので後者を選ぶ。

丁寧にSwift Package Pluginを作ってある。Swift Syntaxで Package.swift を書き換える。

Swift SyntaxによるPackage.swiftの書き換え

Swift Syntaxでは、SyntaxRewriter を継承して適当な visit(_:) メソッドを実装すると、ASTを辿って必要な箇所を書き換えられる。Sources/swift-package-checksum-rewriter/BinaryTargetSourceRewriter.swift として実装している。書き換え前後で余計な差分が出にくいように Trivia というやつで改行や空白を丁寧に調整している。(それもあってSwiftSyntaxBuilderがあまり思うように使えていない。)

これを executableTarget にしておいて、Swift Package Pluginからは実行ファイルとして呼び出す。だからプラグインとしての実装は次の通りで、ただのラッパーになった。

import Foundation
import PackagePlugin

@main
struct RewriteChecksumCommandPlugin: CommandPlugin {
    func performCommand(context: PluginContext, arguments: [String]) async throws {
        let tool = try context.tool(named: "swift-package-checksum-rewriter")
        let process = try Process.run(URL(fileURLWithPath: tool.path.string), arguments: arguments)
        process.waitUntilExit()
    }
}

使い方

これを使うには Package.swift に依存を追加する。

     dependencies: [
        .package(url: "https://github.com/cockscomb/swift-package-checksum-rewriter", from: "0.1.0"),
    ],

あとは swift package コマンドから呼び出すだけ。--allow-writing-to-package-directory するのがコツ。

- name: Rewrite Package.swift
  run: |
    swift package \
      --allow-writing-to-package-directory \
      rewrite-package-binary-target \
      --url=https://github.com/cockscomb/swift-gojq/releases/download/${{ steps.next_version.outputs.new_tag }}/GOJQBinding.xcframework.zip \
      --checksum=${{ steps.checksum.outputs.checksum }} \
      Package.swift \
      GOJQBinding

ここが一番面倒だった。

Package.swiftをコミット

ここはどうやってもいい。今回は stefanzweifel/git-auto-commit-action を使った。簡単。

- uses: stefanzweifel/git-auto-commit-action@v4
  with:
    commit_message: Bump version to ${{ steps.next_version.outputs.new_tag }}

タグを打つ

さっきは dry_run したけど今回が本番。

- name: Bump version
  id: bump_version
  uses: mathieudutour/github-tag-action@v6.1
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    default_bump: ${{ github.event.inputs.bump }}
    tag_prefix: ''

GitHubでリリースを作成

actions/upload-release-assetアーカイブされているので、案内に従って softprops/action-gh-release を使う。これも簡単。

- name: Release
  uses: softprops/action-gh-release@v1
  with:
    tag_name: ${{ steps.next_version.outputs.new_tag }}
    body: ${{ steps.next_version.outputs.changelog }}
    files: Frameworks/GOJQBinding.xcframework.zip

完成

結構ややこしい感じだけど、これでバイナリターゲットを含むSwift Packageを自動的にリリースできるようになった。

ちょっとやったらできるだろうと思って作業し始めたけど、Package.swiftの書き換えがややこしくて思ったより大変だった。

React Server ComponentsとGraphQLは競合するか

Next.jsのapp directoryについて話していて、GraphQLを使う場面ではServer Componentsの魅力がいくらか落ちるよな、と思った。裏を返せば、Server Componentsが活用されるような時代ではGraphQLの重要度が下がるかもしれない。

現にServer ComponentsのRFCの「Credits and Prior Artを見ると次のように書いてある。

  • Relay’s data-driven dependencies, which allow the server to dynamically choose which Client Component to use.
  • GraphQL, for demonstrating one approach to avoiding multiple round trips when loading data for client-side apps.

GraphQLやRelayは「Prior Art」だ。

GraphQLが強力な理由に、データのグラフがそのまま扱えることや、フラグメントによってコンポーネントが必要とするデータを宣言的に表現できることがある。これらによって解決された課題は、Server Componentsでも解決できる。Server Componentsはサーバーで構築されるから、Web APIを複数回にわたって呼び出す際のRTTが十分に短くなるし、それぞれのコンポーネントで必要なデータを個別に取得してもそれなりにいい感じになる。

ということで、Server Componentsを前提とすると、GraphQLによって得られていた利益(の一部)が他の手段でも叶えられることになる。またはGraphQLを前提とすると、Server Componentsによって得られるはずの利益が多少は目減りする。

もちろんServer Componentsによって達成されることは他にもあるし、GraphQLにも他にメリットがある。

GraphQLは引き続き有力

GraphQLの重要なメリットに、オーバーフェッチングを防ぐことがある。またエコシステムの発展のおかげで、ツーリングも優れている。ネイティブアプリからも利用でき、ひとつのAPIで多様なクライアントをサポートできる。GraphQLはWeb APIを構築する際に、引き続き有力な選択肢である。

またGraphQLの側も@defer@streamディレクティブによって、Next.jsのStreamingと同じ課題を解決しようとしている。ふたつの技術がある意味では競合するという例でもある。

しかし、RelayやApollo Clientの強力なキャッシュ機構を今のServer Componentsと組み合わせてうまく動かすのはややこしいだろう。と言いつつ、先のページのFAQに「Does this replace Apollo, Relay, or other GraphQL clients for React?というのがあり、そこでは「No」という回答に加えて、次のようにある。

For example, internally we use Relay and GraphQL in conjunction with Server Components.

つまり詳細はわからないがFacebookでは何らかうまくやっているようである。ただし直感的には、urqlとか、ややライトウェイトなクライアントが取り回しやすい、みたいなことが起きるんじゃないかという気はする。もちろんRelayやApollo Clientの側もさらに進歩して、適応していくだろう。

Server Componentsの時代

ということでServer Componentsの時代では、これまでのクライアントサイドレンダリング(+1ページ目はサーバーサイドレンダリング)のような、クライアント側でやっていくスタイルから変化し、GraphQLの意味付けも変わっていく予感がある。より単純なクライアントが好まれるようになったり、そもそもGraphQLではなくRESTあるいはRPCスタイルのWeb APIが流行るかもしれない。

1Password CLIで.env.localを作る

1PasswordにはVS Code拡張があって、.env ファイルなどにハードコードされたシークレットを1Passwordに保存して、secret reference というURL形式に置き換えてくれる。

ちなみにコミュニティ製のJetBrains IDE用の拡張もある。

これを使って .env.local.template を作る。

TWITTER_CONSUMER_KEY='op://Private/Twitter/API Key/consumer_key'
TWITTER_CONSUMER_SECRET='op://Private/Twitter/API Key/consumer_secret'

op:// のところがsecret reference。

.gitignore ファイルでは .env.local は無視しているけど、.env.local.templateリポジトリに入れてしまう。

# .env
.env.local
.env.*.local

あとは、1PasswordのVS Code拡張で「Preview with secrets」すると、secret referenceが解決されたプレビューが得られるので、これを .env.local として保存したら良い。けどちょっと面倒なので、1Password CLIでやってしまう方が便利。

$ op inject --in-file .env.local.template --out-file .env.local

1Passwordをチームで使っていると便利ですね。

ショートカット.appでjq

jq

jqJSONをいい感じにクエリできるやつで、広く使われている。

$ echo '{"foo": "bar"}' | jq '.foo'
"bar"

例えばGitHub Actionsのランナーにもデフォルトで入っている。

あるいはGitHubCLIツール gh にも --jq オプションがあって、統合されている。

つまりソフトウェアエンジニアにとっては、jqはJSONを触るときのデファクトスタンダードツールと言える。

ショートカット.app

iOS/iPadOS/macOSには、ショートカット.appがある。特に説明は不要と思う。

ショートカットでなんらかのJSON APIを利用したい場合、「URLの内容を取得」アクションでデータを取得して、「辞書の値を取得」(あるいは「リストから項目を取得」)アクションで情報を取り出す。

これでいいといえばいい。が、やはりjqを使いたい。

Swiftからgojqを使う

id:itchyny さんが作っている、Goによるjqの実装、gojqというのがある。

これを使う。

Goにはgomobileというのがあって、Goでモバイルアプリを作るやつという印象だが、実際にはGoでライブラリを作ることもできる。

ということで、gojqを呼び出すためのグルーコードを書いていく。

package binding

import (
    "github.com/itchyny/gojq"
    _ "golang.org/x/mobile/bind"
)

type Query struct {
    query *gojq.Query
}

func NewQuery(src string) (*Query, error) {
    query, err := gojq.Parse(src)
    if err != nil {
        return nil, err
    }
    return &Query{query: query}, nil
}

func (q *Query) Run(input []byte) (*Iterator, error) {
    ...
}

そしてうまいことオプションをつけて gomobile コマンドを実行する。

$ gomobile bind \
    -target=ios,iossimulator,macos,maccatalyst \
    -iosversion 14 \
    -prefix GOJQ \
    -o Frameworks/GOJQBinding.xcframework \
    github.com/cockscomb/swift-gojq/binding

-targetiOSmacOSを指定する。-prefix も設定できる。これで、XCFramework形式で出力できる。

これは次のようなインターフェースを持っていて、Objective-Cのヘッダが作られる(ここではSwiftから見た様子の一部を抜粋している)。

import Foundation

open class GOJQBindingIterator : NSObject, goSeqRefInterface {
    public init()

    open func next() throws -> Data
}

open class GOJQBindingQuery : NSObject, goSeqRefInterface {
    public init?(_ src: String?)

    open func run(_ input: Data?) throws -> GOJQBindingIterator
}

public func GOJQBindingNewQuery(_ src: String?, _ error: NSErrorPointer) -> GOJQBindingQuery?

Goの string がSwiftの String になったり、同じように []byteData になったり、基本的な変換はもちろん行われる。さらに errorNSError になるし、Goの構造体が NSObject を継承したクラスに変換されたりしている。よくできている。(Swiftでところどころ Optional になるのは仕方ない。)

ここまでやってくれると、もうちょっとSwiftから使いやすいようにラッパを書くのも簡単だ。

import GOJQBinding

enum QueryError: Error {
   case unknown
}

public struct Query {
    private let binding: GOJQBindingQuery

    public init(_ query: String) throws {
        var error: NSError?
        guard let binding = GOJQBindingNewQuery(query, &error) else {
            throw error ?? QueryError.unknown
        }
        self.binding = binding
    }

    public func run(_ input: Data) throws -> AsyncThrowingStream<Data, any Error> {
        ...
    }
}

あとはXCFrameworkを binaryTarget に加えたSwift Packageを作る。

// swift-tools-version: 5.7

import PackageDescription

let package = Package(
    name: "SwiftGoJq",
    platforms: [ .macOS(.v13), .macCatalyst(.v14), .iOS(.v14) ],
    products: [
        .library(name: “SwiftGoJq”, targets: ["SwiftGoJq"]),
    ],
    dependencies: [],
    targets: [
        .binaryTarget(
            name: "GOJQBinding",
            url: "https://github.com/cockscomb/swift-gojq/releases/download/0.1.0/GOJQBinding.xcframework.zip",
            checksum: "1c45710de17fb7020dcfc75105344729725c5e3875e7058e98790e5f4e178162"),
        .target(
            name: "SwiftGoJq",
            dependencies: [
                "GOJQBinding",
            ]),
    ]
)

GitHubに置いておいたのでどうぞご利用ください。

ショートカット

これでようやく本題の、ショートカットのアクションにしていく。

iOS 16/macOS VenturaからApp Intentsフレームワークというのが追加されている。これはアプリの機能をシステムに公開するための新しいしくみだ。これを使う。

次のように AppIntent プロトコルを実装したコードをアプリに含めておくと、システムが自動的に認識して、ショートカットから呼び出せるアクションにしてくれる。XxxManager に登録、みたいなことはいっさい不要だ。

import AppIntents

import AsyncAlgorithms
import SwiftGoJq

struct JQIntent: AppIntent {
    static var title: LocalizedStringResource = "jq"

    @Parameter(title: "JSON") var input: String

    @Parameter(title: "Query") var query: String

    static var parameterSummary: some ParameterSummary {
        Summary("\(\.$input) | jq '\(\.$query)'")
    }

    func perform() async throws -> some IntentResult {
        let jq = try Query(query)
        let results = try jq.run(input)
        let array = try await Array(results)
        return .result(value: array)
    }
}

@Parameter のついたプロパティが入力で、perform メソッドが実行され、結果を出力できる。async throws なので非同期的な処理も容易だ。

また parameterSummary によってショートカット.app中での表示をコントロールできる。ここでは、入力 | jq 'クエリ' という感じで、シェルっぽくしてみた。かわいいでしょう。

実際の表示は次のとおりで、入力がうまく表示されている。

いかがでしたか

iOS 16で追加されたApp Intentsフレームワークのおかげで、とても簡単にショートカット用のアクションを提供できた。App IntentsやiOS 16のことがもっと知りたくなってきたと思う。

iOS 16について学ぶのに何かいいリソースはないかな〜。

お〜っと、2022年12月24日発売の「WEB+DB PRESS Vol.132」に、ちょうどiOS 16の特集「iOS 16最前線」が載っているぞ!!

ということで、こちらの特集をはてなの同僚 id:yutailang0119 / id:kouki_dan と一緒に書きました。App Intentsについても紹介していますので、どうぞお買い求めください。もちろん他の記事もどれもおもしろいので、年末年始のお供にぴったりです。

こちらからは以上です。ハッピーホリデー!


以上、「potatotips #80 iOS/Android開発Tips共有会」での発表を再録しました。当日は主催者および参加者のみなさんのおかげで、とても楽しく過ごせました。またどのLTもたいへんおもしろく拝見しました。ありがとうございました。またよろしくお願いします。

macOSのコンテナ開発環境におけるVirtualization frameworkの採用

Docker Desktop for Mac

Docker Desktop for Macでは、仮想マシン上のLinuxでDockerを動かしている。仮想マシンにはhyperkitやQEMUが使われていた。が4.14.0からVirtualization frameworkがデフォルトで使われる。

Set Virtualization framework as the default hypervisor for macOS >= 12.5.

Virtualization frameworkmacOS内蔵の仕組みで、macOS 11で導入されてから、徐々に機能が拡張されている。Virtualization frameworkは高レベルなAPIで、より低レベルなAPIとしてmacOS 10.10から搭載されているHypervisor frameworkがあり、おそらくVirtualization frameworkもこれを利用している。(hyperkitやQEMUも利用している。)

Virtualization frameworkを使うと、Docker Desktopの設定でvirtiofsを有効にできる。現時点ではベータ扱いになっている。4.15.0でGAになった。おそらくVirtualizatoin frameworkのVZVirtioFileSystemDeviceConfigurationを利用しているのだろう。

virtiofsを有効にすると、ホストのmacOSとゲストのLinuxの間でのファイル共有が高速化される。ファイルの共有の遅さは、Docker Desktop for Macの積年の課題だった。

さて、Docker Desktopのロードマップを見ると、今後はApple Siliconでx86_64アーキテクチャのバイナリを実行する際にRosetta 2を利用することが検討されている検討されていたが、4.16.0でベータリリースされた

macOS 13では、Virtualization framework上のLinuxでもRosetta 2が利用できる機能が追加されており、これを利用するつもりだろう。

Rosetta 2はAOT変換(とJIT変換)によって、Apple Silicon上でx86_64のバイナリをかなり高速に実行させられることが知られている。現在、Apple Siliconでx86_64のDocker Imageを実行する際にはエミュレーションが行われており、ネイティブよりかなり遅い。Rosetta 2が利用できれば、一定の高速化が期待できる。

ということで、Docker Desktop for MacはVirtualizaton frameworkへの移行によって、高速化を達成しつつある。

Lima

ところで、Docker Desktop以外のソリューションはどうしているのかというと、Rancher DesktopにせよFinchにせよ、macOSLinuxを動かすというところではLimaを使っている。

LimaはQEMUを使っているが、最近ちょうどVirtualization frameworkのサポートが追加されつつあり、同時にvirtiofsも利用可能になる。

それだけでなく、Apple SiliconではRosetta 2によるx86_64バイナリの実行がサポートされようとしており、最新のベータにもこれが含まれている。

ということで、Lima 0.14に大注目だ。

まとめ

macOSでのコンテナ開発環境は、macOS上でどうやってLinuxを動かすかというところから始まっていて、最近ではmacOSに搭載されたVirtualization frameworkの採用が広がっている。そしてvirtiofsとRosetta 2によって、開発体験が改善されつつある。Docker DesktopもLimaを採用する他のソリューションも同様に進歩していて、目が離せない。

筆者は現在、勤め先がDockerの有料プランを契約しているため、それを使っています。

ソフトウェアエンジニアとしての最初の10年

働き始めてから丸10年経った。

2012年、僕は北海道に住む大学院生で、趣味としてプログラミングを楽しんでいた。Appleのファンだから、macOSiOSのアプリケーションを開発して、ちょっとでもAppleに近づいたような気持ちになっていた。その夏1ヶ月のインターンシップに参加した。インターンシップで、それまで趣味だったプログラミングが突然違った価値を持ち始めて、これを仕事にしないといけないと思うようになった。それで、両親や先生に謝って、大学院を退学して、インターン先の会社に正社員として入社した。それが2012年11月のことで、それから10年間、株式会社はてなで働いている。

この業界では、10年同じ会社で働いているというと、ちょっと珍しい部類なのかなと思う。とはいえ社内ではそれほど珍しくもなくて、あまり気にならない。いろいろなプロダクトを夢中になって開発していたら、いつの間にか10年経っていた。

この10年の間に結婚もしたし、ふたりの子供にも恵まれて、生活環境の方は大きく変化した。上の子は5歳だから、半分以上の期間は親として暮らしていると思うと、少し不思議な感じもする。

ソフトウェアエンジニアとしては、もともとスマートフォンアプリのエンジニアという感じだったが、仕事をし始めてからはサーバーサイドの開発もそれなりにやった。やってはいたが、振り返ってみれば、最初の5年くらいは何も分かっていなかった。何も分かっていないなりに必死にやっていたら、2017年に転機が訪れた。

2017年、サーバーサイドから何から一通りを新規開発する案件のテックリードを任された。プロダクトの性質を考えるとSingle-Page Applicationがよさそうだったので、いろいろ調べながらReactを導入していった。コンテナで運用しようということになって、一夜漬けでDockerを学んで、翌日開発途中のアプリケーションをコンテナ化した。こういったことの過程で、突然、いままでやっていたことの点と点がつながって、いろいろなことがはっきりと理解できるようになった。

それから、ソフトウェアのアーキテクチャについて急にくっきりしてきて、おもしろおかしくソフトウェア開発できている。たぶんそんなに珍しい話ではなくて、経験を分解して再構築できた、というだけのことなんだとは思う。

2018年頃にはGraphQLの導入をして、それからしばらくはGraphQLにハマっていた。いろいろなところで導入して、ブログに書いたりしていたら、WEB+DB PRESSの特集に繋がった。

我が身を省みると、あまり勉強熱心な方ではなく、スーパーハッカーへの夢は遠い。それでも10年かけて、興味のあることだけでも地道に取り組んでいたら、いくらかは形になった。尊敬する上司や同僚たちに導かれつつではあるが、元はただのAppleファンボーイだったことを思えば、悪くないと思う。

ということで、次の10年が始まりました。引き続きよろしくお願いします。

ステージマネージャの使い方

macOS VenturaおよびiPadOS 16で導入されたステージマネージャだけど、どうやって使うのか段々わかってきた。

ステージマネージャのコンセプト

ステージマネージャは、ウインドウのセットを作る機能だ。タスクに合わせてウインドウのセットを作れば、複数のタスクを行ったり来たりする場合に、ステージマネージャの切り替えをするだけでよくなる。

例えば、Webサービスを作っているとき、筆者の場合、Visual Studio CodeとTerminalとGoogle Chromeを使う。これをひとつのセットにまとめておく。iOSアプリを作っているときは、XcodeとSimulatorをセットにする。同僚とのコミュニケーションや通知を受け取るのに、Slackとメールもセットにしておく。作業内容に応じてこれらを切り替えながら仕事を進めていくイメージだ。

ステージマネージャの使い方

Appleのドキュメントを読むとだいたい書いてある。

有効・無効はコントロールセンターから切り替えられる。

セットの作り方

画面端に最近使ったアプリケーションのサムネイルが表示されている。Slackとメールをひとまとめにしたい場合、Slackを前面に表示した状態で、メールをサムネイルからドラッグして持ってくる。あるいは、Shiftキーを押下しながらアプリケーションをクリックすると、現在のセットに追加できる。こちらの方が手早くていい。

反対に、現在のセットからアプリケーションを取り除きたいときは、ウインドウをドラッグしてサムネイルの並びに持っていく。もしくは、Shiftキーを押下しながらウインドウのタイトルバー部分をクリックする(macOSのみ)。または、ウインドウの「+」ボタンをホバーすると表示される「ウインドウをセットから削除」を選ぶ(macOSのみ)。

macOSではShiftキーが便利なので、これだけ覚えておけばよさそうだ。iPadOSでは、ウインドウ上部の「…」メニューから一通りの操作を行える。

マルチディスプレイ

マルチディスプレイでは、ディスプレイ単位でステージマネージャが機能する。つまり、複数のセットを同時に利用できる感じになる。ので、ディスプレイ単位でセットを作る。(iPadOS 16.1時点ではまだマルチディスプレイは利用できない。)

macOSでは、Mission Controlのスペース(仮想デスクトップのような機能)でも、スペースごとにステージマネージャが機能する。

ステージマネージャの設定

macOSでは、「システム設定」の「デスクトップとDock」からステージマネージャをカスタマイズできる。

「最近使ったアプリケーション」は、画面端のサムネイルをデフォルトで表示するかどうか。表示していても、サムネイル部分をウインドウが覆うと表示されなくなる。いずれにしても、マウスカーソルを画面端に持っていけばサムネイルが出てくる。

「デスクトップ項目」は、デスクトップ上のファイルやフォルダを常に表示するか、デスクトップをクリックしたときだけ表示するか、という設定。

「アプリケーションウインドウの表示方法」は、1つのアプリケーションが複数のウインドウを開いているときにどうするかという設定。「ウインドウを一度にすべて表示」では、2つ以上ウインドウを開いてもデフォルトで1つのセットに表示される。「ウインドウを1つずつ表示」にすると、2つ目以降のウインドウがデフォルトで異なるセットになる。いずれにせよ、自分でウインドウごとにセットに入れたり外したりできる。

macOSでは、ダイアログ的に小さいウインドウを表示するアプリケーションがあるので、「ウインドウを一度にすべて表示」の方が使いやすいかもしれない。というのも、macOSではウインドウがステージマネージャ上でどう扱われるかはNSWindow.CollectionBehaviorによって決まるらしい。これが適切に設定されていないアプリケーションでは、変な挙動になる。

いかがでしたか

まだ変な挙動もあるものの、まあまあ便利と思います。