Understanding MetalKit: Getting started with Apple's graphics framework

Understanding MetalKit: Getting started with Apple's graphics framework

MetalKit is a high-level framework which makes getting to grips with Metal easier. Here’s how to get started using Apple’s 3D framework.

Getting started

Metal is Apple’s 3D graphics and game pipeline to render 3D objects on Apple devices. Designed to replace OpenGL and other 3D frameworks Metal has the advantage of being optimized for Apple hardware to achieve maximum performance.

Apple provides buttery-smooth 3D rendering on Apple devices at performance levels not possible with other 3D frameworks.

You have probably seen an example of Metal rendering on an iOS or macOS device if you’ve subscribed to and run Apple’s Arcade game app. The brief introduction animation in Arcade is rendered with Metal:

Apple Arcade app intro rendered using Metal.

In 2015 at WWDC, Apple introduced another, higher-level framework for Metal called MetalKit. This framework makes it easier to use Metal by providing some higher-level functions which make 3D app development simpler.

Specifically, MetalKit provides additional Metal APIs in the following areas:

    Texture loading Model I/O View management

Texture loading

Using MetalKit, asset and texture loading is easier by using the MTKTextureLoader class. This class provides an easy way to load assets and textures as well as set texture options.

These options include mipmap usage and loading, cache and storage modes, texture coordinate transformation, cube texture usage, and RGB color options.

A mipmap (or MIP map) is just a multi-layered image with each layer being a progressively lower resolution than the preceding layer. Mipmaps are used to speed image rendering and remove aliasing artifacts such as Moire patterns.

A Moire pattern is a distracting banding or color artifact pattern that sometimes appears in computer graphics consisting of lines or regular pixel patterns such as alternating pixel grids:

A Moire pattern.
A Moire pattern.

Full documentation for MTKTextureLoader is available on Apple’s developer website in the Metal framework documentation at Documentation/MetalKit/MTKTextureLoader.

Model I/O

Model I/O is Apple’s developer framework for managing 3D and 2D assets. MetalKit’s Model I/O integration includes APIs for quickly loading textures into Metal buffers and use mesh data using containers.

There are currently about a half-dozen Model I/O-related classes in MetalKit, mostly dealing with meshes. (We’ll get to classes and object-oriented programming in a minute).

View management

Most iOS and macOS apps use views – standard classes that present visual information and UI elements on-screen. Different view subclasses provide different types of views.

For example, in iOS a UIView is the view base class, but UIButton is a button view class derived from UIView. By using object-oriented view classes in iOS or macOS, you can build additional functionality which relies on standard functionality already defined by Apple classes.

This is known as object inheritance. Think of an object in an app as a bundle of code that encapsulates both code and data that code operates on. By bundling both together into objects, code can be easily reused and repurposed by additional objects.

In particular in MetalKit, a new class – MTKView – is provided which allows developers to create fully-fledged Metal views in apps. By having a dedicated Metal view class, the view can be drawn and managed optimally by Metal without any additional code.

Apple’s documentation for MTKView is on the developer website at Documentation/MetalKit/MTKView. MTKView also requires you to first set an MTLDevice in one of its properties to tell it which device and screen to render Metal objects into.

MTKView also provides an MTLRenderPassDescriptor when asked which you can render your textures into. Check out the Documentation/Metal/Render Passes section of Apple’s developer site.

A little OOP

In Object-Oriented Programming (OOP), objects are defined by classes. A class is a definition in a source code file that defines what an object contains, and in Swift, the actual implementation of an object.

A class defines methods (functions) that can receive messages sent to them by other objects to perform some function. Each method contains code to perform some work.

A class also defines properties or variables that can contain data. Typically a class’s methods perform some work on the class’s properties. Most, but not all methods can read most (but not all) properties contained in the class or in one of its superclasses (parent classes).

All of the above is known as object encapsulation. Objects encapsulate both data and methods to keep everything tidy and organized. It’s easier to transport, reference, copy, and use objects with their associated data in one place than it is to have to keep track of the data separately.

Inheritance is an OOP feature by which new classes can be defined from another class. The derived object is called a subclass and the parent class is called the superclass.

Long chains of objects can be defined by subclassing. Inheritance is powerful because it allows you to reuse existing code with almost no work.

Subclasses inherit all the behavior and properties of their parent classes with almost no additional work. Subclasses can add additional methods only they (or their subclasses) know about.

Even better, when you instantiate (create) an instance (one copy) of an object in a program it also instantiates a copy of all its superclass objects automatically.

With one line of code, you can gain vast levels of program functionality just by creating one instance of a class.

Instantiation is simply creating an object, allocating memory for it in RAM, and making it available to a program.

All of this is usually defined in one or, in the case of Objective-C, two source code files – usually one or two files per class.

So in our discussion above, an MTKView is defined as a class (by Apple) and is instantiated when created in code (by you). The result is an MTKView object in memory, ready for use. When the MTKView object is no longer needed, it is de-allocated which removes it from memory, destroying it.

Most apps are programs that create, use, manage, and destroy hundreds of such objects.

The OOP programming paradigm is powerful because it vastly reduces the amount of code needed via subclassing and reuse, and keeps programs more modular and reusable by encapsulating code and data.

Once you’ve written a class to do some specific work, you can simply reuse the class or subclass it in another program to create another app quickly.

Core Animation in MetalKit

Like many iOS or macOS standard views, MTKView also has a Core Animation Layer. Core Animation is Apple’s high-performance 2D animation framework.

Most views have a CALayer – a Core Animation layer object which can draw and animate 2D graphics. CALayers can be grouped and combined to create complex animations.

MTKView has its own CALayer subclass called CAMetalLayer which Metal can render into. You can combine CAMetalLayer with other CA layers to create combined 2D and 3D animations.

In most cases for both 2D and 3D CALayers, drawing is much faster and more efficient than the drawing that occurs in UIViews. You can also set the opacity, or alpha of CA layers to make parts of them transparent.

MTKView modes

MTKView supports three modes of drawing:

    Timed Notifications Explicit

In Timed drawing the view updates at regular intervals set internally in the object. Most games use this mode when a game scene is rendered or drawn at a specific rate described in frames per second (fps).

With Timed mode, you can also set or clear the isPaused property to start and stop the view animation.

In Notification mode, redraw happens when some part of all of the view becomes invalidated. This allows you to redraw just a portion of the view or layer – which takes less time and improves game performance.

To force a redraw using Notification mode simply send the view object a setNeedsDisplay() message to force it to redraw. This forces the view to redraw all its subviews by sending them each a setNeedsDisplay() message also.

In Explicit drawing, you redraw view content by sending the view object a draw() message directly. This is generally discouraged unless you have some custom drawing workflow you use that does something outside the standard view/subview hierarchy.

You can also redraw only parts of a view by sending their subviews setNeedsDisplay() message also, thereby bypassing the top-level view redraw. In general, the fewer objects that get redrawn, the better the performance.

In the case of an MTKView or a subclass thereof, in your drawing method, you obtain an MTLRenderPassDescriptor from the view, render into it, then present the resulting drawable for display.

A drawable is any Metal object which has been encoded and is ready to be displayed.

MTKViewDelegate

In Apple’s Swift and Objective-C programming languages, a delegate is an object that performs some work on behalf of another object.

Usually, one object will declare a delegate object as one of its properties, and the delegate declares which methods (functions) it provides.

Delegates are powerful because you can change the behavior of an object simply by changing its delegate property. Delegates are also used to provide additional functionality to objects without having to subclass an object to create another object.

MTKView has its own delegate object called MTKViewDelegate class also described in Apple’s documentation. MTKViewDelegate mostly responds to view redraw and resize events.

MTKViewDelegate also inherits from a standard Objective-C protocol common to all Apple objects called NSObjectProtocol.

Think of delegates and protocols as additional objects and methods which can be attached to or “glued” onto other objects.

In Objective-C and Swift, a protocol is simply a list of additional methods a class must implement. The contents of each method in a protocol are up to each class to define.

The MTKViewDelegate is mostly concerned with changing a view’s layout (on device rotation, for example), and drawing.

For example, you could define several MTKViewDelegate objects, each with a different behavior, then change your MTKView‘s drawing or rotation behavior simply by resetting its delegate property to any one of the delegate objects at will and redrawing.

Rendering

When using MTKView, implement the methods of the MTKViewDelegate in your renderer. This allows your renderer to interact with the MTKView and provide drawing and layout changes.

You can obtain info when it is time to render each frame by using the MTKView’s currentRenderPassDescriptor property. This allows you to interact with each frame to be rendered.

For example in Swift:

if let onscreenDescriptor = view.currentRenderPassDescriptor

This gets the MTKView‘s current render pass descriptor and stores it in a variable called onscreenDescriptor.

After rendering, you must use the drawable to update the view’s contents. To do so call the present(_:) method on the MTLCommandBuffer object, then send the commit() message and the command buffer to the GPU for display.

There’s a more detailed discussion of this process in the MTKView‘s documentation.

SIMD

Apple also has a math-related framework called SIMD which comes in handy when manipulating 3D and 2D objects and performing calculations on them and matrices. Most of these functions are used to perform fast, efficient floating point math prevalent in 3D calculations.

SIMD can come in handy when you need to transform 3D objects and vertices on objects. The most common and useful data structure in SIMD is simd_float4x4, which is a four-by-four matrix of single-precision floating values.

Tying it all together in Xcode

Armed with all this background knowledge, you’re now ready to create a MetalKit app in Xcode. In the following example, we’ll assume you’ll be creating a simple 3D app containing a single scene that contains a single Metal 3D object.

To write an Xcode MetalKit app you’ll need to be familiar with Apple’s Swift and Objective-C programming languages, and a little bit of ANSI-C – an earlier C-only language invented at Bell Labs in 1972 when UNIX was created.

To get started open Xcode, and select File->New Project from the File menu. From the project template chooser, choose iOS or macOS at the top, then choose Game from the icons below and click Next:

Chose the
Chose the “Game” template.

On the next pane enter an app name, bundle ID, and organization info and select Swift and Metal from the two lower popup menus:

Enter project details.
Enter project details.

Click Next and save your new Xcode project to disk.

You’ll also need to define a texture image for your 3D object as a .png file and add it to your Xcode project. This texture file gets “wrapped” onto your 3D object at render time.

Xcode’s Metal game template app provides the minimal default template source files you’ll need for your app, but first, you’ll need to add the Metal frameworks to tell Xcode to link those frameworks to your app at runtime.

To do so, in the Xcode project editor select the name of your project by selecting the project icon in the upper left corner of the project window, then select the target name to the right of that under the Targets section:

Select the target.
Select the target.

Scroll to the bottom of the window and under the “Frameworks, Libraries, and Embedded Content” section, click the “+” button. The framework selection pane will appear.

Click the
Click the “+” button.

Type “metal” in the search box at the top, and Command-click on six of the seven frameworks listed, excluding the “MetalFX.framework”. There are hundreds of Xcode frameworks available.

Add framework sheet.
Add framework sheet.

You’ll also want to add the libswiftsimd.tbd library, Core Services frameworks, and optionally the Accelerate framework.

“tbd” is a placeholder for “To be determined” as the version numbers of the actual code libraries can change. Including a .tbd library in Xcode tells Xcode to use the most recent version of that library.

If you want to use Model I/O to manage assets, also add “libswiftModelIO.tbd” and “ModelIO.framework”.

If you created an iOS app in the template chooser, also add UIKit.framework. If you created a macOS app in the template chooser, also add Cocoa.framework.

Finally, include the Foundation.framework and CoreFoundation.framework. Foundation is a core C-language framework that most iOS and macOS apps use. All Foundation API calls are in plain C.

Shaders

Complete code for a Metal game app is beyond the scope of this article so we’ll briefly cover just the basics here for our one-object example. Apple’s sample project template creates a single 3D cube that rotates in space.

Xcode creates an app delegate file that controls the general event loop of the app itself, and a ShaderTypes.h file which is a header file defining the shader’s mesh and vertex info along with a C struct defining the projection matrix and model view matrix.

These are used by the shader when drawing.

The “Shaders.metal” file imports the “ShaderTypes.h” header file defined above which is shared between the renderer and the GameViewController.swift file which we’ll get it in a moment. You import header files into other Swift or Objective-C source code files using the import preprocessor directive:

#import "ShaderTypes.h"

Preprocessor directives are compiler instructions that run prior to actual compilation and usually begin with a “#” sign.

“Shaders.metal” also imports two other files, metal_stdlib and simd.h using the earlier ANSI-C import directive #include. Both #import and #include are similar and we won’t get into the detailed differences between the two here.

Below that you’ll see this line:

using namespace metal;

Namespaces are a C++ idiom that allows similar or identical sections of code to be defined and isolated by defining them under a namespace. Each namespace has its own name, in this case metal.

In Shaders.metal you define a Vertex and ColorInOut structure, and several functions which define the shaders – in this case only a vertex shader and fragment shader. The fragment shader also contains a sampler variable which allows you to define and use mipmaps if you wish.

The fragmentShader function takes as its arguments color information, a Uniforms structure defined in SharderTypes.h, and a texture2d as defined in the Metal library header “metal_texture”.

The Uniforms parameter contains, as previously discussed, the projection matrix and the model view matrix.

Renderer

The next file, Renderer.swift defines the object’s Renderer class which inherits from the base Objective-C class, NSObject and conforms to the MTKViewDelegate protocol.

As a bit of a historical note, NSObject harkens all the way back to the NeXT Computer days – Steve Jobs‘ second company after he was fired from Apple in 1985. NeXT invented Objective-C and had an OS and framework called NeXTStep. The “NS” in NSObject stands for “NeXTStep”.

Most early NeXTStep objects had the prefix “NS” to differentiate them from third-party objects. When Apple bought NeXT Computer Inc. in 1997 it acquired all of NeXT’s technology, including NeXTStep.

To this day macOS and iOS are based on NeXTStep.

Properties of the Renderer class include a MTLDevice, MTLCommandQueue, MTLBuffer, MTLRenderPipelineState, MTLDepthStencilState, and MTLTexture as well as properties for the matrices, rotation, mesh, and a semaphore.

A semaphore is a thread (path of execution) that relies on a flag to tell it when it can run and when it can’t.

When you instantiate a Render object, you pass it an MTKView in its init method, which we’ll get to in a moment.

As soon as the object is created, its init method runs, and all the code in that method runs.

The init method sets up and assigns all its properties at the top of the method, then creates a render buffer via the self.device.makeBuffer() line.

Then it sets a few properties on the metalKitView passed in to the init method, creates a vertex descriptor via Renderer.buildMetalVertexDescriptor(), and then builds the render pipeline via Renderer.buildRenderPipelineWithDevice().

Next, the code creates depth and stencil info, and then creates a mesh via Renderer.buildMesh.

Finally, it builds a color map and texture via Renderer.loadTexture().

You’ll need to use the Renderer’s texture loader method, loadTexture:device:textureName: to load your texture from the .png file you created above – passing the method your texture’s filename – in this example "ColorMap".

The do/catch Swift construct is for error handling. The code contained in do is attempted and if it fails, the catch block is run, else program execution continues normally.

The superclass’s init() method

Finally at the end of the Renderer’s init method the superclass’s init method is run:

super.init()

Sending the super.init() message to the superclass at the end of a Swift class’s init method ensures the entire chain of objects in the class hierarchy gets created. This is a standard pattern in Swift and Objective-C.

If you fail to call a superclass’s init method, it’s highly likely the object will crash, or at best not function properly – or your app itself will crash.

Since subclasses rely on superclass methods while running, if the superclass object doesn’t exist, a subclass’s method call may be sent off into random memory space where the code it is expecting doesn’t exist.

When that happens and the processor tries to execute the code at that memory location, a crash is certain – there’s no code there to run.

super.init() is usually called last in Swift and Objective-C in order to give your object time to do any setup it needs before the superclass object is set up.

Finally, the Renderer’s init method ends with the closing “}” brace.

Renderer methods

Immediately after the init() method in Renderer.swift are the actual implementations of the other methods in the Renderer class. Each Swift function is prefixed with class func followed by the function name, and any function parameters in parenthesis.

If a Swift method returns a value upon completion, that return value type is defined by the -> construct. For example:

class func buildMetalVertexDescriptor() -> MTLVertexDescriptor defines a method (function) named buildMetalVertexDescriptor which returns a MTLVertexDescriptor on successful completion. This is called the return value or return type.

As we saw previously the buildMetalVertexDescriptor method is called internally on object instantiation from the init()method. Many objects work this way.

But most methods can also be called from external objects unless a class definition explicitly prohibits it.

Game loop

The Renderer game loop drives most Metal games, along with the Renderer and MTKView’s draw methods. This combined with the main event loop monitored in the application delegate object drives the app as it is running on a device.

In the Render.swift file you’ll notice a method named private func updateGameState() this method can be run periodically to update any state stored in the game such as object positions, mouse, keyboard, or game controller inputs, position, timing, scores, etc.

The Swift keyword private means this method is private to and can only be called from this object and any extensions defined in this source file only – external objects can’t send that message to the Renderer object.

This additional access control ensures correct program execution only from within and by certain objects – in this case since the Renderer is responsible for the general execution and control of the game, you wouldn’t want any external object interfering with it.

Apple has an entire object Access Control section in the Swift developer documentation on the Swift Documentation website.

Frame rendering

Next in Renderer.swift, we see the draw() method:

func draw(in view: MTKView)

You pass in the MTKView you want the drawing done into. Note this function has no return value. Such functions in Swift and Objective-C are called void functions.

In the draw() method, which gets called once per frame, the semaphore is told to wait for a certain amount of time:

inFlightSemaphore.wait()

Then the command buffer is created and sent to the semaphore for rendering, adding a completion handler.

A completion handler is a function that gets run automatically when some other tasks or thread finishes. Completion handlers are a way of executing code in a sequential manner but only when some other section of code finishes.

Completion handlers provide guaranteed execution of code, but without having to write code to manage complex timer algorithms and wait conditions.

Next, the 3D object buffers and game state are updated in that order:

self.updateDynamicBufferState()

self.updateGameState()

Next, a render pass descriptor is obtained from the MTKView and the render pass encoder properties are updated:

let renderPassDescriptor = view.currentRenderPassDescriptor

Then a short loop runs to get the mesh vertex descriptor layouts and buffers and store them in the render encoder. Then the render encoder’s fragment texture info is set:

renderEncoder.setFragmentTexture(colorMap, index: TextureIndex.color.rawValue)

Next, for each mesh (object) in the .submeshes array, renderEncoder.drawIndexedPrimitives() is called. This is where each object in the scene is encoded.

To end the encoding phase renderEncoder.endEncoding() is called. The objects are now all ready to be drawn.

The view’s drawable is then obtained via:

if let drawable = view.currentDrawable

and if successful, the entire command buffer is then drawn with:

commandBuffer.commit()

The call to commit actually sends the scene’s frame to the GPU for display onscreen.

All of the above happens at thirty, sixty, or one-hundred twenty fps.

The Renderer.swift file ends with a few general 3D math transformations and rotation functions.

Displaying the scene in a view onscreen

You will notice two additional files in the Xcode project: GameViewController.swift and Main.storyboard. These are typical files found in most iOS apps.

A typical iOS app contains a central top-level UIViewController class defined in the UIKIt framework. A UIViewController is a class that manages and controls another UIKIt class – UIView.

A UIView class is a class that contains other UIView subclasses such as buttons, images, text, and other UIKIt objects. UIView is how an iOS app’s user interface is represented onscreen.

Every UIViewController class has a property named view which is an instance of UIView. The view controller manages the UIView.

If you look at Apple’s documentation for UIViewController, you’ll notice a half dozen methods for managing the view – namely methods for loading the view, being notified when the view loads, and unloading views.

In most iOS apps, you don’t load the top-level view directly – you load it by instantiating a UIViewController subclass you define (in this example a GameViewController). The user interface part of the view is created in Xcode’s Interface Builder editor, or via a text-only SwiftUI view.

Typically when creating an iOS app, you lay out each view in Interface Builder by dragging visual components from the Xcode library and dropping them into a view controller object onscreen.

Once your UI objects are all laid out onscreen you connect them to a view controller’s properties via a Control-click and drag from each UI element to the view controller’s first responder. A first responder is the first object in a view controller object hierarchy that is capable of responding to that object’s messages.

When you release the mouse button from your Control-click and drag above, Xcode displays a list of available object properties to connect the object to.

That’s it – you typically don’t have to do any coding for each UI element – when the view controller gets instantiated and loaded into memory, the Swift or Objective-C runtime makes all the UI connections for you automatically.

This vastly simplifies application development.

Storyboards and Segues

Storyboard files were later added to Xcode to simplify UI layout even further: with Storyboards you define Segues between view transitions – when users navigate between each view on the device, the Segue function you indicate gets called where you can then do any initial view setup or cleanup.

Segues eliminate most view-loading code.

viewDidLoad()

In any case when a view controller finishes loading a view, the controller’s viewDidLoad() method gets called. It’s in viewDidLoad() that you do any additional view setup you need. Once viewDidLoad() exits, the view is ready to use and is displayed onscreen to the user.

You can subclass both UIViewController and UIView to make your views highly customizable. The one thing to remember is that most UI elements in iOS are stored as properties in a UIViewController subclass.

It is possible to create views and view controllers entirely in code without a Storyboard or Interface Builder, but doing so is much more complex and time-consuming.

GameViewController.swift

Let’s take a look at GameViewController.swift

The class is defined at the top of the file:

class GameViewController: UIViewController

This means GameViewController is a subclass of UIViewController.

The class definition is contained in matching open and closed brackets (“{“, and “}”).

Note that the GameViewController class is very short – just over a page. Most of the game processing work happens in the shaders and renderers.

Next, we see two Swift properties as defined by the var keyword:

var renderer: Renderer!

var mtkView: MTKView!

Next we see that GameViewController overrides the UIViewController method viewDidLoad() using the Swift override keyword:

override func viewDidLoad()

This means that when the view controller loads the view and sends the viewDidLoad() message, the GameViewController version of the method will be run instead of the UIViewController version. This is a perfect example of inheritance in action: you can choose to let a superclass’s method run, or override it in a subclass and use that method instead.

Note that in order for this to work, the declarations of both methods in both classes must be identical.

The first thing the override func viewDidLoad() does is send the superclass (UIViewController) a viewDidLoad() message. This allows the UIViewController to do any UI view layout initialization it needs to do.

Without this “super” call the view won’t work correctly because some of its internal parts won’t ever get initialized.

Next, the GameViewController object loads the MTKView and stores it in its internal property mtkView:

guard let mtkView = view as? MTKView else

guard is simply a Swift conditional test to see if something succeeded – similar to if.

GameViewController then also stores a reference to the device’s Metal device in its internal defaultDevice property.

guard let defaultDevice = MTLCreateSystemDefaultDevice() else

The important thing to understand here is that the two internal properties or variables:

var renderer: Renderer!

var mtkView: MTKView!

store references to other objects in memory – in this case the renderer and the Metal view. Once stored, the GameViewController object can access those objects at will. This pattern is how most objects work in Swift and Objective-C.

In Objective-C these two properties would have been declared as:

Renderer *renderer = nil;

MTKView *mtkView = nil;

nil is an Objective-C placeholder which means “nothing” or more specifically no address in memory. nil is used to indicate an Objective-C property or variable doesn’t contain anything.

The '*' is a standard indicator for a C or Objective-C pointer – a variable that holds a memory address to an object instead of a value. Pointers are complex subject so we won’t get into them here.

Also note that Objective-C and C code lines must end with a ';' (semicolon). This isn’t optional – without the semicolon, the code won’t compile and you’ll get an error.

Swift dropped semicolons (but you can actually still use them if you want).

Next the GameViewController stores more references to other objects but this time inside the mtkView property object:

mtkView.device = defaultDevice

mtkView.backgroundColor = UIColor.black

This means store the default rendering device in the mtkView.device property, and store a black UIColor in the tkView.backgroundColor.

UIColor is a standard UIKit object to indicate color – in this case set to black, which will be used as the scene’s background color. Every UIColor object has a .backgroundColor property.

Note what you’re actually doing here is storing references to objects in properties which are themselves properties of this class’s properties. This is confusing at first but once you get the hang of it it’s easy to understand.

By chaining properties across objects, you’re really just Dasiy-chaining objects together.

You can have properties pointing to properties, pointing to other objects. There’s theoretically no limit on how deep property references can go.

Before you release (destroy) an object you should set all its properties to nil in the class’s deinit() method to ensure all references to other objects get released. Failure to do so can result in memory leaks and unwanted retain cycles.

In Objective-C deinit() is called dealloc.

Continuing, the Renderer object is created, passing in the MTKView object and a reference (pointer) to the Renderer is stored in the view controller’s renderer property:

guard let newRenderer = Renderer(metalKitView: mtkView) else

renderer = newRenderer

First, you create the object, then you store a reference to it in a property.

Then the renderer’s pointer to the MTKView is sent the drawableSizeWillChange message:

renderer.mtkView(mtkView, drawableSizeWillChange: mtkView.drawableSize)

This lets the renderer know what the view’s current drawable size is so it knows how and where to scale the scene when it gets sent to the GPU. Note that the drawable size is stored in the MTKView already in its .drawableSize property.

This demonstrates that you can pass an object’s properties to methods as parameters.

Finally, the view’s delegate is set to the renderer itself:

mtkView.delegate = renderer

Recall that in the Renderer.swft file the Renderer class is declared as conforming to the MTKViewDelegate protocol:

class Renderer: NSObject, MTKViewDelegate

This is what allows the mtkView.delegate property to be set to a Renderer object. Without the MTKViewDelegate protocol conformance in the Renderer class definition, the mtkView.delegate = renderer line would likely throw a warning or error when compiled saying that the renderer property doesn’t conform to the MTKViewDelegate protocol.

Also note that one critical gotcha for newcomers to Xcode is that before you destroy a view controller object you must first set its .delegate property to nil. Failure to do so will guarantee your app will crash.

This in fact applies to any Swift or Objective-C object which contains delegates – not just to view controllers.

Why? Because if you don’t release the reference stored in the delegate property first, between the time the containing object actually disappears from memory and the time the system realizes the object has been destroyed, some other object may have sent the delegate object another message.

Not realizing the object which contained the delegate property no longer exists, the message sent to the delegate may still be waiting to be processed – and when it does get processed the delegate is now invalid because its containing object no longer exists.

The delegate gets left dangling in memory but its containing object is long gone – and the system thus has no way to locate the delegate object the message is bound for.

Boom – a crash.

Sending a message to nil in Swift and Objective-C won’t have any harmful effects, and is valid, but sending a message to an address in memory where an object is supposed to be but isn’t will definitely cause a crash.

Run the app

Now you’re finally ready to run the Metal sample app.

Click the Play button at the top of the Xcode window and the code will compile. If there are no errors and everything works, Xcode will launch the iOS Simulator and run the app in it:

Sample app running in iOS Simulator.
Sample app running in iOS Simulator.

Note some, but not all, Metal code won’t run in the simulator. You’ll have to run those Metal programs on a real iOS device instead.

Final Interface Builder tips

As one last look at the sample project, we need to go over a few items in Interface Builder.

Select the Main.storyboard file in Xcode.
Select the Main.storyboard file in Xcode.

If you are new to Xcode and Interface Builder, note that one critical aspect of iOS development most newcomers overlook is that of class names. The class names each item has in Xcode must match exactly each class name as defined in the source code files.

If they don’t, your app won’t work.

For example, the view controller must have its class name set in the Custom Class field in Xcode’s object info panel on the right side. To do so you have to click the Storyboard or .nib (Interface Builder) file, then click the class name in the Scene or view hierarchy, then verify or set the class name in the inspector on the right:

Make sure class names are set in the Storyboard file.
Make sure class names are set in the Storyboard file.

The same holds true for Views and their class names, and other objects such as delegate properties. Failure to set even one class name or property can cause an app to not work.

Most of these usually get set in template files created by Xcode, but it doesn’t hurt to check.

One thing that oddly doesn’t get set in Xcode template files are the connections between view controllers and their View properties. You have to make these connections manually or else your app won’t work.

For example in our sample project, if you Control-click on the Game View Controller object in the view hierarchy you’ll notice that the View property is set to nil. You’ll need to connect the View to the Game View Controller by Control-clicking and then dragging from the Game View Controller to the View in the hierarchy.

When you do, the “Outlets” panel will appear and you need to connect to the “view” property to the Game View Controller object manually:

Connect the view controller and view via the Outlets pane.
Connect the view controller and view via the Outlets pane.

Without this connection, the app won’t work. And the sample template files created by Xcode don’t make this connection for you by default.

Note that the small dot next to outlet names in the Outlet panel indicates whether any given outlet is connected or not.

You may have also noticed that the AppDelegate.swift file contains a subclass of AppDelegate which contains empty boilerplate code but there are no references to the GameViewController anywhere in the app delegate file.

So how does the GameViewController get loaded when the app runs?

The answer is the Storyboard file defines the initial view controller and loads it automatically for you when the app first runs. If you were using older .nib-style (Interface Builder) files and code to load the initial view controller, your app instead would have manually created and loaded a GameViewController object instance the AppDelegate’s application:didFinishLaunchingWithOptions method.

Once the view controller then loaded the view, you would get the viewDidLoad() message on the app delegate if you set the AppDelegate as the view controller’s delegate.

Additional resources

In addition to Apple’s online MetalKit and Metal documentation, there are a number of other good Metal resources you may want to check out.

Be sure to check out metalkit.org and metalbyexample.com which have lots of great tutorials on MetalKit and Metal itself.

Also, be sure to get the definitive third-party book on Metal, the Metal Programming Guide: Tutorial and Reference via Swift by Janie Clayton which teaches you just about everything there is to know about Metal programming.

This has been a long tutorial, but now you should have a much greater understanding of how Metal apps work and how to use MetalKit in your apps to easily load textures and render Metal objects in views in iOS apps.

Add a Comment