Add support for iOS joystick by JasonLeon01 · Pull Request #3750 · SFML/SFML · GitHub
Skip to content

Add support for iOS joystick - #3750

Open
JasonLeon01 wants to merge 5 commits into
SFML:masterfrom
JasonLeon01:iOS-Joystick
Open

JasonLeon01 wants to merge 5 commits into
SFML:masterfrom
JasonLeon01:iOS-Joystick

Conversation

@JasonLeon01

Copy link
Copy Markdown

Description

This PR implements the previously unimplemented iOS joystick backend using Apple's GameController framework. Controllers exposing an extended gamepad profile can now be queried through sf::Joystick and generate the existing joystick events.

The implementation:

This PR implements the previously unimplemented iOS joystick backend using Apple's GameController framework for controllers exposing GCExtendedGamepad.

It tracks controllers through connection notifications and enumeration, rejects input from stale connections, and clears input while the application is inactive. Optional Options, Home, and Xbox Share buttons are read when available, subject to system gesture handling.

The iOS build now links GameController and compiles JoystickImpl.mm with ARC.

Tasks

  • Tested on Linux
  • Tested on Windows
  • Tested on macOS
  • Tested on iOS
  • Tested on Android

How to test this PR?

Build SFML for iOS and run the following example in an iOS application linked against SFML::Graphics and SFML::Main. Observe the event output in the Xcode debug console. The window turns green while button A on joystick 0 is held, exercising the real-time polling API as well as the event API.

The existing joystick example is currently excluded from iOS builds, so enabling SFML_BUILD_EXAMPLES alone does not provide an iOS joystick test application.

  1. Launch with an extended-gamepad controller already connected, then repeat by connecting it after launch. Verify detection, a readable controller name, and the reported button count in both cases, plus a connection event when connecting after launch.
  2. Move both sticks, fully press and release both triggers, and use the D-pad. Verify the corresponding axis events and return to neutral when released.
  3. Press and release each available button. Verify the reported indices, including optional buttons on controllers and OS versions that expose them.
  4. Connect two controllers and disconnect one. The remaining controller should retain its joystick index. Reconnect or replace the disconnected controller and verify connection events and fresh input without stale pressed buttons or axis values.
  5. Hold a button or move a stick, make the app inactive, release the input, and return to the app. Verify there is no stuck input. When inspecting joystick state during an inactive update, all axes should be zero and all buttons released.
#include <SFML/Graphics.hpp>
#include <SFML/Main.hpp>

#include <iostream>
#include <optional>

void printController(unsigned int id)
{
    std::cout << "Connected " << id << ": "
              << sf::Joystick::getIdentification(id).name.toAnsiString()
              << ", buttons: " << sf::Joystick::getButtonCount(id) << '\n';
}

int main()
{
    sf::RenderWindow window(sf::VideoMode({1280, 720}), "iOS joystick test");
    window.setFramerateLimit(60);

    for (unsigned int id = 0; id < sf::Joystick::Count; ++id)
    {
        if (sf::Joystick::isConnected(id))
            printController(id);
    }

    while (window.isOpen())
    {
        while (const std::optional event = window.pollEvent())
        {
            if (event->is<sf::Event::Closed>())
                window.close();
            else if (const auto* connected = event->getIf<sf::Event::JoystickConnected>())
                printController(connected->joystickId);
            else if (const auto* disconnected = event->getIf<sf::Event::JoystickDisconnected>())
                std::cout << "Disconnected " << disconnected->joystickId << '\n';
            else if (const auto* pressed = event->getIf<sf::Event::JoystickButtonPressed>())
                std::cout << "Joystick " << pressed->joystickId << ", button "
                          << pressed->button << " pressed\n";
            else if (const auto* released = event->getIf<sf::Event::JoystickButtonReleased>())
                std::cout << "Joystick " << released->joystickId << ", button "
                          << released->button << " released\n";
            else if (const auto* moved = event->getIf<sf::Event::JoystickMoved>())
                std::cout << "Joystick " << moved->joystickId << ", axis "
                          << static_cast<int>(moved->axis) << ": " << moved->position << '\n';
        }

        const bool pressed = sf::Joystick::isConnected(0) && sf::Joystick::isButtonPressed(0, 0);
        window.clear(pressed ? sf::Color::Green : sf::Color::Black);
        window.display();
    }
}

@JasonLeon01

Copy link
Copy Markdown
Author

@JonnyPtn

Copy link
Copy Markdown
Contributor

I'm struggling to test this as I've updated to latest xcode/iOS which SFML doesn't support yet, but on first glance one thing that would certainly be useful is to enable the joystick example for iOS now - that requirres updating examples/CMakeLists.txt and adding the font resource to the cmake target, including SFML/Main.hpp and adding the resourcesDir workaround the other examples have (at least until #3688 is merged)

@JonnyPtn JonnyPtn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing with a dualshock controller and mostly seems fine, a few comments added on the code.

Is all the lock stuff necessary? as I understand it all of this should be running on the main thread so there should be no race conditions, and would be a lot tidier without it.

I also noticed many of the buttons trigger system behaviour (game center, screen recording, etc.) which we could disable with preferredSystemGestureState - I'd say that makes most sense as a default, as if users want to support these platform specific setttings then can build on top of SFML (whereas with it enabled, they can't use these buttons for their own purposes)

Getting the joystick example running locally I noticed some issues with the button mappings too, but as this is kind of part of a wider problem with SFML's (lack of) consistent controller button mapping I think it's fine to address later

Comment thread src/SFML/Window/iOS/JoystickImpl.mm Outdated
Comment thread src/SFML/Window/iOS/JoystickImpl.mm Outdated
@JasonLeon01

Copy link
Copy Markdown
Author

Testing with a dualshock controller and mostly seems fine, a few comments added on the code.

Is all the lock stuff necessary? as I understand it all of this should be running on the main thread so there should be no race conditions, and would be a lot tidier without it.

I also noticed many of the buttons trigger system behaviour (game center, screen recording, etc.) which we could disable with preferredSystemGestureState - I'd say that makes most sense as a default, as if users want to support these platform specific setttings then can build on top of SFML (whereas with it enabled, they can't use these buttons for their own purposes)

Getting the joystick example running locally I noticed some issues with the button mappings too, but as this is kind of part of a wider problem with SFML's (lack of) consistent controller button mapping I think it's fine to address later

Thanks for testing this! I’ll enable the joystick example on iOS, replace the per-update registry refreshes with initial enumeration and connection/disconnection notifications, and remove the unnecessary autorelease pool in open().

I’ll also request disabling system gestures for the exposed buttons where supported. The button-mapping cleanup can remain a separate follow-up, as you suggested.

About locking, the notifications run on the main thread, but sf::Joystick::update() is also publicly callable. Is the intended contract that joystick updates on iOS must run on the main thread?

@JonnyPtn

Copy link
Copy Markdown
Contributor

Yeah I think various parts of this would fall apart if we didn't do joystick updating on the main thread so reasonable to assume it would be (but worth testing, I'd expect any issues to be quickly apparent)

@JasonLeon01

Copy link
Copy Markdown
Author

Yeah I think various parts of this would fall apart if we didn't do joystick updating on the main thread so reasonable to assume it would be (but worth testing, I'd expect any issues to be quickly apparent)

Hi, I've removed the locking, per-frame enumeration and unnecessary autorelease pools, added main-thread assertions, and enabled the joystick example on iOS.

Generation checks remain to reject stale connections. I also fixed stale input after backgrounding and verified the backend on an iPhone with an Xbox controller.

@JonnyPtn JonnyPtn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need the asserts, we can expect the notifications to come on the main thread anyway, and the system will already warn if a user does try to do something on a thread they shouldn't.

I also don't think the generation counter is needed, we should be able to store the controller pointer in the slot in line with other platforms (and the equivalent implementation in your other PR for mac)

Comment thread src/SFML/Window/iOS/JoystickImpl.mm Outdated
Comment thread src/SFML/Window/iOS/JoystickImpl.mm Outdated
@JasonLeon01

Copy link
Copy Markdown
Author

@JasonLeon01
JasonLeon01 requested a review from JonnyPtn October 15, 2026 05:18
Comment thread src/SFML/Window/iOS/JoystickImpl.mm Outdated
Comment thread src/SFML/Window/iOS/JoystickRegistryImpl.hpp Outdated
@JasonLeon01
JasonLeon01 requested a review from JonnyPtn October 15, 2026 09:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants