[SwiftUI] single window app on macOS

SwiftUI2021

     
⌛️ 3 min.

macOS 上で Single Window にしたい時の SwiftUI での実装を確認します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS14.4
  • Xcode 15.2
  • iOS 17.2
  • Swift 5.9

なお、この記事で説明している振る舞いは 使用している macOS のバージョンで大きく変わると思いますので、ご注意ください。

Note: Window/WindowGroup behavior looks highly depend on macOS/SwiftUI version.

in short

Code snippets for single window app on macOS.

SingleWindow (quit when last window is closed)

Single window app on macOS.

App will be terminated when last window is closed.

No “File” – “New” menu is there, so user can not open more than one window.

@main
struct SingleWindowApp: App {
    var body: some Scene {
        Window("main", id: "main") { // use Window instead of WindowGroup
            ContentView()
        }
    }
}

singleWindow with File-New menu

Single window app on macOS.

App is still running even last window is closed.
“File”-“New” menu will re-open the window. But it does not open more than one window.
When the window is existed (even hidden), “File”-“New” menu is disabled.


@main
struct SingleWindowApp: App {
    @Environment(\.openWindow) private var openWindow
    @State private var windowExist = false
    var body: some Scene {
        WindowGroup(for: String.self, content: { _ in
            ContentView()
                .onAppear { windowExist = true }
                // note: onDisappear can not distinguish hide from close
                //.onDisappear { windowExist = false }
                .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification), perform: { _ in
                    windowExist = false
                })
        }, defaultValue: { "main" })
        .commands(content: {
            // replace "File"-"New"
            CommandGroup(replacing: .newItem, addition: {
                Button(action: {
                    openWindow(value: "main")
                }, label: { Text("New Window") })
                .disabled(windowExist)
                .keyboardShortcut("n", modifiers: .command)
            })
        })
    }
}

background (in Japanese)

WindowGroup -> Window

WindowGroup の代わりに Window を使用すると、Single Window になります。

Window を閉じるとアプリは終了します。

Single Window アプリの振る舞い

Window に置き換えた動作が 期待通りのこともありますが、そうでない時もあります。

1つのウィンドウだけのアプリで、ウィンドウが閉じられたときには、2つの対応方法があります。

パターン1: 「ウィンドウが閉じられたときにはアプリを終了する」
これは、WindowGroup の代わりに、Window を使用した時の動作です。

パターン2:「ウィンドウが全て閉じられても アプリとしては動作を継続する」
 例えば、ウィンドウを閉じた後に、 “File” – “New” を選択されたときには、新しいWindowを表示し、可能であれば以前の状態に復帰することが期待されます。

パターン2については SwiftUI では、直接的なサポートはないようです。

ということで、WindowGroup の特性を調べて、実現性を確認してみました。

WindowGroup

WindowGroup の代わりに Window を使うと、”File” – “New” メニューがなくなります。

Window は、単一ウィンドウを扱うためのものなので、メニューがなくなることは合理的です。

ですが、一度閉じたウィンドウを開くべき メニューとしては、”File” – “New” を期待したいので、Window を使うのは難しいです。(アプリも終了してしまいますし・・・)

WindowGroup には、initializer がたくさんある

Google しても解決策は見つからなかったので、基本に戻って WindowGroup のドキュメントをみます。


参考
WindowGroupApple Developer Documentation

以下の設定により3つに分類できそうでした。
・ID指定の有無
・データ型指定の有無
・使用データ指定の有無
 ・デフォルトデータ あり/なし

Xcode により生成されるテンプレートコードは、ID 指定、データ型、使用データ のいずれも指定しない WindowGroup が使われています。

以下のようになっています。

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }

App の中には、WindowGroup や Window を複数持つことができます。

App は、基本的に、ID と 使用するデータ型で WindowGroup や Window のなかから 対応する Window を開きます。

1つの ID を使っていれば、1つの Window しか開かない気がしますが、残念ながら違います。

DocumentGroup を想像するとわかりやすいですが、1つの型に対して、(データが異なることを考慮すると) 複数の Window を開きたいことはありそうです。

具体的には、ID / データ型だけではなく、データ自体が同じであることが必要です。

ですので、この特徴を考慮すると アプリで ただ1つの Window を開くようにするためには、データ型ではなく 同じデータに対して Window を開くようにすることで、既存の Window があればそれが再利用されるようになります。

Window が既存かどうか

ここまでで openWindow を使って、シングルウィンドウを実現できますが、以下の振る舞いが気になるかもしれません。

・ウィンドウが開かれていても、”File” – “New” は、選択可能状態になっている。ただし、選択しても、何も起こらない

ウィンドウが開かれているときに、メニューを disable にするには、その状態を検知できなればいけませんが、調べた範囲では、SwiftUI では、Window がすでに開かれているかを確認する方法は用意されていない気がします。

ということで、@State を導入して Window がすでに開かれているかどうかを自分で管理することが必要でした。

開かれたことは、.onAppear で検知できます。
ですが、閉じられたことを確認するために一工夫が必要でした。

当初、onDisappear で検知していましが、「ウィンドウが閉じられた」「ウィンドウが Hide された」を区別することができませんでした。

ということで、NotificationCenter からの通知を subscribe して、状態を更新するようにしています。

まとめ

code snippet for Single window app on macOS

code snippet for Single window app on macOS
  • use Window for “simple” single window app on macOS
  • use id & value for WindowGroup for another type of single window app on macOS
  • in case “File”-“New” needs to be disabled, need to manage window existence by ourselves
  • onDisappear will be called not only for closing but also hiding

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

single window app (quit when window is closed)

@main
struct SingleWindowApp: App {
    var body: some Scene {
        Window("main", id: "main") { // use Window instead of WindowGroup
            ContentView()
        }
    }
}

single window app (continue running when window is closed, able to re-open with file menu)


@main
struct SingleWindowApp: App {
    @Environment(\.openWindow) private var openWindow
    @State private var windowExist = false
    var body: some Scene {
        WindowGroup(for: String.self, content: { _ in
            ContentView()
                .onAppear { windowExist = true }
                // note: onDisappear can not distinguish hide from close
                //.onDisappear { windowExist = false }
                .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification), perform: { _ in
                    windowExist = false
                })
        }, defaultValue: { "main" })
        .commands(content: {
            // replace "File"-"New"
            CommandGroup(replacing: .newItem, addition: {
                Button(action: {
                    openWindow(value: "main")
                }, label: { Text("New Window") })
                .disabled(windowExist)
                .keyboardShortcut("n", modifiers: .command)
            })
        })
    }
}

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です