Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Shipping a Mac Catalyst App: The good, the bad ...

Shipping a Mac Catalyst App: The good, the bad and the ugly

PSPDFKit ported PDF Viewer (https://pdfviewer.io), a project with over 2 MLOC and lots of UIKit code to the mac via Catalyst. Having played with "Marzipan" in 2018, the team was able to port their huge app in a relative short timeframe. Learn what you have to do after checking the [X] Mac checkbox, how far Catalyst can be pushed, how it interacts with AppKit, and how to work around the worst bugs. We'll show real code, a selection of r̵a̵d̵a̵r̵ Feedback Assistant items, strategies to keep code branching low, and some design alterations that were necessary to make it feel more at home on the Mac.

Peter Steinberger

October 08, 2019
Tweet

More Decks by Peter Steinberger

Other Decks in Programming

Transcript

  1. Shipping a Mac Catalyst App The good, the bad and

    the ugly Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  2. Sweet sweet History aka Marzipan & "UIKit for Mac" Image:

    https://commons.wikimedia.org/wiki/File:Marzipan_(8311107161).jpg
  3. Sharing Code between iOS and macOS Peter Steinberger - @steipete

    - Shipping a Mac Catalyst app - FrenchKit 2019
  4. Why? Situation Technology Existing macOS App AppKit Existing iOS App

    Catalyst New cross-platform App SwiftUI Existing complex Web-App Electron Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  5. Compatibility macOS iOS Date OS X 10.9 Mavericks iOS 7

    Sept 2013 OS X 10.10 Yosemite iOS 8 Sept 2014 OS X 10.11 El Capitan iOS 9 Sept 2015 macOS 10.12 Sierra iOS 10 Sept 2016 macOS 10.13 High Sierra iOS 11 Sept 2017 macOS 10.14 Mojave iOS 12 Sept 2018 macOS 10.15 Catalina iOS 13 Sept 2019 Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  6. What makes a good Mac app? Peter Steinberger - @steipete

    - Shipping a Mac Catalyst app - FrenchKit 2019
  7. Designing for Big Screens » Popovers -> Sidebars » Navigation

    Bar pushes -> Fade/Disable » UIToolbar -> NSToolbar Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  8. Toolbars #if TARGET_OS_UIKITFORMAC #import <AppKit/AppKit.h> #import <UIKit/NSToolbar+UIKitAdditions.h> #endif windowScene.titlebar?.toolbar =

    NSToolbar() NSToolBarItem(identifier, UIBarButtonItem) NSToolbarItemGroup Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  9. Search Field let searchItem = NSToolbarItem(...) searchItem.view = NSSearchField() Peter

    Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  10. AppKit Bundles - Loading // Swift let pluginPath = Bundle.main.builtInPlugInsPath!.appending("/CatalystBundle.bundle")

    let bundle = Bundle(path: pluginPath)! bridge = AppKitBundleLoader().load(bundle) // Objective-C ! - (id<PDFVAppKitUIObjcBridge>)loadBundle:(NSBundle *)bundle { return (id<PDFVAppKitUIObjcBridge>)[[[bundle principalClass] alloc] init]; } PSPDFKitGlobal.sharedInstance.appKitBridge.cursorMode = cursor; Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  11. Back to Search let bridge = (UIApplication.shared.delegate as! AppDelegate).bridge! let

    item = bridge.customToolbarItem(identifier: itemIdentifier.rawValue) { searchText in self.delegate?.searchTextDidChange(searchText: searchText) } // AppKit - (NSToolbarItem *)customToolbarItemWithIdentifier:(NSString *)identifier callback:(void (^)(NSString *))callback { NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:identifier]; item.view = [NSSearchField new]; // store handler in subclass return item; } Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  12. UIMenuBuilder & UIMenuSystem override func buildMenu(with builder: UIMenuBuilder) { guard

    builder.system == .main else { return } // The format menu doesn't make sense builder.remove(menu: .format) let goContentMenu = UIMenu(title: "go", identifier: UIMenu.Identifier("go-content"), options: [.displayInline], children: [ UIKeyCommand(title: PSPDFLocalize("PrevPage"), action: #selector(PSPDFKeyHandler.previousPageAction), input: UIKeyCommand.inputUpArrow), UIKeyCommand(title: PSPDFLocalize("NextPage"), action: #selector(PSPDFKeyHandler.nextPageAction), input: UIKeyCommand.inputDownArrow), ]) let goMenu = UIMenu(title: PSPDFLocalize("Go"), identifier: UIMenu.Identifier("go"), children: [goContentMenu]) builder.insertSibling(goMenu, beforeMenu: .window) Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  13. Creating an Open Recent Menu Peter Steinberger - @steipete -

    Shipping a Mac Catalyst app - FrenchKit 2019
  14. Challenges » Create arbitrary menu items » Dynamically reload menu

    after open (UIMenuSystem!) » Add images to menu (wohoo beta 9!) » Get file icon from system (uhm...) » Save/Load security scoped bookmark Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  15. File Type Icons let interactionController = UIDocumentInteractionController(url: fileURL) let allIcons

    = interactionController.icons // allIcons is guaranteed to have at least one image switch preferredSize { case .smallest: return allIcons.first! case .largest: return allIcons.last! } } Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  16. Security Scoped Bookmarks » Entitlement: com.apple.security.files.bookmarks.app-scope -> 1 » NSURLBookmarkCreationWithSecurityScope

    & NSURLBookmarkResolutionWithSecurityScope » startAccessingSecurityScopedResource & stopAccessingSecurityScopedResource https://gist.github.com/steipete/40a367b64b57bfd0b44fa8d158fc016c
  17. We force it ! #define NSURLBookmarkCreationWithSecurityScope ( 1 << 11

    ) #define NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess ( 1 << 12 ) #define NSURLBookmarkResolutionWithSecurityScope ( 1 << 10 ) Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  18. Challenges » ✅ Create arbitrary menu items » ✅ Dynamically

    reload menu after open » ✅ Add images to menu $ » ✅ Get file icon from system » ✅ Save/Load security scoped bookmark Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  19. NSDocumentController /* Add an item corresponding to the data located

    by a URL to the Open Recent menu, or replace an existing item with the same URL. You can use this even in non-NSDocument-based applications. */ open func noteNewRecentDocumentURL(_ url: URL) /* The action of the Open Recent menu's Clear Menu item. */ @IBAction open func clearRecentDocuments(_ sender: Any?) /* Return an array of URLs for the entries currently appearing in the Open Recent menu. */ open var recentDocumentURLs: [URL] { get } Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  20. Native Open Recent NSMenuItem *recent = [fileMenu addItemWithTitle:@"Open Recent" action:nil

    keyEquivalent:@""]; NSMenu *recentMenu = [[NSMenu alloc] initWithTitle:@"Open Recent"]; [recentMenu performSelector:@selector(_setMenuName:) withObject:@"NSRecentDocumentsMenu"]; [recent setSubmenu:recentMenu]; recent = [recentMenu addItemWithTitle:NSLocalizedString(@"Clear Menu", nil) action:@selector(clearRecentDocuments:) keyEquivalent:@""]; http://lapcatsoftware.com/blog/2007/07/10/working-without-a-nib-part-5-open-recent-menu/
  21. Native Open Recent: UIKit override func buildMenu(with builder: UIMenuBuilder) {

    let fileOperationsMenu = UIMenu(title: "FileOps", identifier: UIMenu.Identifier("file_ops"), options: [.displayInline], children: [ UIKeyCommand(title: "New File", action: #selector(AppDelegate.newFile), input: "n", modifierFlags: [UIKeyModifierFlags.command]), UIKeyCommand(title: "Open...", action: #selector(AppDelegate.openFile), input: "o", modifierFlags: [UIKeyModifierFlags.command]), UIMenu(title: "Open Recent", // This needs to stay non-localized to itentify later identifier: UIMenu.Identifier("open_recent"), options: [], children: []), ]) builder.insertSibling(fileOperationsMenu, beforeMenu: .close) // This needs to run after the menu has been built, so let's do it a runloop later. DispatchQueue.main.async { let bridge = (UIApplication.shared.delegate as! AppDelegate).bridge! bridge.addOpenRecentMenu() } Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  22. Native Open Recent: AppKit - (void)addOpenRecentMenu { // This searches

    for "Open Recent" and hacks it to actually deliver for (NSMenuItem *menu in NSApp.mainMenu.itemArray) { for (NSMenuItem *submenu in menu.submenu.itemArray) { if ([submenu.title isEqualToString:@"Open Recent"]) { NSMenu *openRecentMenu = [[NSMenu alloc] initWithTitle:@"Open Recent"]; [openRecentMenu performSelector:NSSelectorFromString(@"_setMenuName:"]) withObject:@"NSRecentDocumentsMenu"]; submenu.submenu = openRecentMenu; break; } } } } Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  23. Native Open Recent: AppKit - (void)addOpenRecentMenu { // This searches

    for "Open Recent" and hacks it to actually deliver for (NSMenuItem *menu in NSApp.mainMenu.itemArray) { for (NSMenuItem *submenu in menu.submenu.itemArray) { if ([submenu.title isEqualToString:@"Open Recent"]) { NSMenu *openRecentMenu = [[NSMenu alloc] initWithTitle:@"Open Recent"]; [openRecentMenu performSelector:NSSelectorFromString(@"_setMenuName:"]) withObject:@"NSRecentDocumentsMenu"]; submenu.submenu = openRecentMenu; break; } } } } // Hack: triggers installation of NSRecentDocumentsMenu [NSDocumentController.sharedDocumentController valueForKey:@"_installOpenRecentMenus"]]; Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  24. Recap » History of Mac Catalyst/Marzipan » Use Cases +

    Platform Adoption » What makes a good Mac app? » Creating Toolbars & Search Fields » AppKit Bundles & NSCursor » Security Scoped Bookmarks » NSDocumentController ! Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  25. Thank You FrenchKit! More hacks & rants: @steipete on Twitter

    Try PDF Viewer for Mac: pdfviewer.io/store-mac https://www.motor1.com/news/77404/you-can-thank-star-wars-for-the-spaceballs-eagle-5-winnebago/