When Xcode Moves Past Xamarin: Fixing the iOS Build

Apple requires that apps submitted to the App Store be built with a reasonably current version of Xcode and the iOS SDK, and it raises that requirement periodically. For a supported framework this is a version bump. For Xamarin, which stopped receiving compatibility work in May 2024, it eventually becomes a wall.

The difficult part is that the wall and the ordinary breakage look identical from the build log. This post is about telling them apart, because the answer determines whether you are facing an afternoon or a project.

Start by separating four failures

A stuck Xamarin.iOS build is almost always one of these, and they have different fixes:

  1. The machine. The build host was upgraded — macOS, Xcode, or both — and the toolchain no longer matches what the project expects.
  2. Signing. Certificates expired, the provisioning profile no longer covers the entitlements, or the account changed. Nothing to do with SDKs.
  3. A dependency. A binding library or native framework is not compatible with the current SDK or the current architecture rules.
  4. The framework itself. The Xamarin.iOS version your project is pinned to genuinely cannot target the required SDK.

Only the fourth is the wall. The first three are ordinary maintenance, and in our experience most "we can't build for iOS any more" calls turn out to be number one or number two.

Diagnosing the machine

Find out what the host actually has:

xcodebuild -version
xcode-select -p
ls /Applications | grep Xcode

Then find out what the project expects. Xamarin.iOS builds against whatever Xcode xcode-select points at, so a machine with two Xcode installations and a changed selection is a very common cause of a build that worked last month.

The specific failure pattern worth knowing: an error naming a Swift runtime library, a missing simulator SDK, or a linker error mentioning a symbol you have never heard of usually means toolchain mismatch, not your code.

If a CI runner started failing and nothing in the repository changed, the runner image was updated. Pin the image version, confirm the build comes back, and only then decide what to do about it. A pinned runner is a legitimate short-term answer; it is not a long-term one, because the image will eventually stop being offered.

Diagnosing signing

Signing failures are noisy and mostly self-describing, but three cases recur on apps that have not shipped in a while:

  • Expired certificates. Distribution certificates expire annually. Regenerating one is easy; finding out who has the private key is sometimes not.
  • Entitlement drift. A profile generated before a capability was added to the app will fail at signing with a message about entitlements not matching. Regenerate the profile after changing capabilities, never before.
  • Account changes. Someone left, the team was restructured, or the app moved between accounts. This is an administrative problem with an engineering-shaped error message.

Before anything else on a rescue engagement, we establish who can produce a signed build today, on what machine. The answer is frequently "nobody has tried since the last release", and that alone can be the critical path.

Diagnosing a dependency

If the error names a specific framework or a binding, the question is whether a version of that dependency exists that supports the current SDK. Native binding libraries are the usual suspect: a binding compiled against an older native SDK can fail on bitcode expectations, missing architecture slices, or simply on the vendor having stopped shipping.

Check the vendor's current release before doing anything else. If they still ship, this is a rebuild of the binding project. If they do not, it is a replace-or-remove decision and belongs in the dependency inventory rather than in the build log.

Diagnosing the wall

You have hit the actual wall when: the machine is correct and current, signing is clean, no dependency is implicated, and the failure is that the Xamarin.iOS version you are pinned to does not support the SDK the store now requires. The give-away is that the fix for every other cause has been applied and the build still cannot target the required SDK.

There is no configuration change for this. The framework is not receiving compatibility updates and will not gain support for a newer SDK.

What to do at the wall

The shortest compliant path is a .NET MAUI project that hosts the existing screens through the compatibility layer. You are not rewriting the UI at this stage — you are moving the project structure and the build onto a toolchain that can target the current SDK, which is what the store is asking for.

This is worth understanding as good news rather than bad. The work is not thrown away: the same restructuring is the first slice of the migration you were going to do anyway, and doing it under deadline pressure is unpleasant but not wasteful.

Before the next one

Two habits prevent most of this:

Build the app on a schedule, not on demand. A CI job that builds and signs a release artifact weekly turns "the build is broken" into a notification rather than a discovery during an incident.

Make certificates and profiles owned by more than one person. Every rescue engagement we run includes this, because the alternative is finding out during a deadline that the key is on a laptop that was returned.

If the app is at the wall now and there is a date attached, that is exactly what a release rescue is scoped for.