SwiftUIでMetalKit

SwiftUIでMetalKit(MTKView)を使ってみたくなったので、実装してみた。

## ゴール
こんな風に使える様にしたい。

```Swift
struct ContentView: View {
    @EnvironmentObject var representedObject: Model
    var body: some View {
        VStack {
            Text(representedObject.msg)
                .font(.largeTitle)
            MetalView()
        }.padding()
    }
}
```

## 実装
`MetalView`でNSViewRepresentableを実装し、`MetalViewCoordinator`でMTKViewDelegateを実装した。
`MetalViewCoordinator`はNSViewRepresentableのCoordinatorにもなっている。

### MetalView
`MTKView`を作るが、保持は`MetalViewCoordinator`に任せた。
ただし、`MTKView`の設定は`MetalView`でのみ行う様にした。

```Swift
struct MetalView: NSViewRepresentable {
    typealias NSViewType = MTKView
    typealias Coordinator = MetalViewCoordinator

    func makeCoordinator() -> MetalViewCoordinator {
        let view = MTKView()
        view.device = view.preferredDevice
        return MetalViewCoordinator(view)
    }

    func makeNSView(context: Context) -> MTKView {
        let view = context.coordinator.view
        view.delegate = context.coordinator
        view.enableSetNeedsDisplay = true
        return view
    }
}
```

### MetalViewCoordinator
`MTKView`を保持し`MTKViewDelegate`を実装している。
Metalに関するものはすべてこのクラスで実装している。

```Swift
class MetalViewCoordinator: NSObject, MTKViewDelegate {
    var view: MTKView
    var device: MTLDevice!
    init(_ view: MTKView) {
        self.view = view
        self.device = view.device
    }

    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
         :
    }

    func draw(in view: MTKView) {
         :
    }
}
```

### AppDelegate

ウィンドウを閉じた時に、アプリケーションを終わらせたかったので、`NSApplicationDelegateAdaptor`をつかって`AppDelegate`を実装した。

```Swift
class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { true }
}

struct SwiftUIApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
```

## まとめ
`MetalViewCoordinator`のおかげで`MTKViewDelegate`を実装するクラスを別に準備する必要がなかった。
`MTKView`の保持は`MetalView`と`MetalViewCoordinator`のどちらがより相応しいのかはよくわからなかった。

![SwUIApp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98396/9fe6b9a0-64dd-59fc-4efb-5ae8968bca65.png)

### ソースコード

<details><summary>ContentView</summary><div>

```Swift:ContentView.swift
import SwiftUI
import MetalKit

struct ContentView: View {
    @EnvironmentObject var representedObject: Model
    var body: some View {
        VStack {
            Text(representedObject.msg)
                .font(.largeTitle)
            MetalView()
        }.padding()
    }
}

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

class MetalViewCoordinator: NSObject, MTKViewDelegate {
    var view: MTKView
    var device: MTLDevice!
    var library: MTLLibrary!
    var commandQueue: MTLCommandQueue!
    var renderPipelineState: MTLRenderPipelineState!
    var vertexBuffer: MTLBuffer!

    func makeCommandBuffer() -> MTLCommandBuffer {
        commandQueue.makeCommandBuffer()!
    }

    func makeFunction(_ name: String) -> MTLFunction? {
        library.makeFunction(name: name)
    }

    struct Vertex {
        var position: simd_float3
        var color: simd_float4
    }
    
    var vertices: [Vertex] = [
        Vertex(position: simd_float3( 0,  1, 0), color: simd_float4(1, 0, 0, 1)),
        Vertex(position: simd_float3(-1, -1, 0), color: simd_float4(0, 1, 0, 1)),
        Vertex(position: simd_float3( 1, -1, 0), color: simd_float4(0, 0, 1, 1))
    ]

    init(_ view: MTKView) {
        self.view = view
        self.device = view.device
        library = device.makeDefaultLibrary()
        commandQueue = device.makeCommandQueue()
        super.init()

        let desc = MTLRenderPipelineDescriptor()
        desc.colorAttachments[0].pixelFormat = .bgra8Unorm
        desc.vertexFunction = makeFunction("vertex_function")
        desc.fragmentFunction = makeFunction("fragment_function")
        renderPipelineState = try! device.makeRenderPipelineState(descriptor: desc)
        vertexBuffer = device.makeBuffer(bytes: vertices, length: MemoryLayout<Vertex>.stride * vertices.count, options: [])
    }

    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
        guard view.frame.size != size else { return }
        view.drawableSize = view.frame.size
    }

    func draw(in view: MTKView) {
        let cmdbuf = makeCommandBuffer()
        let cmdenc = cmdbuf.makeRenderCommandEncoder(descriptor: view.currentRenderPassDescriptor!)!
        cmdenc.setRenderPipelineState(renderPipelineState)
        cmdenc.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
        cmdenc.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertices.count)
        cmdenc.endEncoding()
        cmdbuf.present(view.currentDrawable!)
        cmdbuf.commit()
    }
}

struct MetalView: NSViewRepresentable {
    typealias NSViewType = MTKView
    typealias Coordinator = MetalViewCoordinator

    func makeCoordinator() -> MetalViewCoordinator {
        let view = MTKView()
        view.device = view.preferredDevice
        return MetalViewCoordinator(view)
    }

    func makeNSView(context: Context) -> MTKView {
        let view = context.coordinator.view
        view.delegate = context.coordinator
        view.enableSetNeedsDisplay = true
        return view
    }

    func updateNSView(_ view: MTKView, context: Context) { }

    static func dismantleNSView(_ view: MTKView, coordinator: ()) { }
}
```

</div></details>

<details><summary>Shader</summary><div>

```C++:Shader.metal
#include <metal_stdlib>
using namespace metal;

struct Cpu2Vertex {
    float3 position;
    float4 color;
};

struct Vertex2Fragment {
    float4 position [[ position ]];
    float4 color;
};

vertex Vertex2Fragment vertex_function(const device Cpu2Vertex *vertices [[ buffer(0) ]],
                                       uint vertexID [[ vertex_id ]]) {
    Vertex2Fragment r;
    r.position = float4(vertices[vertexID].position, 1);
    r.color = vertices[vertexID].color;
    return r;
}

fragment float4 fragment_function(Vertex2Fragment v [[ stage_in ]]) {
    return v.color;
}
```

</div></details>

<details><summary>main</summary><div>

```Swift:App.swift
class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) { }
    func applicationWillTerminate(_ notification: Notification) { }
    func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { true }
    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true }
}

class Model: ObservableObject {
    @Published var msg: String = "hello!"
}

@main
struct SwUIApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate

    var body: some Scene {
        WindowGroup {
            ContentView()
                .frame(minWidth: 300, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity, alignment: .center)
                .environmentObject(Model())
        }
    }
}
```

</div></details>