SwiftUIでMetalKit(MTKView)を使ってみたくなったので、実装してみた。
ゴール
こんな風に使える様にしたい。
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
でのみ行う様にした。
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に関するものはすべてこのクラスで実装している。
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
を実装した。
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
のどちらがより相応しいのかはよくわからなかった。
ソースコード
ContentView
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: ()) { }
}
Shader
# 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;
}
main
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())
}
}
}