【macOS】SwiftUIのSettingsで、TabViewを切り替えたときにタブごとにウィンドウサイズを変える

Macのメニューバーに常駐するノートアプリ「Mizuame」を個人で開発・公開しています。

Mizuame

Mizuame

  • Akira Nakamura
  • ユーティリティ
  • 無料
apps.apple.com

このアプリの設定ウィンドウ(よくcommand + , で出るアレ)はTabViewで実装しているのですが、各タブの内容に合わせて設定ウィンドウのサイズが変わらないようになっていました。そのため、設定内容が多いタブであれば問題ないのですが、設定内容が少ないタブだと余白が大きくなってしまいます。
すでに世にある他のアプリの設定ウィンドウは、各タブの内容に応じてウィンドウサイズがみょんみょん変わっているので、私も長いものに巻かれるべく、今回対応しました。

動作環境

どうやった?

最初は以下の状態だったとします。
TabViewには、一般タブ(TabGeneral)とノートタブ(TabNote)が実装してあるとします。

@main
struct SampleDayo: App {
    var body: some Scene {
        // 設定ウィンドウはSettingsView()で実装
        Settings {
            SettingsView()
        }
    }
}


struct SettingsView: View {
    var body: some View {
        TabView() {
             TabGeneral()
                 .tabItem {
                     Label("一般", systemImage: "gearshape")
                 }
            TabNote()
                .tabItem {
                    Label("ノート", systemImage: "macwindow")
                }
        }
    }
}

まず、TabViewでどのタブが選択されたか判別できるようにします。
そのために、 TabView(selection: )に変更し、selection:に渡すためのEnumを定義します。さらに、各タブに定義したEnumtag(_ tag:)で割り当てます。

struct SettingsView: View {

    @State private var selectedTab: TabType = .general

    var body: some View {
        TabView(selection: $selectedTab) {
             TabGeneral()
                 .tag(TabType.general)
                 .tabItem {
                     Label("一般", systemImage: "gearshape")
                  }
             TabNote()
                 .tag(TabType.note)
                 .tabItem {
                     Label("ノート", systemImage: "macwindow")
                 }
        }
    }
}


extension SettingsView {
    enum TabType {
        case general
        case note
        
        // 各タブのサイズを定義
        var frameSize: (width: CGFloat, height: CGFloat) {
            switch self {
            case .general:
                return (400, 350)
            case .note:
                return (400, 500)
            }
        }
    }
}

次に、ウィンドウのサイズを指定するためにframe(width: , height: )を実装します。
私は最初、以下のように実装しました。各タブのサイズを指定するのだからタブのViewに付ければ良いと考えてしまったわけですが、ウィンドウサイズが全く期待通りに変わらず、しばらく困ってしまいました。

TabView(selection: $selectedTab) {
    TabGeneral()
         .frame(400, 350) //これ
         .tag(TabType.general)
         .tabItem {
             Label("一般", systemImage: "gearshape")
         }
}

これは.border(Color.red)などを使ってTabGeneral()の外枠に色を付けてみると分かりやすいですのですが、あくまでもTabGeneral()のサイズを指定したのであって、決してSettingsView()のサイズを指定したわけではないという当たり前のことでした。
また、TabView()frame(width: , height: )でサイズを指定すると、理由は分からなかったのですが、期待通りに動作しませんでした。最終的にZStackVStackなどでTabView()を包んで、外側のZStack等にframe(width: , height: )でサイズ指定すると期待通りの動作をしました。

struct SettingsView: View {

    private var w: CGFloat = 400
    private var h: CGFloat = 350

    @State private var selectedTab: TabType = .general

    var body: some View {
        ZStack {
            TabView(selection: $selectedTab) {
                 TabGeneral()
                     .tag(TabType.general)
                     .tabItem {
                         Label("一般", systemImage: "gearshape")
                     }
                  TabNote()
                      .tag(TabType.note)
                      .tabItem {
                          Label("ノート", systemImage: "macwindow")
                      }
            }
        }
        .frame(width: w, height: h) //ここ
    }
}

最後に、タブが切り替わったことをonChange(of: )で受け取り、最初に定義した各タブのサイズを割り当てます。そして、サイズの割り当てと同時にUIを更新できるように@Stateを変数whに付けます。
最終的には、以下のようになりました。

動作環境にも書いたのですが、macOS 14.4で動作しています。しかし、.onChange(of: )macOS 13.x以下と14.x以上で仕様が変わっています。なので、macOS13.x以下でも動作させるためには、if #available(macOS 14, *) {} else {}で場合分けして、OSバージョンに合った.onChange(of: )を実装する必要があります。

また、他のアプリの動作と比べると分かりやすいのですが、今回の対応では、ウィンドウサイズが変わるときの滑らかなアニメーションがありません。サッと一瞬でサイズが変わってしまいます。この点は課題として残りましたので、どこかの機会で対応したいと思います。

@main
struct SampleDayo: App {
    var body: some Scene {
        // 設定ウィンドウはSettingsView()で実装
        Settings {
            SettingsView()
        }
    }
}


struct SettingsView: View {

    @State private var w: CGFloat = 400
    @State private var h: CGFloat = 350

    @State private var selectedTab: TabType = .general

    var body: some View {
        ZStack {
            TabView(selection: $selectedTab) {
                 TabGeneral()
                     .tag(TabType.general)
                     .tabItem {
                         Label("一般", systemImage: "gearshape")
                     }
                  TabNote()
                      .tag(TabType.note)
                      .tabItem {
                          Label("ノート", systemImage: "macwindow")
                      }
            }
            .onChange(of: selectedTab) {
                w = selectedTab.frameSize.width
                h = selectedTab.frameSize.height
            }
        }
        .frame(width: w, height: h)
    }
}


extension SettingsView {
    enum TabType {
        case general
        case note
        
        // 各タブのサイズを定義
        var frameSize: (width: CGFloat, height: CGFloat) {
            switch self {
            case .general:
                return (400, 350)
            case .note:
                return (400, 500)
            }
        }
    }
}

実際に実装したソースコードはこちらです。

github.com

Macのメニューバーに常駐するノートアプリMizuameのソースコードは、MITライセンスですべて公開しています。

github.com