EDIT: January 13, 2019
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!
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.
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:
- Boot your macOS to Recovery mode (hold
- Open Terminal (you can find it in the Utilities drop-down menu)
csrutil disablein Terminal to disable SIP
nvram boot-args="amfi_get_out_of_my_way=0x1"to disable AMFI
- 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.
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 here: 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 your'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
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 from here https://github.com/biscuitehh/MarzipanPlatter/tree/master/Frameworks and copy it to a directory within your project.
Now back in Build Settings update
Runpath Search Paths parameter to point to your frameworks directory.
In the Build Phases tab, add your UIKit framework in the Link Binary With Libraries section.
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.
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 ran the release build as well (more about this shortly).
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 firstly 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:
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 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.