This guide exists, because I wanted to port my Porn Fetch app to iOS. I have no idea about Apple's eccosystem and I do not own a real mac, nor a modern iPhone. I can not assist you if anything breaks. This guide is intended to help you, not to replace Qt's own guidance!
Caution
This guide is heavily AI assisted, but verified by a human (me lol) on real hardware. I have successfully ported my Porn Fetch project to iOS. In this guide I have published the concrete steps on how I and GPT6-Astra got that working.
- Arch Linux
- Virtualized macOS from: https://github.com/notAperson535/OneClick-macOS-Simple-KVM
- XCode 26.3
- iPhone 7 (Jailbroken) with Palera1n on iOS 15.8.8
- iPhone 7 (not jailbroken) on iOS 15.8.8 too
Note
This guide is unofficial and not affiliated with Qt. It mirrors the structure of PySide6-to-Android, adapted from our iOS build work. Authoritative references:
Important
Read the guide end-to-end. iOS is not Android with a different filename: device and simulator binaries differ, Qt/PySide are linked statically, native Python extensions need framework packaging, and a stock phone requires Apple signing/provisioning.
| Component | Reference build | Meaning |
|---|---|---|
| Qt / PySide6 / shiboken6 | 6.12.0, matching versions | Reference iOS deployment toolchain |
| Host | Intel macOS 15.7.2 VM | Apple Silicon needs a different simulator target |
| Xcode | 26.3, iPhoneOS/Simulator 26.2 SDKs | Full Xcode, not just Command Line Tools |
| Host Python | 3.13.16 | Runs tools; does not become the embedded interpreter |
| Embedded Python | 3.15.0rc2, cp315-cp315 |
Pinned reference; not Android's cp310-abi3 wheels |
| Official Qt baseline | iOS 18+ | Supported platforms |
| Experimental legacy target | iOS 15.0, ARM64 | Custom source build/patch; iPhone 7, iOS 15.8.8, palera1n rootless |
Python 3.15.0rc2 is the exact embedded release used here, not advice to replace a newer host Python. Changing it requires matching wheels and native extensions.
Verified: Intel simulator launch; physical Python + Qt; Material/QML rendering and touch; CFFI callbacks; chompjs; selectolax; curl-cffi sync/async HTTPS; cryptography; a real application's download → in-process FFmpeg remux → video/audio playback workflow on the jailbroken iPhone 7.
Prepared, not runtime-verified: the PornFetch stock-iOS IPA for iLoader/SideStore. Its layout, nested signatures and archive integrity pass checks. Stock installation, rendering and downloads still need testing. Apple Silicon simulator and clean rebuilds of all generalized helpers are not claimed verified.
31 October update: the fresh full-matrix test IPA imports all 52 bindings and has a working tap counter on the non-jailbroken iPhone 7 / iOS 15.8.8, as confirmed by the user. That result applies to the matrix test app; the separate PornFetch stock-iOS IPA still needs its own runtime test.
- Overview
- Get Prebuilt iOS Wheels
- Environment Setup (Xcode/SDK)
- Build the App / IPA
- Common Errors & Fixes
- Debugging Strategy (Highly Recommended)
- Building Qt and the Wheels Yourself
- Contributing
- Support
An iOS app embeds Python. pyside6-ios-deploy generates an Xcode project, not a finished IPA.
Xcode builds a native launcher, links the selected Qt/PySide/plugins, and bundles the runtime and code.
| Wheel platform | CPU | Destination | Reference status |
|---|---|---|---|
ios_arm64 |
ARM64 | Physical iPhone/iPad | Custom iOS 15 build verified on jailbroken phone |
ios_arm64_simulator |
ARM64 | Apple Silicon simulator | Documented, not exercised on this Intel host |
ios_x86_64_simulator |
x86_64 | Intel simulator | Prebuilt-wheel path verified |
An ARM64 simulator wheel is not an ARM64 device wheel. The CPU matches; the platform/ABI does not. Keep target Qt prefixes, unpacked wheel caches and build directories separate.
For the experimental iOS 15 matrix, use the 6.12.0 iOS 15 release. It supplies matching wheel pairs and static Qt SDKs for devices and both simulator CPUs, plus the Intel host kit, Python runtime, corresponding source, patches and build evidence. See the full matrix guide for module scope and the deployment-tool patch, and the release results for per-target validation.
From this repository, download just your wheel pair and verification files:
export GUIDE_ROOT="$PWD"
export WORK_ROOT="$GUIDE_ROOT/work"
export TARGET=ios_x86_64_simulator
mkdir -p "$WORK_ROOT/wheels/$TARGET"
gh release download 6.12.0-ios15-cp315 --repo EchterAlsFake/PySide6-to-iOS \
--dir "$WORK_ROOT/wheels/$TARGET" \
--pattern "*-$TARGET.whl" --pattern SHA256SUMS --pattern SHA256SUMS.asc
python3.13 scripts/verify-release-assets.py "$WORK_ROOT/wheels/$TARGET" --target "$TARGET"
python3.13 scripts/check-ios-wheels.py "$WORK_ROOT/wheels/$TARGET" --target "$TARGET" --max-ios 15.0The checksum file is GPG-signed with fingerprint
7082BBFF855D47C6BB13C3BF6AC58B8CA26D1C5F. With that public key in your
keyring, run gpg --verify SHA256SUMS.asc SHA256SUMS in the download directory.
Checksums alone verify file integrity; verify the signature to check its signer.
Get matching iOS PySide6/shiboken6 wheels. Desktop wheels installed with pip supply host tools.
Do not install target wheels into the host environment.
Public archive: https://download.qt.io/official_releases/QtForPython/.
The 6.12.0 / Python 3.15 pair uses:
pyside6-6.12.0-6.12.0-cp315-cp315-ios_arm64.whl
shiboken6-6.12.0-6.12.0-cp315-cp315-ios_arm64.whl
pyside6-6.12.0-6.12.0-cp315-cp315-ios_arm64_simulator.whl
shiboken6-6.12.0-6.12.0-cp315-cp315-ios_arm64_simulator.whl
pyside6-6.12.0-6.12.0-cp315-cp315-ios_x86_64_simulator.whl
shiboken6-6.12.0-6.12.0-cp315-cp315-ios_x86_64_simulator.whl
Download a pair and verify its published checksums:
export GUIDE_ROOT="$PWD" # run from this repository
export WORK_ROOT="$GUIDE_ROOT/work"
export TARGET=ios_x86_64_simulator # Intel; select the appropriate row above
mkdir -p "$WORK_ROOT/wheels/$TARGET"
cd "$WORK_ROOT/wheels/$TARGET"
for package in pyside6 shiboken6; do
wheel="$package-6.12.0-6.12.0-cp315-cp315-$TARGET.whl"
url="https://download.qt.io/official_releases/QtForPython/$package/$wheel"
curl -fL --retry 3 "$url" -o "$wheel"
curl -fL --retry 3 "$url.sha256" -o "$wheel.sha256"
shasum -a 256 -c "$wheel.sha256"
done
cd "$GUIDE_ROOT"
python3.13 scripts/check-ios-wheels.py "$WORK_ROOT/wheels/$TARGET" --target "$TARGET"The upstream qtpip download PySide6 --ios --arch arm64 --platform device route is another option.
Here, qtpip 0.2.0 requested commercial credentials and direct public files worked. That observation
does not establish access to every Qt package or future download.
Official upstream wheels keep Qt's newer iOS baseline. For iPhone 7/iOS 15, use this repository's experimental release or the custom source-build path.
Install full Xcode and open it once to finish initial setup. With Homebrew already available:
brew install cmake ninja git python@3.13 pkgconf autoconf automake libtool gh gnupg zstdSelect Xcode for this terminal, without changing the whole machine:
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
# If kept in Downloads:
# export DEVELOPER_DIR="$HOME/Downloads/Xcode.app/Contents/Developer"
xcodebuild -version
xcrun --sdk iphoneos --show-sdk-path
xcrun --sdk iphonesimulator --show-sdk-pathKeep the deployment virtualenv outside the app's own folder:
python3.13 -m venv "$WORK_ROOT/venvs/deploy"
source "$WORK_ROOT/venvs/deploy/bin/activate"
python -m pip install 'PySide6-Essentials==6.12.0'
requirements=$(python -c 'import pathlib,PySide6; print(pathlib.Path(PySide6.__file__).parent / "scripts/requirements-ios.txt")')
python -m pip install -r "$requirements"
pyside6-ios-deploy --helpOn Intel, install an Intel-capable simulator runtime. This can be a large download:
xcodebuild -downloadPlatform iOS -architectureVariant universal
xcrun simctl list runtimes
xcrun simctl list devices availableThe reference used 16 GB RAM and later 8 virtual CPUs. Extra RAM was unnecessary; disk space mattered more. Keep tens of GB free for SDKs, Qt objects and intermediates. The retained lab was about 21 GB, excluding Xcode and simulator data. Build as your normal graphical-login user, not root.
There is no buildozer.spec for this route. Useful [ios] options in pysidedeploy.spec are:
wheel_pyside,wheel_shiboken: matching files; their platform tags choose the destination.xcframework_path: official Python runtime or the tool's automatically populated cache.bundle_id: your unique reverse-DNS identifier.team_id,signing_style: your Apple team and signing configuration.deployment_target: normally leave empty for Qt's baseline; lower only with a compatible rebuilt kit.entitlements: optional plist; stock provisioning must authorize requested entitlements.
[qt] modules and qml_files control which bindings/resources are linked and registered. Inspect
generated main.mm when an import or QML plugin is missing: static registration happens before Python starts.
Use upstream --init; an interactive pause patched into the deployment script is unnecessary:
mkdir -p "$WORK_ROOT/apps/hello"
cp "$GUIDE_ROOT/scripts/examples/hello/"* "$WORK_ROOT/apps/hello/"
cd "$WORK_ROOT/apps/hello"
pyside6-ios-deploy main.py --init --name HelloIOS \
--bundle-id com.example.HelloIOS \
--wheel-pyside "$WORK_ROOT/wheels/$TARGET/pyside6-6.12.0-6.12.0-cp315-cp315-$TARGET.whl" \
--wheel-shiboken "$WORK_ROOT/wheels/$TARGET/shiboken6-6.12.0-6.12.0-cp315-cp315-$TARGET.whl"
# Review pysidedeploy.spec; add your own [ios] team_id for device signing.
pyside6-ios-deploy -c pysidedeploy.spec--init still validates/unpacks wheels and may download Python. Later, pass -c explicitly to
reuse config without repeating wheel arguments. It is not an offline no-op.
Open deployment/$TARGET/HelloIOS.xcodeproj in Xcode. Choose a destination matching your wheels.
For an Intel simulator, a command-line build is:
xcodebuild -project "deployment/$TARGET/HelloIOS.xcodeproj" \
-scheme HelloIOS -configuration Debug -sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' -derivedDataPath "$WORK_ROOT/derived/hello-$TARGET" \
ARCHS=x86_64 ONLY_ACTIVE_ARCH=YES CODE_SIGNING_ALLOWED=NO \
CODE_SIGN_IDENTITY=- EXPANDED_CODE_SIGN_IDENTITY=- \
EXPANDED_CODE_SIGN_IDENTITY_NAME=ad-hoc buildFor a stock device, configure Signing & Capabilities with your own team and unique ID. Use Xcode's archive/export workflow for a provisioned distribution IPA. No Apple signing identity was available in this VM; its experimental stock IPA instead needs iLoader/SideStore to re-sign it. GPG signing a Git commit does not sign an iOS app.
The simulator command supplies - explicitly because CPython's build phase signs extension
frameworks itself even when Xcode's own signing is disabled. An empty expanded identity caused
“The specified item could not be found in the keychain.” These ad-hoc overrides are for the
unsigned simulator/re-signing workflow, not a replacement for your stock-device identity.
To package a built device .app for that re-signing workflow:
python3.13 "$GUIDE_ROOT/scripts/package-ipa.py" \
--app /absolute/path/to/HelloIOS.app \
--output "$WORK_ROOT/HelloIOS-sideload.ipa" \
--bundle-id com.example.HelloIOS.Sideload --display-name 'Hello iOS Test'The helper copies the app, normalizes framework metadata, removes build-only objects, signs
frameworks before the main app, and verifies the ZIP. Its empty entitlements and ad-hoc signatures
still require Apple Account re-signing/provisioning. It cannot turn simulator code into device code.
It removes the copied Qt build-kit packages/PySide6/Qt/lib tree while retaining QML resources;
runtime dylibs must already be embedded in the app's Frameworks directory. Additional module
stacks such as Qt Multimedia need their own dependency collection and are not a verified matrix here.
Simulator:
xcrun simctl list devices available
export SIMULATOR_ID=YOUR_SIMULATOR_UUID
xcrun simctl boot "$SIMULATOR_ID" # omit if already booted
open "$DEVELOPER_DIR/Applications/Simulator.app"
xcrun simctl bootstatus "$SIMULATOR_ID" -b
xcrun simctl install "$SIMULATOR_ID" /absolute/path/to/HelloIOS.app
xcrun simctl launch --console "$SIMULATOR_ID" com.example.HelloIOSUse the ID in built Info.plist, not the process/display name. These overrides made Qt Quick render
through OpenGL in the no-GPU Intel VM:
SIMCTL_CHILD_QT_QUICK_BACKEND=rhi SIMCTL_CHILD_QSG_RHI_BACKEND=opengl \
SIMCTL_CHILD_QSG_RENDER_LOOP=basic \
xcrun simctl launch --console "$SIMULATOR_ID" com.example.HelloIOSThose are VM simulator overrides. Do not force them onto the phone; the verified phone uses Metal.
Stock phone: use Xcode, or import the device IPA into iLoader or SideStore and let it re-sign with your account. Connect/trust the host, then trust the developer under Settings → General → VPN & Device Management if asked. iOS 15 has no iOS 16 Developer Mode switch. Stock installation does not use SSH.
Jailbreak experiment: we used palera1n rootless, an SSH key, ldid, /var/jb/Applications,
uicache and uiopen. This is separate from normal iOS deployment, not a PySide6 requirement.
Rootless may provide /var/jb/usr/bin/sh instead of /bin/sh.
See current build notes.
| Symptom | What to check |
|---|---|
| iOS SDK not found | Full Xcode and DEVELOPER_DIR, not only CommandLineTools |
pip rejects the target wheel |
Expected; feed it to pyside6-ios-deploy |
| ARM64 code cannot run on device | Device vs simulator platform: inspect LC_BUILD_VERSION, not only CPU |
| Generic simulator build tries ARM64 with Intel wheels | Pass ARCHS=x86_64 ONLY_ACTIVE_ARCH=YES; Apple Silicon uses ARCHS=arm64 |
| Minimum iOS rejected | Rebuild compatible Qt/PySide or use supported iOS; plist edits do not replace new APIs |
| CMake cannot find target Python | Supply the include/python3.15 and lib/libpython3.15.dylib compatibility view |
| Quick wrappers cannot find OpenGL headers | Build Core,Gui,Network,OpenGL,Svg,Qml,Quick,QuickControls2 in that order |
CFFI callback crashes in ffi_prep_closure_loc |
Match CFFI Apple trampoline-table defines to target libffi |
| Missing PySide/QML module | Check static registration/plugins; regenerate after changing module selection |
Missing certifi/data/.dist-info |
Preserve package trees and metadata; .py-only copying is insufficient |
| Read-only bundle writes | Use Documents/Application Support/Cache in the data container |
subprocess unavailable |
Expected iOS restriction; use an in-process library such as the FFmpeg bridge |
.mp4 will not play |
Check container format; MPEG-TS renamed .mp4 still needs remuxing |
| Black screen with live process | Check scene/window exposure and Metal; offscreen painting is not screen presentation |
| IPA entitlement/signature error | Re-sign/provision, remove private jailbreak entitlements and verify every framework |
| Python copy phase reports a missing keychain item | Supply the explicit ad-hoc expanded identity in the simulator command above |
Framework labeled APPL |
Normalize to FMWK before signing |
codesign prints [Dict] |
Add --xml when parsing its entitlement output with plistlib |
Prove one layer at a time: UIKit → embedded Python → Qt window → QML/touch → native dependencies → full app. Write a local startup log before depending on external networking:
from pathlib import Path
import os, sys, traceback
working = Path.home() / 'Documents' / 'MyApp'
working.mkdir(parents=True, exist_ok=True)
os.chdir(working)
sys.stdout = (working / 'ios-runtime.log').open('w', buffering=1)
sys.stderr = sys.stdout
from PySide6.QtCore import qInstallMessageHandler
qInstallMessageHandler(lambda kind, context, message: print(message, flush=True))
print('Python entry reached', sys.version, sys.platform, flush=True)Wrap your own entry function in exception handling and log traceback.print_exc() before re-raising.
For failures before Python starts, use Xcode's device console/crash organizer or macOS Console.
On stock iOS, do not assume SSH exists.
Our black-screen failure survived successful offscreen painting. Useful checks were
MTLCreateSystemDefaultDevice(), window/scene attachment, exposure and actual frame presentation.
The final touch counter was read back from the phone, not inferred from compilation.
For all three iOS 15 targets and the full supported Qt-for-Python module set, use the separate full release matrix workflow. The older helpers below build the minimal Quick stack used during the initial port.
See SOURCE_BUILD_6.12.md: matching host tools, static device Qt, iOS 15
compatibility changes, Python discovery, host Shiboken generator and binding order.
RUNTIME_BUILD.md covers .fwork packaging, CFFI/libffi, selectolax/chompjs,
curl-cffi, cryptography/OpenSSL and the FFmpeg extension.
SOURCE_BUILD_6.11.md retains the Android guide's historical chapter location
while explicitly recording that no PySide6 6.11 iOS build is claimed.
# Supported baseline; the first source build can take a long time.
TARGETS=ios_arm64 bash scripts/rebuild-ios-matrix.sh
# Experimental iPhone 7 path; use a different work root from an iOS 18 build.
WORK_ROOT="$PWD/work-ios15" IOS_DEPLOYMENT_TARGET=15.0 EXPERIMENTAL_IOS15=1 \
TARGETS=ios_arm64 bash scripts/rebuild-ios-matrix.shAudits check formats, platforms and deployment targets; they do not execute the app. Three iOS targets are not Android's multi-Python, four-ABI matrix.
Open an issue/PR with your host architecture, Xcode/SDK, Qt/PySide, embedded Python ABI, wheel platform, deployment target and signing route. Say whether the device is jailbroken and report build, installation and runtime success separately. Stock-device and Apple Silicon results are welcome.
A ⭐ helps others discover the guide. Detailed, dated evidence is in CURRENT_BUILD_NOTES.md.
If this saved you time, you can support the work through:
Thank you very much <3