曇りガラス風のビューをNSVisualEffectViewを使ってカスタムする

# 概要

- 曇りガラスといえばSwiftUIで[Material](https://developer.apple.com/documentation/swiftui/material)が追加されましたが、ウインドウが非アクティブのときにエフェクトが切れてしまうのが気に入りませんでした。
- そこで`NSVisualEffectView`を使い、ウインドウのアクティブ状態に関わらずエフェクトを維持するようにカスタムしてみます。

# 追記
- @Uhucream さんに、NSVisualEffectViewを使わずに実現する方法を教えていただきました！

>https://qiita.com/IKEH/items/2a1720a328188be052a2#comment-08fd833a98081568f776
>実は、NSVisualEffectView を使わずとも、 .environment(\.controlActiveState, .active) を .background(.ultraThinMaterial) の後に指定してやるだけで、やりたいことが実現できます！
>
>.environment で、要素のアクティブ状態を固定してやる、という発想です👀
>
>余談ですが、同じノリの話として、SwiftUI の Material は UIKit の UIBlurEffect.Style と比較して、**MaterialLight / **MaterialDark がないかと思いますが、その代わりに、.environment(\.colorScheme, .light) / .environment(\.colorScheme, .dark) を該当箇所に指定してやることで、**MaterialLight / **MaterialDark と同じ見た目を表現できるようになっています

```Swift
ZStack {
    Color.clear
        .background(.ultraThinMaterial)
        .environment(\.controlActiveState, .active)
//        .environment(\.colorScheme, .light)
        .environment(\.colorScheme, .dark)
    Text("ultraThinMaterial")
}
```

# 成果物
- アクティブ時

<img width="400" alt="image" src="https://i.imgur.com/mh0ADm4.jpg">

- 非アクティブ時

<img width="400" alt="image" src="https://i.imgur.com/pNhv5PZ.jpg">

# Gist
- https://gist.github.com/pommdau/99d1eeb6aec670b31b4bea6edf411870

# 実装
## コード
- まず下準備として、ウインドウの背景を透明にしておきます
    - [SwiftUI: macOSでウインドウが閉じたことを検出する](https://zenn.dev/kyome/articles/b578c29e3c65e6)

```swift
import SwiftUI

@main
struct MaterialDemoMatomeApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onReceive(NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification)) { notification in
                    if let window = notification.object as? NSWindow {
                        window.backgroundColor = NSColor(red: 0, green: 0, blue: 0, alpha: 0.001)
                    }
                }
        }
    }
}
```

- SwiftUIのViewとして使えるように`NSViewRepresentable`を使用します。
    - https://0xffa4.com/posts/4
    - [NSVisualEffectView\.Material](https://developer.apple.com/documentation/appkit/nsvisualeffectview/material)
    - [Swift: NSTextFieldをNSViewRepresentableでSwiftUI対応させる](https://zenn.dev/kyome/articles/a402e4a3e2676c)
- またウインドウのアクティブ状態に関わらずエフェクトを維持するには、[NSVisualEffectView\.State](https://developer.apple.com/documentation/appkit/nsvisualeffectview/state)を、デフォルトの`followsWindowActiveState`から`active`にします。

```swift
struct BlurView: NSViewRepresentable {
    
    private let material: NSVisualEffectView.Material
    
    init(material: NSVisualEffectView.Material) {
        self.material = material
    }
    
    func makeNSView(context: Context) -> some NSVisualEffectView {
        let view = NSVisualEffectView()
        view.material = material
        view.blendingMode = .behindWindow
        view.state = .active
        return view
    }
    
    func updateNSView(_ nsView: NSViewType, context: Context) {
        nsView.material = material
    }
}
```

- あとはこれをSwiftUIで利用するだけですね。

```swift
ZStack {
    Color.clear
        .background(BlurView(material: material))
    Text("NSVisualEffectView")
}
```

- 余談ながら、[ultraThinMaterial](https://developer.apple.com/documentation/swiftui/shapestyle/ultrathinmaterial?changes=_9)と全く同じ効果になるものは、[NSVisualEffectView\.Material](https://developer.apple.com/documentation/appkit/nsvisualeffectview/material)には無いようでした。