At the recent WWDC 2019, Apple announced that in the current Xcode 11 betas they are bringing a new way to port a native iOS iPad app to macOS. We’ve been keeping an eye on the progress of Apple’s Project Catalyst (previously Project Marzipan) for a while, but now that the functionality is available to developers we decided to give it a try on one of our previous iPad-optimised projects. This article is intended to show you how to port a typical existing iPad app over to macOS Catalina using UIKitForMac.
In theory, all we need to do is select the “Mac” checkbox in the project settings of our existing iPad app project.
For a simple app this may well be the case, but for a typical app with lots of Cocoapods dependencies it’s unfortunately not that simple. This article will describe the process I went through to convert an iPad Pro app into a native macOS app that uses Apple’s new UIKitForMac.
After selecting the Mac checkbox above I then tried to build the project but got a number of errors for different pods within my project, mainly because these pods had not yet been ported to UIKitForMac or iOS 13. There are also still outstanding issues related to Project Catalyst support within Cocoapods, which means that some of the more complicated pods aren’t easy to work with.
Out of the pods that were causing errors, I decided which pods I needed and which pods the app could run without. The pods that I decided were unnecessary for the initial port were: Crashlytics, Firebase/Analytics, Fabric and JGProgressHUD. I will now go through the pods that were necessary but had issues, and how I fixed each of these pods so that they would run in a native Mac app.
Realm wouldn’t build for UIKitForMac due to an error:
in .../Pods/Realm/core/librealmcore-ios.a(bptree.o), building for UIKitForMac, but linking in object file built for iOS Simulator, for architecture x86_64”.
This is quite a typical error when porting from iOS to macOS and refers to the lack of available binaries for the UIKitForMac platform. Surprisingly, the UIKitForMac build is actually considered ‘iOS’, but with an architecture of x86_64. Until xCode 11 an x86_64 architecture for iOS would always have been a binary built for the simulator, but this is no longer the case. This seems to have been some of the motivation behind the new .xcframework bundle type, which is explained in this WWDC 2019 talk.
On the Realm GitHub there was an open branch for Xcode-11-2, which I hoped may fix this issue but the same issue persisted. After reading through a ticket on the Realm GitHub with regards to Xcode 11 fixes, I found there was an open feature branch for Swift Package Manager support.
I commented-out the Realm pod in the podfile and opted to add the pod through the Swift Package Manager route. To do this I followed these simple instructions:
The result of adding Realm to my project in this way was that I still had 2 pods in my Podfile that depended on Realm. If I left them in my podfile as-is they would re-download the Realm pod as their dependency, and it’s difficult to share Swift Package Manager dependencies with Cocoapods. Therefore I decided that the best option would be to add these 2 pods as Swift Packages instead, keeping the dependency tree within the same package management system.
I found it relatively straightforward to create a package.swift file based on what we see in a dependency’s GitHub repository and existing .podspec. Below is an example of the Package.swift file I made for the RxRealmDataSources dependency.
// swift-tools-version:5.1
import PackageDescription
let package = Package(
name: "RxRealmDataSources",
// 1
platforms: [
.macOS(.v10_15), .iOS(.v12), .tvOS(.v9), .watchOS(.v3)
],
// 2
products: [
.library(name: "RxRealmDataSources", targets: ["RxRealmDataSources"])
],
// 3
dependencies: [
.package(url: "https://github.com/foresightmobile/RxSwift", .branch("removing-uiwebkit")),
.package(url: "https://github.com/foresightmobile/RxRealm.git", .branch("removed-realm")),
.package(url: "https://github.com/realm/realm-cocoa", .branch("tg/spm"))
],
// 4
targets: [
.target(
// 5
name: "RxRealmDataSources",
// 6
dependencies: ["RxRealm", "RxSwift", "RealmSwift", "Realm", "RxCocoa"],
// 7
path: ".",
// 8
sources: ["RxRealmDataSources"]
)
]
)
I184KL0LDI72.2KRankn/aAgel0whoissourceRank136KMore dataSummary reportDiagnosisDensity00n/a
The first two lines are always necessary, the first line tells the compiler which version of Swift Tools we are using and so decides the syntax of the Package.swift file. The rest of the file is the package definition itself.
When changing Package.swift files and pushing changes to GitHub, you may find that Xcode isn’t picking up your updates from the remote repository. if your Package.swift file doesn’t seem to be updating you will need to alternate between two urls to force updates: i.e. switch between “https://github.com/foresightmobile/RxSwift” and “https://github.com/foresightmobile/RxSwift.git” in the dependency settings. This should force Xcode to update its cached package files.
For these two pods there was no Package.swift file in their repository, which is required to add a dependency as a Swift Package. So I had to fork these two pods and create Package.swift files for each of them here and here.
This pod was using a class called UIWebKit that is not available for UIKitForMac, so I had to fork the project and remove the UIWebKit references from the code. Then later on in the porting process I realised that another pod was relying on RxSwift 4.5 (rather than the latest 5.x), so I had to create another branch with the UIWebKit removed from RxSwift 4.5.
The most up to date version of this pod used RxSwift 5, but we had another pod that was relying on RxSwift 4.5. Therefore I had to fork this project and make a branch where the Package.swift file was pointing to the version of RxSwift detailed above.
Nuke was using a deprecated version of the URLCache initialiser, which meant I had to switch the name of one of the arguments in the initialisation of URLCache from “diskPath” to “directory” and supply a URL instead of a string. I also made use of an #if targetEnvironment statement here to determine whether to use “diskPath” or “directory”, depending on whether the target build environment is UIKitForMac or not. This means the dependency will still run on iPad as well as Mac.
public static let sharedUrlCache = { () -> URLCache in
#if targetEnvironment(UIKitForMac)
return URLCache(
memoryCapacity: 0,
diskCapacity: 150 * 1024 * 1024, // 150 MB
directory: URL(fileURLWithPath: cachePath))
#else
return URLCache(
memoryCapacity: 0,
diskCapacity: 150 * 1024 * 1024, // 150 MB
diskPath: cachePath)
#endif
}
…/Pods/Fabric/run: No such file or directory
Command PhaseScriptExecution failed with a nonzero exit code
After fixing issues with the above pods, I was still having an issue with the Fabric pod, which I had commented out of my Podfile earlier. This was because I had a Fabric run script leftover from when Fabric was still in the app. I removed this and the error involving Fabric went away.
The final error I was getting was an error that the XCTest framework could not be found. This was because some extra packages had been imported at some point during the Swift Package Manager transition. These extra packages could be found under the “Link with Binary Libraries” section in the “Build Phases” tab of the project settings. Once I removed the packages that I had not added manually (RxBlocking, RxTest), the app ran and I could successfully use it in a window on my Mac and also in the iPad simulator.
After getting the app to run in a window on my Mac, it became apparent there was an issue with the main layout and the title bar of the window. This title bar was covering the top part of the screen in the app. To fix this I simply changed the top constraint for the main view element at the top of the page to constrain to the safe area rather than the outside of the window.
One of the pods I decided to remove was the JGProgressHUD. This was a pod that I could not fix quickly, so an alternative had to be considered. We were using this pod to display the progress of loading and processing data from Contentful. To fit more in-line with macOS interaction paradigms the alternative we decided to pursue was to disable all the buttons on the screen while this data was being downloaded and processed so that a user cannot interrupt this data flow. This was a quick fix and more in-line with macOS interaction paradigms.
The intention of this project was to determine the impact of an iPad to macOS port with a real-world example. The porting process was relatively simple, and given a few more weeks I’m sure that a lot of our dependencies would have added Swift Package Manager support and made the transition easier.