⚙️

SwiftUIのSettingsについてのメモ

2021/12/27に公開

macOS 11+からSwiftUIで環境設定画面を作るためのSettingsクラスが使えるようになっています。
SettingsをAppのbodyに入れておくと自動で Preferences… メニューがFileメニューに追加されショートカットに Cmd-, が設定されます。メニューから選択すると最小化と最大化ボタンが無効化されたウィンドウが表示されます。

import SwiftUI

@main
struct PreferencesExampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        Settings {
            SettingsView()
        }
    }
}

macOS 12.1現在ではAppKitを使わずにはウィンドウの最小化と最大化ボタンの無効化はできないのでSettingsが使えないかいろいろ調べたことをメモとして残しておきます。

動作確認はmacOS 12.1 + Xcode 13.2.1 (初回投稿時) および macOS 13.1 + Xcode 14.2 (2023-01-07更新) で行いました 。

メニューアイテム以外からSettingsを表示したい

たとえばメニューバーにだけ表示されるアプリケーションのように自身のウィンドウを通常もたないアプリからSettingsを表示したいときのように Preferences… メニュー以外からSettingsを表示したいと思いました。これは NSApp.sendActionshowPreferencesWindow: を送ることで表示できるようです。
追記: macOS 13からはアクション名が showSettingsWindow: に変わりました。

if #available(macOS 13, *) {
    NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
} else {
    NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
}
NSApp.activate(ignoringOtherApps: true) // 最前面に表示する

これは https://stackoverflow.com/a/65356627/6945346 で知りました。

SettingsにTabViewを使うときウィンドウタイトルを "(アプリ名) - (設定)" にしたい

環境設定ウィンドウのデフォルトのタイトルはアプリ名 (macOS 12まで) または TabViewの名称 (macOS 13) です。

SettingsではAppleのSettingsのドキュメントにもサンプルでありますがTabViewを使うことでツールバー形式のタブビューで表示してくれます。ですが、macOS 12では環境設定ウィンドウのタイトルはどのタブでもアプリ名のままです。

いろいろ試してみたところTabViewの各要素にnavigationTitle, navigationSubtitleのmodifierで設定することでSettingsのウィンドウタイトルをカスタマイズすることができるようです。

struct SettingsView: View {
    var body: some View {
        TabView {
            Text("General")
                .tabItem {
                    Label("General", systemImage: "gear")
                }
                .navigationTitle("ParentTitle")
                .navigationSubtitle("General")
            Text("Advanced")
                .tabItem {
                    Label("Advanced", systemImage: "star")
                }
                .navigationTitle("ParentTitle")
                .navigationSubtitle("Advanced")
        }
        .padding(20)
        .frame(width: 375, height: 150)
    }
}

(追記) macOS 13ではnavigationTitleのデフォルトはtabItemの名称になったようです。

TabViewのaccentColorを設定する

TabViewのaccentColorを設定してもタブアイコンのハイライト色がなぜか青のまま変わらないのですが、 Assets.xcassets でアプリ全体のaccentColorが設定されていると変わるようでした。


(未解決) タブごとのビューのサイズに合わせてウィンドウをアニメーションしながらリサイズする

タブを切り替えたときに内容に合わせたサイズにウィンドウをリサイズしてほしいのですが、TabViewの各項目のframeにサイズを設定していた場合、タブの切り替え時に一瞬でウィンドウのサイズが変わってしまいます。OSの環境設定などであればアニメーションしながらリサイズされるのでこの挙動には違和感があります。

NSWindowにアクセスすればアニメーションしながらのウィンドウのリサイズもできるようですが、SettingsのNSWindowを直接いじるのはめんどうですね。将来modifiersでいじれるとうれしいです。

https://stackoverflow.com/questions/59385446/change-window-size-based-on-navigationview-in-a-swiftui-macos-app

macOS 13からOSの環境設定がSwiftUI化され、サイドバー形式になりました。この見た目はNavigationSplitView に似ています。

ですがSettingsのbodyにTabViewではなくNavigationSplitViewを使ってみると、タイトルバー下に不自然な余白ができてしまいます。

NavigationSplitView版のSettings

struct SettingsNavigationSplitView: View {
    @State private var section: Section?

    enum Section: CaseIterable, Identifiable {
        var id: Self { return self }
        case general, advanced
    }

    var body: some View {
        NavigationSplitView {
            List(Section.allCases, selection: $section) { section in
                if case .general = section {
                    Text("General")
                } else {
                    Text("Advanced")
                }
            }
        } detail: {
            if case .general = section {
                Text("General")
            } else {
                Text("Advanced")
            }
        }
    }
}

(ちなみにタイトルバーの下のボタンはサイドバーの表示切り替えを行うもの)

Discussion