SwiftUIでNSFontPanelを使ったフォントピッカーを作成

# 概要
- 現状(2023-10月)、[NSColorPanel](https://developer.apple.com/documentation/appkit/nscolorpanel)のSwiftUIの代替に[ColorPicker](https://developer.apple.com/documentation/swiftui/colorpicker)が出ているが、[NSFontPanel](https://developer.apple.com/documentation/appkit/nsfontpanel)のSwiftUIの代替はでていない。
- そのため`NSFontPanel`をSwiftUIで扱えるピッカー用のViewを作成してみる。
- (下記の`Select...`ボタンが今回作成したフォントピッカー)

<img width="600" alt="image" src="https://i.imgur.com/iOxZfhD.png">

# 参考
- [tyagishi / FontPicker](https://github.com/tyagishi/FontPicker/tree/main)
- [structをCodableとRawRepresentableに準拠させて@AppStorageで値を保存する](https://zenn.dev/ikeh1024/articles/bd7dcf28e38f3c)

# GitHub
- [nsfontpanel\-in\-swiftui\-demo](https://github.com/pommdau/nsfontpanel-in-swiftui-demo/tree/master)

# 実装

- Bindingで`NSFont`を渡し、裏で`NSFontPanel`を呼び出して変更を反映させる。
- これによりSwiftUI側では特に意識せずに`NSFont`を渡して`NSFontPanel`のUIでフォントの更新が可能となる。

```swift
import SwiftUI

/// refs: [tyagishi / FontPicker](https://github.com/tyagishi/FontPicker/tree/main)
fileprivate class FontPickerDelegate {
    
    private var parent: FontPicker

    init(_ parent: FontPicker) {
        self.parent = parent
    }
    
    @objc
    private func changeFont(_ id: Any) {
        parent.fontSelected()
    }

}

struct FontPicker: View {
    
    private let label: LocalizedStringKey?
    @Binding private var font: NSFont
    @State private var fontPickerDelegate: FontPickerDelegate? = nil
    
    init(_ label: LocalizedStringKey? = nil, selection: Binding<NSFont>) {
        self.label = label
        self._font = selection
    }
    
    var body: some View {
        Button {
            fontPickerDelegate = FontPickerDelegate(self)
            NSFontManager.shared.target = fontPickerDelegate
            NSFontPanel.shared.setPanelFont(font, isMultiple: false)
            NSFontPanel.shared.orderBack(nil)
        } label: {
            if let label {
                Text(label)
            } else {
                EmptyView()
            }
        }
    }
    
    fileprivate func fontSelected() {
        font = NSFontPanel.shared.convert(self.font)
    }
}
```

- また今回フォント名とサイズを保存したいので、`FontData`というカスタムクラスを作成し、これを`@AppStorage`で保存している
    - 参考: [structをCodableとRawRepresentableに準拠させて@AppStorageで値を保存する](https://zenn.dev/ikeh1024/articles/bd7dcf28e38f3c)

```swift
struct FontData {
    var name: String
    var pointSize: CGFloat
}
```

- 外部的には`NSFont`を公開して、内部的に`FontData`でデータの保存を行っている。

```swift
public class Settings: ObservableObject {
    
    static let shared = Settings()
    
    @AppStorage("selected-font")
    private var fontData: FontData = .init(name: "Helvetica", pointSize: 14)

    var nsFont: NSFont {
        get {
            fontData.nsFont
        }
        
        set(newValue) {
            fontData = newValue.fontData
        }
    }
}
```

- 呼び出し例は以下の通り。

```swift
import SwiftUI

struct ContentView: View {
    
    @ObservedObject var settings = Settings.shared
    
    var body: some View {
        Form {
            Section {
                HStack {
                    ...
                    FontPicker("Select...", selection: $settings.nsFont)
                }
                ...
                .frame(maxWidth: .infinity, alignment: .trailing)
                
            }
            
            Section {
                Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, ...")
                    .font(Font(settings.nsFont))
            } header: {
                Text("Preview")
            }
        }
        .formStyle(.grouped)
    }
}
```