Working on iOS 15+ Code on MacOS Catalina
Tags: ios, macos
Published: 2023-08-29 05:43:16

I try very hard to keep macOS Catalina, which my iMac late 2013 was stuck on, going for iOS development with Flutter. I've previously shared [various](flutter-macos-catalina.md) [workarounds](ios-debug-catalina.md) that allowed compiling Flutter app successfully and deploying to a real iOS device using updated device support files. Today, I am back with yet another workaround that is not previously discovered.

With the [previous method](flutter-ios-compile-errors.md), we had to delete references to any new API introduced in iOS 15 or later so we can use the iOS 14 toolchain to compile it. Otherwise, the toolchain will indicated an error. That may not work if you need to depend on closed source components where you cannot change the source code.

This new workaround goes a little further. It will actually compile iOS 15+ code using iOS 15+ toolchains and SDKs. This method does not require us to modify any (open source) library code we reference, and also helps us compile against closed source libraries where we do not have access to the source code.

*Note: the best way to develop for iOS is to use the latest Xcode on the latest macOS. And on an unsupported Mac it means using OCLP (OpenCore Legacy Patcher). That will ensure the best development experience without much tinkering. This guide is for people who for whatever reason do not want to use OCLP or want to remain on macOS Catalina.*

But I must warn you that this workaround comes with significant drawbacks. Here are the pros and cons up front before you invest too much time:

Pros:

* Compile iOS 15+ code using iOS 15+ SDK on Catalina w/ Xcode 12.4
* Debug code compiled with the iOS 15+ toolchain on the iOS 14.4 simulator that comes with Xcode 12.4

Cons:

* Cannot deploy code compiled with iOS 15+ SDK on a real device
* Cannot test code compiled with iOS 15+ SDK on iOS 15+ simulator (because the iOS 15+ simulators will not run on Catalina)
* Cannot submit compiled code to App Store (You will need the latest Xcode to do this!)
* Xcode sometimes bugs out and uses the wrong SDK to compile, resulting in annoying errors

You've been warned, now let's dive in.

## Download Xcode

In order to get newer iOS SDKs and compilers, we need newer copies of Xcode. This can be downloaded from Apple's developer website as long as you have an Apple ID. No paid membership is required. 

Head to the [download section](https://developer.apple.com/download/applications/) of the developer website, then check the Xcode downloads. I recommend only using stable versions of Xcode. For this example, I will use Xcode 14.2 (which comes with iOS 16.2 SDK).

Wikipedia has a [helpful page](https://en.wikipedia.org/wiki/Xcode#Version_comparison_table) on Xcode compatibility with various macOS versions and the SDKs they come with.

After you have the `xip` file, double click it to extract it as a macOS program.

## Copy SDK

Xcode 14.2 runs natively on macOS 12.5, so it will not run under macOS Catalina. We will copy files from inside its content, similar to what we did with Device Support files in a previous method.

Right click on the Xcode program, select "Show Package Contents", then go into `Contents/Developer/Platform`. Here you will see the various SDKs Xcode 14.2 comes with. We will need both `iPhoneOS.platform` and `iPhoneSimulator.platform` for iOS. You may also collect SDKs for other platforms such as tvOS, but I have not tested them.

Go into `iPhoneOS.platform/Developer/SDKs`, rename `iPhoneOS.sdk` to `iPhoneOS16.2.sdk` (you can delete the symlink with the same name, we won't need it), then copy to your Xcode 12.4 installation (the default would be your Application folder: `/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform`). After copying, your Xcode 12.4 folder should have `iPhoneOS.sdk`, `iPhoneOS14.4.sdk` (symlink), `iPhone16.2.sdk` (the one we copied).

Do the same for `iPhoneSimulator.platform`.

After this step, your Xcode 12.4 will see new SDKs, although you cannot compile code with it yet. That's because the SDK will use new language features introduced in newer Swift versions, and the default toolchain (compiler, linker, etc) will not be able to understand the new Swift features.

## Copy Toolchain

We will now copy the new toolchain. Toolchain is the collection of utility to compile and package your code into a testable, deployable, or uploadable form. This includes the Swift tools (based on LLVM).

Normally, you can download additional toolchains from [swift.org](https://www.swift.org/download/). These toolchains are the open source versions of Swift the Xcode toolchains are based on. If you install any open source toolchains, they will be installed at `/Library/Developer/Toolchains/`. Xcode is designed to work with the open source toolchains, which we are taking advantage here. Although this feature also comes with the limitation that you cannot deploy any compiled binaries onto a real device (and thus our workaround will be subject to the same limitation).

From the Xcode 14.2 base, go into `Contents/Developer/Toolchains`. You will see `XcodeDefault.xctoolchain`. Copy this to `/Library/Developer/Toolchains/` and rename it to something more descriptive and to your liking. For example, I use `Xcode-14.2.xctoolchain`. 

You cannot use this toolchain without performing one further step, which is to fool Xcode into thinking it is one of the open source toolchains from swift.org.

Right click on the xctoolchain file and select "Show Package Contents". The Xcode toolchain comes with a file `ToolchainInfo.plist`, which is not useful for our purpose. We will delete it. We will create a file called `Info.plist`. I provided the content below. (Another way to obtain this file is to download the corresponding open source toolchain and copy over this file).

```plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Aliases</key>
	<array>
		<string>swift</string>
	</array>
	<key>CFBundleIdentifier</key>
	<string>org.swift.573202201171a</string>
	<key>CompatibilityVersion</key>
	<integer>2</integer>
	<key>CompatibilityVersionDisplayString</key>
	<string>Xcode 8.0</string>
	<key>CreatedDate</key>
	<date>2023-01-15T23:17:44Z</date>
	<key>DisplayName</key>
	<string>Swift 5.7.3 Release 2022-01-17 (a)</string>
	<key>OverrideBuildSettings</key>
	<dict>
		<key>ENABLE_BITCODE</key>
		<string>NO</string>
		<key>SWIFT_DEVELOPMENT_TOOLCHAIN</key>
		<string>YES</string>
		<key>SWIFT_DISABLE_REQUIRED_ARCLITE</key>
		<string>YES</string>
		<key>SWIFT_LINK_OBJC_RUNTIME</key>
		<string>YES</string>
		<key>SWIFT_USE_DEVELOPMENT_TOOLCHAIN_RUNTIME</key>
		<string>YES</string>
	</dict>
	<key>ReportProblemURL</key>
	<string>https://bugs.swift.org/</string>
	<key>ShortDisplayName</key>
	<string>Swift 5.7.3 Release</string>
	<key>Version</key>
	<string>5.7.3.20220117101</string>
</dict>
</plist>
```

The exact names inside this file does not matter too much. You can for example change the `ShortDisplayName` to `Xcode 14.2` to better identify it in Xcode.

After all is done, Xcode will be able to see the toolchain as if it is one of the open source toolchains, and can use it to compile code using the corresponding SDK.

## Doing Compilation in Xcode

Open Xcode 12.4, at the top menu bar, under "Xcode -> Toolchains", you should see a new toolchain listed. Select it.

Open your project if you haven't already, open the project file and go into "Build Settings", under "Base SDK", you should be able to choose between SDKs (such as iOS, macOS, tvOS, etc) and their versions. Xcode by default hides the highest version (the `iOS 16.2` version we copied) because it assumes the base version `iOS` would be the highest. Since we didn't change the symlink in the SDK folder, our default iOS SDK is the same as `iOS 14.4`. Regardless, select "Other..." and type in `iphoneos16.2`. This will instruct Xcode to compile the project with 16.2.

The next step is just to compile the project for running on the simulator. You may need to clean the build folder and Xcode cache or even restart Xcode or the computer, since Xcode may be confused about files produced by an old toolchain. I have a lot of problems with switching between the newer toolchains + SDKs and the default toolchain + SDK. Sometimes Xcode will be using the default toolchain to compile with newer SDKs despite the project file saying otherwise.

This is the end of the tutorial. 

## Afterword

This is probably the closest you can get to be compiling iOS 15+ code on macOS 10.15 Catalina. It works if your primary development flow relies heavily on the simulator and you don't need to test iOS 15 integrations. 

Apple has also been making a lot of changes to the new Xcode 15 (iOS 17 SDK) architecture, including changing the [device stack for iOS](https://developer.apple.com/forums/thread/730947) (no more device support files). The new swift toolchain (5.9 beta) also no longer runs on Catalina. This means the end of the road for Catalina is coming in a few months even if you managed to use this workaround.

You're now strongly advised to seek other alternatives, such as a new mac, OCLP, or a macOS VM. It will give you less headache and more time for focusing on development. Either way, I hope you enjoyed this tutorial.