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

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.

832ece085bfe2c7c5b0ed6be62d7e675?s=128

Peter Steinberger

October 08, 2019
Tweet

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. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  3. pdfviewer.io Peter Steinberger - @steipete - Shipping a Mac Catalyst

    app - FrenchKit 2019
  4. Sweet sweet History aka Marzipan & "UIKit for Mac" Image:

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

    - Shipping a Mac Catalyst app - FrenchKit 2019
  6. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  7. Just a Checkbox? Peter Steinberger - @steipete - Shipping a

    Mac Catalyst app - FrenchKit 2019
  8. https://www.youtube.com/watch?v=2OuQarA0a7I

  9. marzipanify https://github.com/steventroughtonsmith/marzipanify

  10. https://developer.apple.com/ipad-apps-for-mac/

  11. 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
  12. https://imgur.com/gallery/PNBF707

  13. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  14. Image: https://www.apple.com/macos/catalina/

  15. 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
  16. iOS Adoption: iOS 12 Peter Steinberger - @steipete - Shipping

    a Mac Catalyst app - FrenchKit 2019
  17. iOS Adoption: iOS 13 Peter Steinberger - @steipete - Shipping

    a Mac Catalyst app - FrenchKit 2019
  18. macOS Adoption Peter Steinberger - @steipete - Shipping a Mac

    Catalyst app - FrenchKit 2019
  19. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  20. Breaking News Peter Steinberger - @steipete - Shipping a Mac

    Catalyst app - FrenchKit 2019
  21. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  22. https://twitter.com/tylerhall/status/1181324733893287938

  23. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  24. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  25. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  26. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  27. What makes a good Mac app? Peter Steinberger - @steipete

    - Shipping a Mac Catalyst app - FrenchKit 2019
  28. Buttons https://pilky.me/appreciating-appkit-part-1/

  29. Text Input Peter Steinberger - @steipete - Shipping a Mac

    Catalyst app - FrenchKit 2019
  30. Sliders Peter Steinberger - @steipete - Shipping a Mac Catalyst

    app - FrenchKit 2019
  31. Date Picker Peter Steinberger - @steipete - Shipping a Mac

    Catalyst app - FrenchKit 2019
  32. Table & Outline Peter Steinberger - @steipete - Shipping a

    Mac Catalyst app - FrenchKit 2019
  33. Rule Editor Peter Steinberger - @steipete - Shipping a Mac

    Catalyst app - FrenchKit 2019
  34. Grid View Peter Steinberger - @steipete - Shipping a Mac

    Catalyst app - FrenchKit 2019
  35. Limitations of Catalyst Peter Steinberger - @steipete - Shipping a

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

    Bar pushes -> Fade/Disable » UIToolbar -> NSToolbar Peter Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  37. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  38. Toolbars Peter Steinberger - @steipete - Shipping a Mac Catalyst

    app - FrenchKit 2019
  39. 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
  40. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  41. Search Field let searchItem = NSToolbarItem(...) searchItem.view = NSSearchField() Peter

    Steinberger - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  42. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  43. Cursor Peter Steinberger - @steipete - Shipping a Mac Catalyst

    app - FrenchKit 2019
  44. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  45. NSCursor NSCursor.pointingHand.set() Peter Steinberger - @steipete - Shipping a Mac

    Catalyst app - FrenchKit 2019
  46. NSCursor _ = NSClassFromString("NSCursor")!.value(forKeyPath: "pointingHandCursor.set") Peter Steinberger - @steipete -

    Shipping a Mac Catalyst app - FrenchKit 2019
  47. AppKit Bundles Peter Steinberger - @steipete - Shipping a Mac

    Catalyst app - FrenchKit 2019
  48. AppKit Bundles - Structure Peter Steinberger - @steipete - Shipping

    a Mac Catalyst app - FrenchKit 2019
  49. 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
  50. 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
  51. Menu Bar Peter Steinberger - @steipete - Shipping a Mac

    Catalyst app - FrenchKit 2019
  52. 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
  53. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  54. Creating an Open Recent Menu Peter Steinberger - @steipete -

    Shipping a Mac Catalyst app - FrenchKit 2019
  55. 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
  56. UIMenuSystem Peter Steinberger - @steipete - Shipping a Mac Catalyst

    app - FrenchKit 2019
  57. 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
  58. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  59. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  60. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  61. File Type Icons let icon = NSWorkspace.shared.icon(forFile: "Cookies.pdf") Peter Steinberger

    - @steipete - Shipping a Mac Catalyst app - FrenchKit 2019
  62. Accessing Files aka Security Scoped Bookmarks Peter Steinberger - @steipete

    - Shipping a Mac Catalyst app - FrenchKit 2019
  63. Security Scoped Bookmarks » Entitlement: com.apple.security.files.bookmarks.app-scope -> 1 » NSURLBookmarkCreationWithSecurityScope

    & NSURLBookmarkResolutionWithSecurityScope » startAccessingSecurityScopedResource & stopAccessingSecurityScopedResource https://gist.github.com/steipete/40a367b64b57bfd0b44fa8d158fc016c
  64. OSX != Mac Catalyst Peter Steinberger - @steipete - Shipping

    a Mac Catalyst app - FrenchKit 2019
  65. 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
  66. 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
  67. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  68. https://me.me/i/hmm-helpful-you-arenot-memegen-com-hmm-helpful-yoda-meme-f92f5922fb14402493c195daf6746b16

  69. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  70. 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
  71. 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/
  72. 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
  73. 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
  74. https://imgflip.com/i/z7mtd

  75. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  76. Something is missing... Peter Steinberger - @steipete - Shipping a

    Mac Catalyst app - FrenchKit 2019
  77. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  78. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  79. 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
  80. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  81. Legal? FB6929711 Peter Steinberger - @steipete - Shipping a Mac

    Catalyst app - FrenchKit 2019
  82. Peter Steinberger - @steipete - Shipping a Mac Catalyst app

    - FrenchKit 2019
  83. 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
  84. Buzzfeed, https://www.pinterest.at/pin/48202658485156254/

  85. 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/