Creating a Window with GLFW
Now that we’ve set up Metal on our machine, we need a way to display our pixel magic on the screen, but before we do that, we need a window.
Technically, we could use platform-specific code to open a window, create a rendering context, attach it to the window, and handle all the input events — like resizing, moving the window, and managing edge cases.
However, that approach is both complex to set up and beyond the scope of this tutorial series.
That’s exactly why we’ll use a powerful cross-platform library that takes care of all that for us — including window creation, input handling, and even support for OpenGL and Vulkan contexts, if you’re interested in those as well.
GLFW
GLFW is a lightweight, open-source library designed to create windows, manage OpenGL/Vulkan/Metal contexts, and handle user input — like keyboard, mouse, and gamepad events — in a platform-independent way.
It’s widely used in graphics applications and game development for its simplicity and minimal overhead.
GLFW can be linked into your project either statically or dynamically.
When you link it statically, the entire GLFW library is compiled directly into your application binary - this makes deployment easier since there’s no need to ship external dynamic libraries alongside your executable.
It also reduces the risk of runtime errors due to missing or incompatible library files on the user’s system.
Static linking on the other hand increases the size of your binary and requires you to recompile your application if you update GLFW.
In addition to this of course, you must include the GLFW license text when distributing your application.
Dynamic linking means your application loads the GLFW library at runtime from a separate file, such as a .dll
on Windows, .dylib
on macOS, or .so
on Linux.
This keeps your binary smaller and allows you to update or replace the GLFW library without recompiling your entire project.
The downside is that you must ensure the dynamic library is correctly installed and accessible on the target system, which can complicate deployment and lead to runtime errors if the library is missing or incompatible.
Each method has trade-offs, and the choice depends on your project’s needs: static linking for simplicity and portability, dynamic linking for modularity and easier updates.
If you’re curious, you can take look at this, explaining things in a bit more detail.
To keep things simple and self-contained, we’ll be using GLFW’s static library for this tutorial.
The first thing we’ll be doing is downloading the library from the official website, specifically the precompiled binaries.
If you’re on a Mac with Apple Silicon (M1, M2, M3, or M4), don’t worry — the macOS x64 package also includes arm64 libraries, so you’re covered.
Once downloaded go ahead and move the glfw folder in the root folder of your project.
The only parts we really care about are the include
folder, which contains the GLFW header files, and the lib
directory corresponding to your operating system and architecture — for example, on Apple Silicon, that would be the lib-arm64
folder.
Now let’s set it up:
- Go to
Build Settings
→Header Search Paths
, click+
, and add the path to the GLFWinclude
folder. - Then go to
Library Search Paths
and add the path to the appropriatelib
folder. - Under
Build Phases
→Link Binary With Libraries
, click+
, chooseAdd Other...
, and select the.a
static library file from the correctlib
folder for your architecture.
Congratulations! Now GLFW is successfully integrated into your project.
In these tutorials you’ll find both
.mm
and.cpp
files - Why? The.mm
extension tells the compiler we’re using Objective-C++, which allows us to seamlessly mix Objective-C and C++ code — something that’s essential when working with Cocoa (Apple’s native windowing system) and Metal together.
Let’s have a look at how we can go ahead and create a window now:
main.mm
#pragma once
#pragma once
is a preprocessor directive that ensures a header file is included only once during compilation, no matter how many times it’s referenced.
This prevents multiple definition errors and redundant code processing, which can happen when the same header is included in several files, directly or indirectly.
It’s a modern alternative to traditional include guards like:
#ifndef MY_HEADER_H
#define MY_HEADER_H
// header contents here
#endif
Unlike include guards, #pragma once
is shorter, cleaner, and less error-prone — though it’s not officially part of the C++ standard.
That said, it’s widely supported by all major compilers, including GCC, Clang, and MSVC, making it safe to use in nearly all modern projects.
However, if you’re working on a highly portable, cross-platform codebase where maximum compiler compatibility is critical, keep in mind that #pragma once
may not be recognized by very old or obscure compilers.
#define GLFW_INCLUDE_NONE
#import <GLFW/glfw3.h>
This tells GLFW not to include any OpenGL or Vulkan headers for us when we include glfw3.h
.
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
Up next, we define two separate variables, to easily set our width and height, SCR_WIDTH
and SCR_HEIGHT
- note that we are using unsigned int
- this is a common convention, since the window size can’t be negative.
On top of this, many graphics APIs often expect uint32_t
or unsigned int
for things like viewport size, framebuffer dimensions etc.. , so this is just another way to stay even more consistent with the code we write.
void glfwErrorCallback(int error, const char* description)
{
std::cerr << "[GLFW ERROR] (" << error << "): " << description << std::endl;
}
This function defines a GLFW error callback — a handler GLFW calls whenever it encounters an error.
It takes two parameters: an int
error code and a descriptive const char*
description.
Inside, it prints the error code and description to the standard error stream (std::cerr
) with a clear “[GLFW ERROR]” prefix. This helps you catch and log GLFW-specific errors in real time, making debugging much, much easier.
To use it, you register this function with GLFW via:
glfwSetErrorCallback(glfwErrorCallback);
But we’ll do this right after we initialize glfw correctly.
// Initialize GLFW library
if(!glfwInit())
{
std::cerr << "[ERROR] Failed to initialize GLFW." << std::endl;
glfwTerminate(); // Cleanup GLFW before exit
std::exit(EXIT_FAILURE); // Exit program with failure code
}
// Set the error callback function to handle GLFW errors
glfwSetErrorCallback(glfwErrorCallback);
// Tell GLFW not to create an OpenGL context (for Vulkan, Metal, etc.)
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
// Create a GLFW window with given width, height and title
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "My Window", nullptr, nullptr);
// Check if window creation failed
if (!window)
{
std::cerr << "[ERROR] Failed to create GLFW window." << std::endl;
glfwTerminate(); // Cleanup GLFW before exit
std::exit(EXIT_FAILURE); // Exit program with failure code
}
// Get actual framebuffer size (important for high-DPI displays)
int width, height;
glfwGetFramebufferSize(window, &width, &height);
So first things first, we need to call glfwInit()
to initialize GLFW itself - if this fails, we log an error, clean up by calling glfwTerminate()
and exit the program with failure;
right after this we set the GLFW error glfwErrorCallback
to catch and log detailed GLFW errors automatically. Then, it calls
It configures GLFW with glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API)
, telling GLFW not to create an OpenGL context since we’ll be using Metal.
Next, we create the GLFW window with our specified width
, height
, and title
.
I’ve also added glfwGetFramebufferSize
to make it so the width and height of the framebuffer are the same all across our application - whether it’d be the window’s resolution, the metal layer size or the framebuffer’s size - this way everything is the same.
/ Main event loop: run until the window is closed
while (!glfwWindowShouldClose(window))
{
glfwPollEvents(); // Process pending window events
// clear the screen
// render
}
This is our main loop.
It’s where our rendering logic will go, once we’ve finished setting up all of our data and we are ready to render it onto the screen.
// Cleanup and close the window
glfwDestroyWindow(window);
glfwTerminate(); // Terminate GLFW
return 0;
}
We then finish by exiting the main loop (currently stopped when we hit the window’s “x” button), destroy the GlfwWindow
and terminate GLFW alltoghether.
Build And Run
Build (Cmd+B)
and Run (Cmd+R)
the project. If everything is set up correctly, you should see a window pop up ready to display some nice graphics!

Download The Project Files
If you encounter issues with the code or simply want to test with the correct setup, you can download the latest project files below: