Devlog

Connecting a Game to Native iOS Services

Making Games with AI · Part 13

Making Games with AI, part 13: Connecting a Game to Native iOS Services. Historical Kibble Street TD screenshots and development diagrams.

This article revisits development in June–July 2026 and early August, using records through August 3, 2026. References to “current” behavior, values, images, candidates, and validation describe that historical snapshot, not today's released game.

Previous: Adding and Removing Features Both Matter

When Kibble Street TD consisted of battles, upgrades, and saved progress, most development stayed inside one game logic layer. Preparing an iOS submission changed that. Leaderboards needed Game Center, products and transactions came from StoreKit, ads depended on AdMob and consent status, and analytics events went to Firebase.

Platforms and third-party services provide those capabilities, but players trigger them from game screens. A leaderboard submission can cross game scripts, a native bridge, system authentication, a network request, and a UI callback. If any layer fails without explaining why, the player sees an unresponsive button.

The problem was therefore larger than calling another SDK. Platform services needed a route into the game that kept gameplay rules, platform state, and asynchronous callbacks understandable.

Define responsibilities before building the bridge

The project divides this boundary into three layers. Game scripts own screens, gameplay state, and business decisions. Objective-C++ receives messages between the game runtime and iOS and routes them to the appropriate service. Swift and system frameworks handle modern Apple APIs, asynchronous tasks, and facts returned by the platform.

Responsibilities of game scripts, native routing, and iOS services
Responsibilities of game scripts, native routing, and iOS services

The native layer does not reimplement the game. It does not calculate battle damage, own the inventory, or decide how many resources a purchase grants. Game scripts, in turn, do not guess whether a system account is authenticated, a transaction is unfinished, or a system screen can be presented.

Each side exchanges only the data needed for the action. A leaderboard submission carries a leaderboard identifier, progress, and a request ID. The native service returns success, cancellation, or a specific error. The platform reports what happened; the game decides how to explain it, whether to retry, and how to continue.

One bridge entry, four independent channels

An easy early approach is to let every native module install its own global callback. Once ads, analytics, leaderboards, and purchases coexist, a later registration can overwrite an earlier one. The resulting failures may vary with initialization order.

This implementation installs one receiver in the game and one main router on iOS. Each message is dispatched by channel to StoreKit, Game Center, AdMob, or Firebase. A service subscribes only to its own channel.

One bridge entry routes messages to four native services
One bridge entry routes messages to four native services

This concentrates conflicts in one inspectable place. Unknown channels, unparseable messages, and callbacks without handlers produce explicit errors. A new capability joins the same router with its own protocol, instead of competing for the global entry point.

A native call is not an ordinary function return

Local calculations often return immediately. Authentication, product loading, and purchases do not. A player can cancel, a network can time out, a system screen can appear later, and the app can close before a transaction completes.

Calls requiring a result carry a unique request ID and a command. The game records the pending request and starts a timeout. The native service returns an event, success status, data or error, and the same ID. Completion requires both the ID and the expected event to match.

Request IDs connect asynchronous responses to pending callers
Request IDs connect asynchronous responses to pending callers

This distinguishes concurrent requests of the same kind and exposes late, duplicate, or unassigned responses. A timeout gives the screen an explainable failure it can retry.

Different capabilities also need different protocols. Leaderboards and purchases use request–response. Rewarded ads emit lifecycle events for loading, showing, closing, and earning a reward. Analytics uses a side channel whose failure must not block a battle, settlement, or purchase. Treating all three as the same function would obscure when an action actually ends.

Follow one leaderboard submission there and back

The leaderboard service creates a request ID and sends the screen's request through the shared bridge. The iOS router recognizes the Game Center channel and passes the data to its service. After authentication or submission, the native service encodes a response and returns it through that channel.

The game finds the waiting request, checks the event type, clears its timeout, and returns the result to the original caller. It does not broadcast the response indiscriminately. Native presentation of authentication or leaderboard screens must also move onto the main thread.

Each step leaves a useful debugging question: did the screen initiate the action, was the request registered, did the channel match, did the system call back, and did the response find its request? That is more actionable than “the leaderboard sometimes does nothing.”

Why Objective-C++ and Swift coexist

The two languages serve different boundaries. Objective-C++ sits close to the game runtime's native interface and is well suited to receiving bridge messages, calling iOS objects, and routing requests. Modern APIs such as StoreKit 2 and app transaction environments are more direct in Swift, with asynchronous tasks, typed results, and main-thread constraints.

The StoreKit channel therefore receives requests in Objective-C++, passes them to a Swift service, and gets the result through a narrow callback.

Objective-C++ and Swift communicate through a narrow interface
Objective-C++ and Swift communicate through a narrow interface

The interface deliberately stays stable. Swift exposes an explicit native name, while Objective-C++ declares only the methods it needs. It does not depend on an automatically generated header whose name changes with the project. Bridge files also need the correct memory-management compilation mode; otherwise authentication callbacks and data objects can crash at runtime.

The choice follows each language's role at the boundary, rather than its age.

A capability must enter the build system

Having bridge source files is not proof that an installable app contains a working capability. The files must belong to the iOS target, Swift compilation must be enabled, system frameworks must be linked, third-party dependencies must be installed, and permissions, privacy declarations, and service configuration must reach the correct locations in the package.

Native services pass through source, configuration, dependencies, and archiving
Native services pass through source, configuration, dependencies, and archiving

An earlier baseline successfully produced a Release archive and exported an IPA. That proved the game project, native code, Swift, frameworks, and dependencies could build together. It preceded the latest analytics protocol changes in this historical snapshot, however, so it could not certify the revised source. A fresh archive and device regression were still required.

Historical build success establishes a baseline. After code changes, it does not establish that the new version has passed.

Separate four layers of verification

At this snapshot, targeted static checks for leaderboards, analytics, purchases, and ads passed. They checked channel names, request protocols, timeouts, recovery, and error branches. The earlier Release archive and IPA export had also passed.

Static checks do not replace compilation, old archives do not replace new ones, and successful archives do not replace platform acceptance. Game Center still required two real accounts to test ranking boundaries and account switching. StoreKit needed Sandbox or TestFlight transactions and recovery after process interruption. Ads needed device consent and presentation tests. Analytics needed confirmation that events reached the real backend.

Four distinct layers of native-service verification
Four distinct layers of native-service verification

A code contract, a generated package, correct device behavior, and a received platform result are four separate conclusions. Recording them separately makes “complete” precise.

What the human and AI each contribute

AI can trace interfaces, channels, request IDs, timeouts, and error branches. Comparing source, build settings, and checks helps it find a file missing from a build target or a callback with no subscriber. After a protocol change, it can identify neighboring modules that need verification again.

The human decides which facts belong to the platform, which rules must stay in the game, how cancellation should affect the player, and what evidence supports a release claim. Real-device sign-in, purchase confirmation, system authorization, and backend configuration still require the appropriate real environment and account.

If you want to start

Draw the shortest call chain first: who initiates it, what it sends, who handles it, which results can return, how long it can wait, and who explains failure. Build one channel and one minimal request that can visibly fail.

Once a complete round trip is traceable in logs, add the next capability. Check that it shares the entry point, owns its channel, returns platform facts, and leaves gameplay rules in the game.