Outdated Post

Marzipan was launched at WWDC19 and is now known as Catalyst, available for all developers to use.
Image of Apple Engineer Craig presenting UIKit on the Mac at WWDC 2018.

In 2017, Apple started an ambitious new project: allowing iOS apps to be ported to macOS. This project, known as Marzipan, was previewed at WWDC 2018, where Apple used it to add previously unavailable Apple apps to macOS Mojave.

While Marzipan is in use on macOS Mojave, Apple has opted not to make it available for developers because of its relative instability. However, developers have already managed to create tools to bring iOS apps to macOS using Marzipan thanks to the Apple apps that were ported.

Marzipanify, a popular tool by Steve Troughton-Smith makes it possible to bring any iOS app to macOS, but it has a few restrictions which can add complexity.

In this blog post, I'll talk and explain 5 tips and tricks I discovered while I was converting my iOS app to run with Marzipan.

Getting started with Marzipanify

⚠️ Warning: At this stage, Marzipan requires disabling key security features of macOS. You are responsible for anything that may happen, and should undo your changes afterwards. Additionally, avoid distributing your Marzipan app just yet - the underlying API is still private and your apps won't run on systems that haven't disabled the security features.

I won't go into depth about using Marzipanify in this blog post, because the developer of the tool has already written a great blog post that goes into a lot of depth.

If you haven't played around with Marzipan before, I highly recommend you go and give that blog post a short read so you're familiar with how the tool works and how to get it working.

After disabling the security features as explained in the post, open Xcode and select a simulator, such as iPhone X🅂, as the target, and build your app (Command-B). To run Marzipanify, open Terminal and run one of the following commands:

If your app is targeting iOS 12 or above:

./marzipanify YourApp.app

Or, if your app is targeting an older version of iOS such as iOS 11 (this is not recommended as it can cause issues, as detailed in Importing AppKit):

INJECT_MARZIPAN_GLUE=1 ./marzipanify YourApp.app

The tool will rebuild your app to make it support iOSMac, the internal macOS name of Marzipan. You can then double click the new app to run it, and if you are not using any unavailable APIs or importing unsupported frameworks, you're done!

⭐️ Tip: If your app crashes and macOS doesn't show an alert explaining the crash, run the below command in your Terminal to enable crash reports. This will make it way easier to debug your Marzipanified app! defaults write com.apple.CrashReporter DialogType crashreport

Tip #1: Custom Build Configuration

If you are planning on testing out Marzipan with an existing app, you should create a custom build configuration to make it easy to disable unavailable APIs or frameworks in your app!

Screenshot of Xcode Project settings with a red circle around the New Build Configuration button.

To do this, navigate to your Xcode project settings and tap the + button under the "Configurations" section, then select "Duplicate Debug Configuration" to create a duplicate of the Debug configuration which you can rename Marzipan (or anything else).

Once you've created your new build configuration, you can create a new target and set the default build configuration to the new one you just created, then build using that target in the future.

This allows you to use code such as the following to disable certain code or APIs when running on Marzipan:

// SafariServices is not available on macOS
#if os(iOS) && !MARZIPAN
let viewController = SFSafariViewController(url: url)
present(viewController, animated: true)
#endif

Tip #2: Common Issues

The first tip is about the different types of common issues that can cause your app to crash on Marzipan. Your app will likely crash because Marzipan does not support a number of frequently used APIs and frameworks, such as UINotificationFeedbackGenerator, used for haptic feedback on modern iPhones, or the IntentsUI framework, used for Siri Shortcuts on iOS.

Issue 1: Missing Symbol

One of the most frequent issues I've experienced with Marzipan is when a symbol is missing, because it isn't available in macOS. In your crash log, it will look like this:

Dyld Error Message:
  Symbol not found: _OBJC_CLASS_$_UINotificationFeedbackGenerator_
  Referenced from: /Users/USER/\*/Marzipan.app/Contents/MacOS/Marzipan (which was built for Mac OS X 12.0)
  Expected in: /System/iOSSupport/System/Library/Frameworks/UIKit.framework/UIKit
 in /Users/USER/\*/Marzipan.app/Contents/MacOS/Marzipan

⭐ Tip: If any third party frameworks are causing this issue, you can follow the steps in Tip #4 below to disable the third party framework

When this happens, it means that your app is referencing a method or variable, typically within UIKit, that isn't available on Marzipan because Marzipan is still quite new.

To resolve this, just use Xcode to find instances of the symbol (in this example, you would search for UINotificationFeedbackGenerator) and remove it. If it continues to happen, make sure you've removed it from all third party frameworks you are using.

Issue 2: Missing Framework

Another likely issue is that your app is using one of many Apple frameworks that are yet to be available on macOS, such as SafariServices, which you're using if you use an SFSafariViewController to present websites within your app.

This is what the crash log will look like in this case:

Termination Reason:    DYLD, [0x1] Library missing

Application Specific Information:
dyld: launch, loading dependent libraries
/System/Library/Frameworks/SafariServices.framework/SafariServices

⭐ Tip: As in Issue 1, if any third party frameworks are causing this issue, you can follow the steps in Tip #4 below to disable the third party framework

To resolve this, you need to make sure you are not importing the missing framework anywhere in your code. As shown in the custom build configuration tip above, you can use code such as this to avoid importing missing frameworks:

// SafariServices is not available on macOS
#if os(iOS) && !MARZIPAN
#import SafariServices

let viewController = SFSafariViewController(url: url)
present(viewController, animated: true)
#endif

Issue 3: Importing AppKit

If your app or included frameworks reference or import AppKit in any way, your app will crash immediately. This is because AppKit has many similar internals to UIKit, which can easily conflict.

Although it may look different depending on the circumstances, the crash log in this case will look like this:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
    reason: 'AppKit is getting loaded into a disallowed context'

This issue is slightly harder to debug. Here are some steps to find where AppKit is being loaded:

  1. Check for mentions of AppKit in your code - make sure you're not importing it anywhere!
  2. Check if any third party frameworks are using AppKit - if so, follow Tip #4 below to disable them
  3. If you are still experiencing an AppKit related crash, an Apple framework is probably loading AppKit. The Photos framework is a known troublemaker, but if you're not using it, you can follow the steps below to check the Apple frameworks you are using:
    • Run otool -L /System/Library/Frameworks/Photos.framework/Photos in Terminal, replacing Photos with the framework name
    • Check if the Terminal output references AppKit, such as in this example (output shortened): /System/Library/Frameworks/Photos.framework/Versions/A/Photos /System/Library/Frameworks/AVFoundation.framework/Versions/A/AVFoundation /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit
      • /System/Library/Frameworks/Photos.framework/Photos:

⚠️ Note: If your app's deployment target is below iOS 12 and you are using INJECT_MARZIPAN_GLUE to make Marzipanify work, this is a known cause of the AppKit import crash! If you fail to find AppKit anywhere in your app, change your deployment target to iOS 12 and try re-building.

Tip 3: Automatic Build Script

Another way to improve your Marzipan build experience now that you have resolved any missing API or framework issues and added a custom build configuration is to automatically Marzipanify your app whenever you build it with the new build configuration. This lets you avoid the constant cycle of running Marzipanify after building.

Screenshot of Xcode target settings in the build phases section with a red circle around the New Build Phase button.

To add a custom build script that is run after your app builds, go to the Build Phases section in your target settings and click + to add a new "Run Script" build phase.

In the new build phase, make sure "Run script only when installing" is disabled so that it is run every time you build the app, and paste the following Bash code in:

if [ "${CONFIGURATION}" = "Marzipan" ]; then
cd /Users/YourName/MarzipanifyDirectoryHere
rm -rf YourApp.app
rm -rf Entitlements-*.plist
cp -f -R ${BUILT_PRODUCTS_DIR}/YourApp.app /Users/YourName/MarzipanifyDirectoryHere
./marzipanify YourApp.app
fi

⭐ Remember: Make sure to replace YourApp.app and /Users/YourName/MarzipanifyDirectoryHere with the name of your app and the directory where Marzipanify is located, or it won't work!

The above build script makes sure it is on the Marzipan build configuration you created earlier. If it is, it copies the build product (your app) to the directory where Marzipanify is located and runs Marzipanify on the binary, building your Marzipan app.

Tip 4: Working with CocoaPods

If you are working with CocoaPods, you may find that you need to disable a certain CocoaPod on your Marzipan build configuration because it uses an unavailable API or imports a missing framework.

After using the code from Tip #1 to disable the CocoaPod inside your app, you can replace the CocoaPod in your Podfile with the following line to make it compile only on iOS/normal build configurations:

pod 'PodName', :configurations => ['Debug', 'Release']

This will avoid compiling the CocoaPod on the Marzipan build configuration created earlier, preventing missing API/framework issues from occurring.

Conclusion

If all went well, you should now have a working Marzipan version of your iOS app running without crashes! You now have a custom build configuration where you can target macOS code and avoid missing API or framework issues.

If you'd like to explore more about Marzipan and make your iOS app fit in with normal macOS apps, I highly recommend you read Steve's blog, where he goes very in depth into new private APIs to implement features such as the macOS dark mode or translucent sidebar into your app.

I hope you enjoyed the tips and tricks on using Marzipan, and hopefully it helped you out with converting your iOS app to run on macOS! If you have any questions or feedback, feel free to send them or email me: [email protected]. Thanks for reading 🎉