How to rescue an iOS app that no longer builds

A safe troubleshooting order for old or inherited iOS projects with Xcode, signing, dependency or Swift compiler failures.

Preserve the current state, record the exact toolchain and error, and change one variable at a time. Many abandoned apps fail because several layers moved together — Xcode, Swift, dependencies, signing or backend configuration. Updating everything at once removes the evidence needed to find the first real blocker.

That last sentence is the whole article, really. The instinct when a project will not build is to update everything and hope. It occasionally works. When it does not, you are left with a project that fails differently, no record of how it failed before, and no way back.

Here is a safer order.

1. Preserve the state before you touch anything

Before the first build attempt, make the current state recoverable.

Create a branch or tag from the exact commit you were given, so you can always return to it. Confirm that lockfiles are committed: Package.resolved for Swift Package Manager, Podfile.lock for CocoaPods, Cartfile.resolved for Carthage. These record what actually worked, and they are the single most valuable artefact in a stalled project.

Note whether the project uses a .xcodeproj or an .xcworkspace, and which scheme is meant to be the app. Capture any build scripts, .xcconfig files, Fastlane lanes or CI definitions. And find the last known good release: which commit shipped, when, and what version number it carried.

If lockfiles are missing, say so in your notes now. It changes what is realistically recoverable, and it is worth knowing early.

2. Work out what the historical environment was

The project was built successfully by someone, on something. Reconstructing what is often most of the puzzle.

Look for the Xcode version in CI configuration, in .xcode-version files, or in the project’s LastUpgradeCheck. Infer the Swift version from the build settings and from the language features the code actually uses. Note which package manager is in play, and whether more than one is. Check the minimum iOS deployment target. Check the CI image, which frequently pins a macOS and Xcode combination precisely.

If you can identify the era, you can often install that Xcode version alongside your current one and get a successful build immediately. A project that builds on old tooling is a very different situation from one that does not build at all: you have a working baseline, and everything after that is a controlled migration rather than an archaeology exercise.

3. Classify the failure

Not all build failures are the same problem, and the order you address them in matters. Read the first error rather than the last, and work out which of these you have.

Dependency resolution. Packages cannot be fetched or resolved. A missing repository, a moved URL, a private package you have no access to, or a version constraint that no longer has a solution.

Compilation. The source itself no longer compiles under a newer Swift. Language changes, removed APIs, stricter checking.

Linking. Compilation succeeds but linking fails. Usually a missing binary framework, an architecture mismatch, or a dependency built for a platform you are not targeting.

Code signing. Everything builds but signing fails. Expired certificates, missing profiles, a mismatched bundle identifier, or no access to the Apple Developer account.

Build scripts. A run script phase fails: a tool that is no longer installed, a path that no longer exists, a shell assumption that changed.

Configuration and secrets. The build needs an environment variable, a configuration file or a key that is not in the repository.

Runtime, not build. It builds and launches, then crashes or shows nothing. That is a different investigation, and reaching it counts as progress.

Fix them in dependency order: resolution first, then compilation, then linking, then signing. Trying to fix signing while packages will not resolve wastes time.

4. Restore dependencies carefully

This is where the most damage gets done, because the temptation to upgrade is strongest.

For Swift Package Manager, resolve against the committed Package.resolved first. Do not update packages until you have seen what the recorded versions do. If a package URL has moved or the repository is gone, look for a fork or a mirror before reaching for a different library.

For CocoaPods, install from the lockfile rather than running an update. pod install respects the lockfile; pod update discards it. For Carthage, check whether prebuilt binaries are still fetchable at all, since that is often where these projects stop.

Abandoned binary frameworks deserve particular care. An .xcframework with no source and no supplier is a hard dependency on something nobody can rebuild. Establish early whether it is still viable, because the answer changes the whole recovery plan.

While you are here, check licences and provenance. An inherited project sometimes contains vendored code with unclear rights, and that is much cheaper to discover now than during a due diligence exercise later.

5. Address language changes incrementally

If the code no longer compiles under current Swift, resist the urge to fix every error at once.

Work in small commits, one category of error at a time, so that each change is reviewable and reversible. Keep warnings visible rather than silencing them; in a recovery, warnings are frequently pointing at the actual behavioural change. Be careful with automated migration tools: they are useful, but review every change they make rather than accepting the whole diff.

Where a language change alters behaviour rather than just syntax, note it. Those are the changes that produce a build that succeeds and an app that misbehaves.

6. Separate building from shipping

Getting a local debug build is one milestone. Producing a signed archive that App Store Connect accepts is a completely different one, and conflating them makes both harder.

Get the app compiling and running in the simulator first. Then get it running on a device, which requires a development signing identity. Then get an archive building. Then get it uploaded. Each step has its own failure modes, and each one is easier to diagnose when the previous step is known good.

On signing, keep everything within company-controlled accounts. Certificates and profiles should be issued from the company’s Apple Developer account by someone with the right role. If access to that account is itself unclear, stop and resolve it first — there is a broader checklist for that in what to do when your iOS developer leaves.

7. Run it, then exercise the critical flows

A build is not a working app. Once it compiles, put it on a physical device and use it.

Launch it. Sign in. Walk through the main journeys. Try purchases if there are any. Turn the network off and see what happens. Compare its behaviour against the version currently on the App Store, which is your reference for what “working” means.

Recovering builds is common; recovering builds where the backend has since been decommissioned, the API contract has changed, or the third-party service has been shut down is common too. Better to find that now than after you have promised a release date.

8. Make the recovery durable

If you stop as soon as it builds on your machine, you have recreated exactly the situation that produced this problem.

Write a README covering setup, configuration and release. Pin or declare the tooling: an .xcode-version file, a documented Swift version, committed lockfiles. Get a CI build running, even a minimal one, because CI is the only honest test of whether the build is reproducible on a machine that is not yours. Document how secrets and configuration are supplied, without committing any of them. And write a release runbook: how a build is cut, submitted and rolled out.

An hour spent here is what stops this happening again.

9. Know when to stop patching

Sometimes recovery is not the right answer, and recognising that early saves money.

Consider stopping when a load-bearing dependency is abandoned with no viable replacement; when the backend the app depends on no longer exists; when the code has diverged so far from a functioning state that reconstruction exceeds a rebuild; or when the product itself has changed enough that the old app is the wrong app regardless.

Even then, the recovery work is not wasted. Understanding why the app cannot be revived is exactly the evidence needed to justify what replaces it.

When to bring someone in

A confident engineer with a week can often work through all of the above. Outside help is worth it when a release is blocked and the cost of the delay is real, when nobody internally has enough iOS depth to judge what is safe to change, or when a decision with money attached is waiting on a technical answer.

The shape that works is a short, contained triage rather than an open-ended engagement. My iOS App Rescue service is built around a one-to-three-day triage: at the end of it you should know whether the immediate problem is fixed, what remains, and whether more work is justified. If what you actually need is a fuller picture of the codebase rather than a build fix, the framework in how to assess an inherited iOS codebase covers that ground.

One last thing: back everything up before you begin, and never take a destructive action against an Apple Developer account you do not fully understand. Revoking a certificate or deleting a profile to “start clean” can turn a recoverable build problem into a much larger one.

Stuck on a build that will not come back?

Tell me what the app is, what error you are seeing, and what access you have. A one-to-three-day triage is usually enough to know whether it is recoverable and what it will take.