Macのメニューバーに常駐するノートアプリ「Mizuame」を個人で開発・公開しています。
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
を定義します。さらに、各タブに定義したEnumをtag(_ 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: )
でサイズを指定すると、理由は分からなかったのですが、期待通りに動作しませんでした。最終的にZStack
やVStack
などで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
を変数w
とh
に付けます。
最終的には、以下のようになりました。
動作環境にも書いたのですが、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) } } } }
実際に実装したソースコードはこちらです。