As you may see from previous episodes, Metal is a super powerful instrument for GPU computing. But as a graphics framework, its main purpose is the visual representation of data: from UI elements to interactive scenes in video games or graphic editors. In this episode, I’ll explore Metal’s approach to UI, how to implement your own Metal view, and how to use MTKView.
Fundamentally, all native UI on Apple platforms is rendered with CoreAnimation. Basically, the visual representation of UI is a CALayer, which may have:
These layers are rendered directly with Metal, or baked with CoreGraphics (as Metal isn’t great at vector graphics) and then placed into the screen buffer:

Though there’re lots of ready-to-use layers whose implementations are hidden from you, there’s also CAMetalLayer, which contains CAMetalDrawable, which contains an MTLTexture, which you can render into.
UIViewCAMetalLayerCAMetalDrawableMTLTextureAlso, as
CALayermay have a masking layer, you can render into that too for some special masking effect.
To use CAMetalLayer instead of default layer, add this to your view implementation:
// ...
override class var layerClass: AnyClass {
return CAMetalLayer.self
}
// ...
To synchronize rendering with device frames, you need to use CADisplayLink.
Starting from iOS 17 (macOS 14), you can use
CAMetalDisplayLink, which offers more control over FPS and makes rendering smoother and more efficient.
In the display link, you can set the preferred FPS, incorporate a rendering loop, and manage it (for example, pause it).
// ...
var displayLink: CADisplayLink
func createDisplayLink() {
displayLink = CADisplayLink(target: self, selector: #selector(render))
displayLink.preferredFramesPerSecond = 60
displayLink.isPaused = false
displayLink.add(to: .current, forMode: RunLoop.Mode.default)
}
@objc
func render(displayLink: CADisplayLink) {
// Render here
}
// ...
As you know from previous episodes, you need a texture to render into. As mentioned above, you can find the texture in CAMetalDrawable:
// ...
@objc
func render(displaylink: CADisplayLink) {
guard let metalLayer = self.layer as? CAMetalLayer, // (1)
let drawable = metalLayer.nextDrawable(), // (2)
let commandBuffer = self.commandQueue.makeCommandBuffer() // (3)
else {
return
}
let targetTexture = drawable.texture // (4)
// rendering into the texture
commandBuffer.present(drawable) // (5)
commandBuffer.commit()
}
// ...
allowsNextDrawableTimeout to true.commit() method.NOTE: Make sure to retrieve the correct pixel format and sampling parameters from the drawable so your pipelines remain compatible with the layer.
Though the manual way isn’t particularly complex, you can still take an easier route by just using MTKView from MetalKit. It covers most of these details, and in most use cases, you only need to implement MTKViewDelegate:
// ...
func draw(in view: MTKView) {
guard let commandBuffer = commandQueue.makeCommandBuffer(),
let encoderDescriptor = view.currentRenderPassDescriptor // (1)
else {
return
}
if let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: encoderDescriptor) {
// render commands...
renderEncoder.endEncoding()
}
if let drawable = view.currentDrawable { // (2)
commandBuffer.present(drawable)
}
commandBuffer.commit()
}
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
// (3)
}
// ...
MTKView, which is already prepared.MTKView for presenting.Additionally, you can directly adjust pixel formats, preferred FPS, and other parameters on the view.
Metal, but hidden in CoreAnimation: (CoreAnimation → CALayer → CAMetalLayer → CAMetalDrawable → MTLTexture)MetalKit.