Unlocking Marzipan - UIKit on macOS

At WWDC 2018, Apple stealthily unveiled a new way of creating macOS apps — a rumored, long-running project codenamed Marzipan. This was not an official announcement though, just an introduction of a couple of new apps in macOS 10.14 Mojave — Stocks, News, Home and Voice Memos — that were previously only available on iOS, and there was a quick mention of how future iOS apps could be ported to macOS by Craig Federighi, Apple's senior VP of software engineering.

Marzipan is still very unofficial, very beta and should only officially be released in 2019, but with some effort, we can play with it today!

So without further ado, let's take an iOS app and make it run on macOS!

THE APP

For this post I prepared a very simple app that allows you to fetch top stories from Hacker News and present them in a simple UITableView. This app is simple as far as functionality is concerned but includes some standard plumbing and networking to see whether common libraries can be used on macOS. The good news is that this is indeed possible, and we can use libraries like Alamofire, Swinject and others without any special effort! Just make sure to review the dependencies of the libraries you'd like to use, as they might link to frameworks that are not yet available on macOS.

Marzipan---Both-apps

PREPARING YOUR macOS

Since this is very new, you will need to install beta versions of macOS and Xcode. Specifically you'll need:

  • macOS 10.14 Mojave (Beta)
  • Xcode 10 (Beta)

To make your Marzipan app run, you unfortunately need to disable some security measures running on your macOS.

I highly suggest you re-enable these security measures when you're done tinkering with Marzipan or that you play with the framework on your non-primary Mac.

First, you need to disable System Integrity Protection. SIP is a security measure originally introduced in OS X 10.11 El Capitan that simply restricts what parts of macOS your app can touch. Disabling SIP is required, because as you will see later in this post, your macOS app must use a new entitlement com.apple.private.iosmac that is currently internal to macOS.

The second thing you need to do is to bypass AppleMobileFileIntegrity Kernel Extension. AMFI is a security measure that instantaneously kills any app that uses restricted entitlements, and com.apple.private.iosmac is indeed a restricted entitlement, so AMFI must be disabled.

To nuke your Mac's security you need to follow a couple of simple steps:

  1. Boot your macOS to Recovery mode (hold ⌘+R)
  2. Open Terminal (you can find it in the Utilities drop-down menu)
  3. Execute csrutil disable in Terminal to disable SIP
  4. Execute nvram boot-args="amfi_get_out_of_my_way=0x1" to disable AMFI
  5. Reboot your Mac and boot to macOS normally

Now that we're done reducing macOS security to shreds we can play with Marzipan!

MAKING AN iOS APP RUN ON macOS

I have prepared a GitHub repository with an app that runs on both iOS and macOS with exactly the same codebase. You can find it here: https://github.com/jankaltoun/TastyMarzipan.

The caveats

Modifying an iOS app to run on macOS is fairly easy. It mostly involves a target-build configuration. There are, however, a couple of caveats that need to be taken into account.

So far, I have not been able to port an app that uses Storyboards and Nibs. This is because Xcode will refuse to compile iOS Storyboards and Nibs when targeting macOS. From what I heard from the folks lucky enough to visit WWDC this year, some new apps use Storyboards, so this must be possible. Just maybe not with the tools that are available to us right now.

If you know how to use iOS Storyboards/Nibs with Marzipan, please let me know. I'd love to figure this out!

My example app defines its UI in the code.

I have run into issues with some UIKit constraints. For example the following code in the NewsCell.swift file builds and works in debug mode but crashes when you run the app outside Xcode.

NSLayoutConstraint.activate([
    stackView.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 1.0),
    bottomAnchor.constraint(equalToSystemSpacingBelow: stackView.bottomAnchor, multiplier: 1.0),
    stackView.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 1.0),
    trailingAnchor.constraint(equalToSystemSpacingAfter: stackView.trailingAnchor, multiplier: 1.0)
])

You need to use a macOS UIKit framework that is not easily obtainable. Fortunately, there's an awesome example by @biscuitteh available at https://github.com/biscuitehh/MarzipanPlatter, where you can obtain this framework.

Modifying and iOS app

Let's describe the steps needed to make your iOS app run on macOS. Hopefully, if you follow all of them, you'll be able to reproduce my results.

Start with a new Target

Open your Xcode project and add a new target. In Build Settings, update your Base SDK and Supported Platforms entries.

  • Base SDK: macOS
  • Supported Platforms: macOS

Marzipan-SDK-platform-1

Your code signing will most likely be messed up in the General tab so you'll need to fix it. As a simple test, unchecking and rechecking Automatically manage signing should be enough.

You will also need to modify the auto-generated Plist file to match a macOS app. Feel free to grab a copy from my example project.

Finally, you will need to update your Podfile with your new target and platform. In the end, it will look something like this:

use_frameworks!

abstract_target "HNClientGlobal" do
    pod "Swinject"
    pod "Alamofire"
    pod "PromiseKit"
    pod "PromiseKit/Alamofire"

    target "HNClient" do
        platform :ios, "12.0"
    end

    target "HNClientMac" do
        platform :macos, "10.14"
    end
end

Link to macOS UIKit

As mentioned above, your app won't compile without the macOS UIKit. Grab a copy at https://github.com/biscuitehh/MarzipanPlatter/tree/master/Frameworks, and copy it to a directory within your project.

Now back in Build Settings, update the Runpath Search Paths parameter to point to your frameworks directory.

Marzipan---runpath-search-paths

In the Build Phases tab, add your UIKit framework in the Link Binary With Libraries section.

Marzipan---link-binary-with-libraries-1

Add forbidden entitlements

We have already nuked our macOS security, so let's add a couple of entitlements to make the app work.

Create a new .entitlements file, and add the following code to it:

<?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>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.files.user-selected.read-only</key>
    <true/>
    <key>com.apple.private.iosmac</key>
    <true/>
</dict>
</plist>

Do not forget to update your Build Settings so that your settings know about this file.

Marzipan---entitlements-1

Add a magical environment variable

To be very honest, I am not very sure why this works, but an environment variable CFMZEnabled=1 must be used whenever you run the macOS app, otherwise it will crash on weird assertions and UILabels will behave weirdly.

Add it to your scheme and don't forget to use it when you run the release build as well (more about this shortly).

Marzipan---environment-variable-1

Build and run your macOS app!

This should be it! Select the scheme that was created for you by Xcode when you added the macOS target, hit the ⌘+R and behold! Your macOS app is running.

To export and run your new app, first archive it:

xcodebuild archive -workspace "YourApp.xcworkspace" -scheme YourAppMacScheme -archivePath ./build/YourApp.xcarchive

Then export the archive:

xcodebuild -exportArchive -archivePath ./build/YourApp.xcarchive -exportOptionsPlist ExportOptions.plist -exportPath ./build/

Your ExportOptions.plist file might look like this:

<?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>destination</key>
    <string>export</string>
    <key>method</key>
    <string>development</string>
    <key>signingStyle</key>
    <string>automatic</string>
</dict>
</plist>

And finally run the app with a magical environment variable:

CFMZEnabled=1 ./build/YourApp.app/Contents/MacOS/YourAppMacScheme

THAT'S IT!

As you can see, converting your iOS app to a macOS app is not that complicated — we made it just by following a couple of fairly easy steps.

When Marzipan finally comes out in 2019, I am sure many of the issues mentioned in this article will be resolved, and I feel that creating macOS apps will be incredibly easy for iOS developers.

I'd say let's prepare for an App Store explosion, as we'll see many new macOS apps made possible by Marzipan.

2019 will be a good year.

This article was written by STRV iOS developer Jan Kaltoun for his blog — blog.kaltoun.cz

Share Article
Jan Kaltoun

Jan Kaltoun

Jan is STRV's iOS developer. He loves coding, mostly in Swift on iOS.

You might also like...