# Custom Style Layers (Metal API) Creating a Custom Style Layer with Metal Custom style layers allow you to draw directly with Metal, enabling you to render specialized shapes, custom geometry, or apply advanced visual effects that go beyond what is possible with standard style layers. Below you can find an example of how to create a custom style layer with ``MLNCustomStyleLayer``. In this implementation, a SwiftUI view wraps an ``MLNMapView`` and appends a subclassed custom style layer once the map loads. The layer’s ``MLNCustomStyleLayer/didMoveToMapView:`` method handles initialization, including compiling Metal shaders and creating a [`MTLRenderPipelineState`](https://developer.apple.com/documentation/metal/mtlrenderpipelinestate?language=objc) for subsequent draw operations. The ``MLNCustomStyleLayer/willMoveFromMapView:`` method provides a place to release or invalidate resources when the layer is removed from the map, while the ``MLNCustomStyleLayer/drawInMapView:withContext:`` method encodes the drawing commands using a [`MTLRenderCommandEncoder`](https://developer.apple.com/documentation/metal/mtlrendercommandencoder) and the map’s projection matrix. By projecting latitude/longitude coordinates into a normalized 0–1 space and then transforming them into tile coordinates, the layer ensures that rendered geometry aligns correctly with the base map. ![](CustomStyleLayerExample.png) ```swift struct CustomStyleLayerExample: UIViewRepresentable { func makeCoordinator() -> CustomStyleLayerExample.Coordinator { Coordinator(self) } final class Coordinator: NSObject, MLNMapViewDelegate { var control: CustomStyleLayerExample init(_ control: CustomStyleLayerExample) { self.control = control } func mapViewDidFinishLoadingMap(_ mapView: MLNMapView) { let mapOverlay = CustomStyleLayer(identifier: "test-overlay") let style = mapView.style! style.layers.append(mapOverlay) } } func makeUIView(context: Context) -> MLNMapView { let mapView = MLNMapView() mapView.delegate = context.coordinator return mapView } func updateUIView(_: MLNMapView, context _: Context) {} } class CustomStyleLayer: MLNCustomStyleLayer { private var pipelineState: MTLRenderPipelineState? private var depthStencilStateWithoutStencil: MTLDepthStencilState? override func didMove(to mapView: MLNMapView) { #if MLN_RENDER_BACKEND_METAL let resource = mapView.backendResource() let shaderSource = """ #include using namespace metal; typedef struct { vector_float2 position; vector_float4 color; } Vertex; struct RasterizerData { float4 position [[position]]; float4 color; }; struct Uniforms { float4x4 matrix; }; vertex RasterizerData vertexShader(uint vertexID [[vertex_id]], constant Vertex *vertices [[buffer(0)]], constant Uniforms &uniforms [[buffer(1)]]) { RasterizerData out; const float4 position = uniforms.matrix * float4(float2(vertices[vertexID].position.xy), 1, 1); out.position = position; out.color = vertices[vertexID].color; return out; } fragment float4 fragmentShader(RasterizerData in [[stage_in]]) { return in.color; } """ let device = resource.device var library: MTLLibrary? = nil var shaderError: Error? = nil do { library = try device?.makeLibrary(source: shaderSource, options: nil) } catch { shaderError = error } assert(library != nil, "Error compiling shaders: \(String(describing: shaderError))") let vertexFunction = library?.makeFunction(name: "vertexShader") let fragmentFunction = library?.makeFunction(name: "fragmentShader") // Configure a pipeline descriptor that is used to create a pipeline state. let pipelineStateDescriptor = MTLRenderPipelineDescriptor() pipelineStateDescriptor.label = "Simple Pipeline" pipelineStateDescriptor.vertexFunction = vertexFunction pipelineStateDescriptor.fragmentFunction = fragmentFunction pipelineStateDescriptor.colorAttachments[0].pixelFormat = resource.mtkView.colorPixelFormat pipelineStateDescriptor.depthAttachmentPixelFormat = .depth32Float_stencil8 pipelineStateDescriptor.stencilAttachmentPixelFormat = .depth32Float_stencil8 do { pipelineState = try device?.makeRenderPipelineState(descriptor: pipelineStateDescriptor) } catch { assertionFailure("Failed to create pipeline state: \(error)") } // Notice that we don't configure the stencilTest property, leaving stencil testing disabled let depthStencilDescriptor = MTLDepthStencilDescriptor() depthStencilDescriptor.depthCompareFunction = .always // Or another value as needed depthStencilDescriptor.isDepthWriteEnabled = false depthStencilStateWithoutStencil = device!.makeDepthStencilState(descriptor: depthStencilDescriptor) #endif } override func willMove(from _: MLNMapView) {} override func draw(in _: MLNMapView, with context: MLNStyleLayerDrawingContext) { #if MLN_RENDER_BACKEND_METAL guard let renderEncoder else { return } // Project to 0..1. let p1 = project(CLLocationCoordinate2D(latitude: 25.0, longitude: 12.5)) let p2 = project(CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0)) let p3 = project(CLLocationCoordinate2D(latitude: 0.0, longitude: 25.0)) // Multiply by the world size so it becomes the tile coordinate system. let worldSize = 512.0 * pow(2.0, context.zoomLevel) let p1Tile = CGPoint(x: p1.x * worldSize, y: p1.y * worldSize) let p2Tile = CGPoint(x: p2.x * worldSize, y: p2.y * worldSize) let p3Tile = CGPoint(x: p3.x * worldSize, y: p3.y * worldSize) // Then build a triangle from tile coordinates struct Vertex { var position: vector_float2; var color: vector_float4 } let triangleVertices: [Vertex] = [ Vertex(position: vector_float2(Float(p1Tile.x), Float(p1Tile.y)), color: vector_float4(1, 0, 0, 1)), Vertex(position: vector_float2(Float(p2Tile.x), Float(p2Tile.y)), color: vector_float4(0, 1, 0, 1)), Vertex(position: vector_float2(Float(p3Tile.x), Float(p3Tile.y)), color: vector_float4(0, 0, 1, 1)), ] // Use the camera's full projection matrix *unchanged*. var matrix = convertMatrix(context.projectionMatrix) // Encode renderEncoder.setRenderPipelineState(pipelineState!) renderEncoder.setDepthStencilState(depthStencilStateWithoutStencil) renderEncoder.setVertexBytes(triangleVertices, length: MemoryLayout.size * triangleVertices.count, index: 0) renderEncoder.setVertexBytes(&matrix, length: MemoryLayout.size, index: 1) // Draw the triangle. renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3) #endif } func project(_ coordinate: CLLocationCoordinate2D) -> CGPoint { // We project the coordinates into the space 0 to 1 and then scale these when drawing based on the current zoom level let worldSize = 1.0 let x = (180.0 + coordinate.longitude) / 360.0 * worldSize let yi = log(tan((45.0 + coordinate.latitude / 2.0) * Double.pi / 180.0)) let y = (180.0 - yi * (180.0 / Double.pi)) / 360.0 * worldSize return CGPoint(x: x, y: y) } struct MLNMatrix4f { var m00, m01, m02, m03: Float var m10, m11, m12, m13: Float var m20, m21, m22, m23: Float var m30, m31, m32, m33: Float } func convertMatrix(_ mat: MLNMatrix4) -> MLNMatrix4f { MLNMatrix4f( m00: Float(mat.m00), m01: Float(mat.m01), m02: Float(mat.m02), m03: Float(mat.m03), m10: Float(mat.m10), m11: Float(mat.m11), m12: Float(mat.m12), m13: Float(mat.m13), m20: Float(mat.m20), m21: Float(mat.m21), m22: Float(mat.m22), m23: Float(mat.m23), m30: Float(mat.m30), m31: Float(mat.m31), m32: Float(mat.m32), m33: Float(mat.m33) ) } } ```