Last week, we built the flat C bridge. This week, we cross it. On macOS, the modern way to build a UI is SwiftUI, and the language is Swift. But Swift doesn't talk directly to C++ headers. It talks to Objective-C. To close the gap, we use a dialect called Objective-C++ (files ending in .mm). It’s a bilingual file that understands both C++ classes and Objective-C objects, making it the perfect glue layer.

The Wrapper Class

We create an Objective-C class that holds our C++ engine as a member variable. Because Objective-C is a superset of C, and Objective-C++ can include C++ headers, this class acts as a translator. It takes Swift calls, converts the data to C++, and passes it to the engine.

// ChessEngineWrapper.mm
#import "ChessEngineWrapper.h"
#include "chess_engine.hpp"

@implementation ChessEngineWrapper {
    chess::Engine *_engine;
}

- (instancetype)init {
    if (self = [super init]) {
        _engine = new chess::Engine();
    }
    return self;
}

- (void)dealloc {
    delete _engine;
}

- (NSString *)getBestMoveForFen:(NSString *)fen {
    std::string cppFen = [fen UTF8String];
    std::string move = _engine->findBestMove(cppFen);
    return [NSString stringWithUTF8String:move.c_str()];
}
@end

The Bridging Header

Swift needs to know about this Objective-C class. We create a ProjectName-Bridging-Header.h and #import "ChessEngineWrapper.h". Once that’s done, the ChessEngineWrapper class is instantly available in every Swift file in the project as if it were written in Swift.

Objective-C++ is the secret sauce of the Mac. It's how every high-performance app — from Photoshop to Final Cut — connects its heavy-lifting C++ core to a smooth system UI.

SwiftUI Integration

Now we can use our engine inside a standard SwiftUI view. We wrap the engine in an ObservableObject to handle the state, ensuring that when the AI finds a move, the UI updates automatically. Note the threading: the search runs on a background queue so the 60 fps UI never stalls while the engine ponders.

class GameViewModel: ObservableObject {
    private let engine = ChessEngineWrapper()
    @Published var fen: String = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"

    func makeAIMove() {
        DispatchQueue.global(qos: .userInitiated).async {
            let bestMove = self.engine.getBestMoveForFen(self.fen)
            DispatchQueue.main.async {
                // Update UI with the new move
            }
        }
    }
}

The modern alternative: direct Swift ↔ C++ interop

As of Swift 5.9 (Xcode 15), Swift can import C++ headers directly, no Objective-C++ wrapper required. You add SWIFT_OBJC_INTEROP_MODE = "objcxx" to a target's build settings, drop the C++ header into your module, and call chess::Engine from Swift as if it were a Swift type. Classes, methods, even some templates round-trip cleanly.

This is the future, and for new projects it's the right call. The Objective-C++ approach above still matters because: most existing codebases are built on it; bigger C++ surfaces (heavy use of templates, exceptions, custom move semantics) still don't survive the direct route; and the bridging pattern is what you'll see when you read other people's code. Either way, the underlying bridge header from Week 44 stays the same — what changes is how thick the wrapper layer needs to be.

Ownership across the boundary

Two memory systems meet inside ChessEngineWrapper: Objective-C ARC (automatic reference counting) and C++ new/delete. ARC manages the wrapper object. The wrapper manually news the C++ engine in init and deletes it in dealloc. Don't hand a unique_ptr to ARC and expect it to know what to do, and don't retain the C++ pointer from Swift — the Swift side only ever sees the wrapper. Each system owns its own side of the fence; they meet only at the wrapper class.

Try it yourself

Why this matters

Every serious Mac app you use has this shape. Logic Pro, Photoshop, Xcode itself: a C++ (or Rust) core for the heavy lifting, a Swift/Objective-C shell for the UI, and a small wrapper layer at the seam. SwiftUI's productivity goes up when the engine underneath is C++, because Swift handles the UI in a few hundred lines while the engine does the work that needs raw performance. You're now writing apps the way Apple does.

What's next

macOS is only one side of the coin. Next week, we head to Windows. We'll look at how to take that same C++ engine and bridge it to the Windows App SDK using C# and P/Invoke.

Week 46 is Windows Native.

Quick check

1. What file extension do Objective-C++ files use?
  1. .cpp
  2. .mm
  3. .swift
Reveal Answer

Answer: B. .m is Objective-C; .mm is Objective-C++ — bilingual, can include both Objective-C interfaces and C++ headers.

2. What changed for C++ interop in Swift 5.9 (Xcode 15)?
  1. Swift gained the ability to import C++ headers directly, no Objective-C++ wrapper required
  2. C++ was removed from macOS
  3. Swift now compiles to Objective-C++
Reveal Answer

Answer: A. Direct interop is the future for new projects. Existing codebases still use the Objective-C++ wrapper pattern.

3. What does ARC (Automatic Reference Counting) manage?
  1. C++ objects automatically
  2. Objective-C and Swift object lifetimes — but not C++'s new/delete
  3. The compile cache
Reveal Answer

Answer: B. ARC handles the Apple-language side. C++ objects must be manually new/delete'd or wrapped in unique_ptr.