【SwiftUI】Mac App タイトルバーを隠して全体に画像表示

SwiftUIによる自分用Macアプリ開発の備忘録(1)です。



# 画像をContentView全体に表示

まず画像をプロジェクトに取り込んで、ContentView全体に表示します。
「IVTask」というのは自分で決めたアプリの名前なので、作りたいアプリの名前で置き換えてもらえれば大丈夫です。

```ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        ZStack {
            Image("osman-rana-DHMlx0Osvg8-unsplash") // 取り込んだ画像ファイル名
                .resizable()
                .aspectRatio(contentMode: .fill) // Viewを埋めるように表示
            Rectangle()
                .foregroundColor(Color.black.opacity(0.5)) // 上から半透明黒色の長方形を被せて暗くしてみる（大筋とは無関係）
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```

```IVTaskApp.swift
import SwiftUI

@main
struct IVTaskApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .frame(minWidth: 1000, minHeight: 600) // 最小幅と最小高さを設定
        }
    }
}
```

![普通のView](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3117774/09a0e76a-ce32-c075-d783-63c1a05936dd.png)

使用している画像 : https://unsplash.com/photos/DHMlx0Osvg8 (Unsplashライセンス)



# タイトルバーを非表示

タイトルバーを非表示にするには、[アプリ名]App.swiftに以下のように書き加えます。

```diff_swift
        WindowGroup {
            ContentView()
                .frame(minWidth: 1000, minHeight: 600)
+       }.windowStyle(HiddenTitleBarWindowStyle()) // タイトルバーを非表示
```

![謎の領域](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3117774/86c41900-de74-70c3-38dc-eb4fdbba9aa8.png)

ウィンドウの端をドラッグして縦横比を変化させると、ウィンドウ上部に謎の領域が見え隠れします。ウィンドウサイズを固定するならこれでも十分目的は果たせていますが、画像を全体に表示したままフレキシブルに動かすために改良します。

![ウィンドウの構成について](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3117774/77fc404a-4b11-670c-2588-9e6657e0edb1.png)

どうやら、[WindowStyle][1]メソッドの設定によらず、常にContentViewは上の橙色の部分を指すようです。

画像はViewを埋めるように配置させているわけなので、画像がViewよりも縦長だとViewの上下に画像がはみ出ます（勝手にのりしろと呼ぶことにします）。下側ののりしろはウィンドウの領域の外なので見えなくなり、上側ののりしろも本来はタイトルバーで隠れます。しかし、タイトルバーを非表示にすると上側ののりしろが見えるようになります。

画像がViewよりも横長であればのりしろはなく、代わりにタイトルバーで隠れていた部分をデフォルトの色で埋め尽くしてウィンドウに仕立て上げているみたいです。だから謎の領域は見えたり見えなかったりするんですね。

それなら、赤色の部分を実際のViewとして画像を表示させることで解決できそうです。

![タイトルバー計測](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3117774/8fd87112-f8fa-c2b9-142a-cc027b1e6dc2.png)

タイトルバーの高さは原始的に測って28ptでした。とりあえずこの値を採用してみます。

```ContentView.swift
import SwiftUI

struct ContentView: View {
    let margin: CGFloat = 28 // タイトルバーの高さを定義

    var body: some View {
        GeometryReader() { geometry in // GeometryReaderを用いてViewの高さと幅を取得
            BackView()
                .frame(width: geometry.size.width, height: geometry.size.height + margin) // 高さを28ptだけ大きく
                .offset(x: 0, y: 0 - margin) // Viewのy座標（左上の点で定義される）を28pt上げる
        }
    }
}

struct BackView: View { // 旧ContentViewを内容をそのままにBackViewと名前を変えたもの
    var body: some View {
        ZStack {
            Image("osman-rana-DHMlx0Osvg8-unsplash")
                .resizable()
                .aspectRatio(contentMode: .fill)
            Rectangle()
                .foregroundColor(Color.black.opacity(0.5))
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```

![縦長ウィンドウ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3117774/7f8e51fb-b825-56ff-8697-4d6789e40dca.png)

GeometryReaderでContentViewの高さと幅を取得し、上に28ptだけ伸ばしたBackViewを埋めるように画像を表示させることで、縦に伸ばしても謎の領域が見えなくなりました。



# 全画面表示に対応

ここまでは、アプリがウィンドウとして表示されていることを想定していましたが、アプリを全画面表示で利用することも考えられます。

![背景サンプル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3117774/3d7ffbb4-3aec-bb00-0606-6951ca1cd89c.png)

青く縁取りをしたこんな背景を使ってみると、アプリがウィンドウとして表示されている時は「左右」あるいは「上下」に青い線が見えます。しかし、アプリを全画面で表示すると（ものすごく横長のディスプレイをお使いでない限りは）下に青い線が一本見えるだけになります。

プレビューやターミナルといったAppを全画面表示にするとタイトルバーが見えなくなることからも推測できますが、全画面表示ではContentView自体がスクリーンの大きさになっています。ゆえに、この場合は高さの+28ptが余計になってしまいます。

「全画面にしたときに横長の画像の上がちょっと欠けるくらいならそんなに気にしないでも...」
と言いつつやっぱり気にしてしまう自分がいるので、こんな風に書いてみます。

```diff_swift
    var body: some View {
        GeometryReader() { geometry in
+           if NSScreen.main?.frame.size.height == geometry.size.height { // 全画面の場合
+               BackView()
+                   .frame(width: geometry.size.width, height: geometry.size.height) // ContentViewの大きさそのまま
+                   .offset(x: 0, y: 0)
+           } else { // 全画面でない（ = ウィンドウとして表示されている）場合
                BackView()
                    .frame(width: geometry.size.width, height: geometry.size.height + margin)
                    .offset(x: 0, y: 0 - margin)
+           }
        }
    }
```

「題名にSwiftUIと書いてあるのにAppKitの[NSScreen][2]を使ってるじゃないか！何やってるんだ！」
すみません、これしか方法が見つからなかったもので...。

`NSScreen.main?.frame.size.height`[^1] はスクリーンの、`geometry.size.height` はここではアプリのContentViewの高さを表します。デスクトップでは上にメニューバー・下にDockがあり、アプリのウィンドウは通常それ以上高さを広げることができないので、スクリーンの高さとContentViewの高さが等しければ全画面表示になっていると見做せるはずです。

以上で、ウィンドウ表示と全画面表示のそれぞれの場合に対応して、アプリの画面を画像で埋めることができました。



# まとめ

- タイトルバーは自分で測ったところ高さ28ptで、ContentViewから上にはみ出たのりしろを隠す役割を果たしている。タイトルバーを隠してその分画像を表示させるViewを上に伸ばすことで、必ずウィンドウを埋めるように画像を表示させることができる

- 全画面表示ではContentViewの大きさがスクリーン全体になるので、この場合はViewを上に伸ばさない

不正確な表記などあればご指摘をお願いいたします。最後まで読んで頂きありがとうございました。

[^1]: `NSScreen.main?.visibleFrame` はDockを含まないなど多少特殊な性質を持つので、ここでは上手く動作しません。次が参考になります。https://cocoaapi.hatenablog.com/entry/00000120/recID33859

[1]: https://developer.apple.com/documentation/swiftui/scene/windowstyle(_:)

[2]: https://developer.apple.com/documentation/appkit/nsscreen