├── AppKitIntegration ├── AppKitController.swift └── Info.plist ├── CatalystExample ├── App │ ├── Controllers │ │ ├── CATAppDelegate+MenuBuilder.swift │ │ ├── CATAppDelegate.swift │ │ └── CATPreferencesController.swift │ └── Main Window │ │ ├── CATMainViewController.swift │ │ ├── CATWindowSceneDelegate+NSToolbar.swift │ │ └── CATWindowSceneDelegate.swift ├── CatalystExample.entitlements ├── Common.swift ├── Content View │ └── CATItemViewController.swift ├── Data Model │ ├── CATElement.swift │ ├── CATFolder.swift │ ├── CATItem.swift │ └── CATSourceFile.swift ├── Middle Column │ ├── CATItemListViewCell.swift │ └── CATItemListViewController.swift ├── Number+Scaling.swift ├── Resources │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Credits.rtf │ ├── Example.json │ ├── Info.plist │ └── Localizable.strings └── Sidebar │ ├── CATSourceListViewController+Reordering.swift │ └── CATSourceListViewController.swift ├── README.md └── catalystapp.xcodeproj ├── project.pbxproj ├── project.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist └── xcshareddata └── xcschemes ├── AppKitIntegration.xcscheme └── CatalystExample.xcscheme /AppKitIntegration/AppKitController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import AppKit 8 | 9 | extension NSObject { 10 | @objc func hostWindowForSceneIdentifier(_ identifier:String) -> NSWindow? { 11 | return nil 12 | } 13 | } 14 | 15 | class AppKitController : NSObject { 16 | var searchItems:[String:NSSearchToolbarItem] = [:] 17 | 18 | // MARK: - 19 | 20 | @objc public func _marzipan_setupWindow(_ note:Notification) { 21 | 22 | NSLog("_marzipan_setupWindow: \(String(describing: note.userInfo))") 23 | 24 | /* 25 | Here, AppKit has generated the host window for your UIKit window. 26 | Now it is safe to go do any AppKit-y things you'd like to do to it 27 | */ 28 | } 29 | 30 | @objc public func closeWindowForSceneIdentifier(_ sceneIdentifier:String) { 31 | guard let appDelegate = NSApp.delegate as? NSObject else { return } 32 | 33 | if appDelegate.responds(to: #selector(hostWindowForSceneIdentifier(_:))) { 34 | guard let hostWindow = appDelegate.hostWindowForSceneIdentifier(sceneIdentifier) else { return } 35 | 36 | hostWindow.performClose(self) 37 | } 38 | } 39 | } 40 | 41 | extension AppKitController { 42 | 43 | @objc public func searchToolbarItem(sceneIdentifier:String, itemIdentifier:NSToolbarItem.Identifier, target:AnyObject, selector:Selector) -> NSToolbarItem { 44 | 45 | if let searchItem = searchItems[sceneIdentifier] { 46 | 47 | return searchItem 48 | } 49 | else { 50 | let searchItem = NSSearchToolbarItem(itemIdentifier: itemIdentifier) 51 | searchItem.target = target 52 | searchItem.action = selector 53 | searchItem.searchField.placeholderString = NSLocalizedString("TOOLBAR_SEARCH_PLACEHOLDER", comment: "") 54 | 55 | searchItems[sceneIdentifier] = searchItem 56 | 57 | return searchItem 58 | } 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /AppKitIntegration/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /CatalystExample/App/Controllers/CATAppDelegate+MenuBuilder.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import UIKit 8 | 9 | extension CATAppDelegate { 10 | override func buildMenu(with builder: UIMenuBuilder) { 11 | 12 | builder.remove(menu: .format) 13 | 14 | do { 15 | let command = UIKeyCommand(input: "N", modifierFlags: [.command, .alternate], action: NSSelectorFromString("newItem:")) 16 | 17 | command.title = NSLocalizedString("MENU_ITEM_NEW_ITEM", comment: "") 18 | 19 | let menu = UIMenu(title: NSLocalizedString("MENU_ITEM", comment: ""), image: nil, identifier: UIMenu.Identifier("MENU_ITEM"), options: [], children: [command]) 20 | builder.insertSibling(menu, afterMenu: .edit) 21 | } 22 | 23 | super.buildMenu(with: builder) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /CatalystExample/App/Controllers/CATAppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import UIKit 8 | 9 | /* 10 | Let Swift know how to talk to the APIs defined in your AppKitIntegration framework, 11 | which you can't link directly because it's built for macOS, not iOS 12 | */ 13 | 14 | #if targetEnvironment(macCatalyst) 15 | 16 | extension NSObject { 17 | @objc public func _marzipan_setupWindow(_ sender:Any) { 18 | 19 | } 20 | 21 | @objc public func searchToolbarItem(sceneIdentifier:String, itemIdentifier:NSToolbarItem.Identifier, target:AnyObject, selector:Selector) -> NSToolbarItem { 22 | return NSToolbarItem(itemIdentifier: itemIdentifier) 23 | } 24 | 25 | @objc func CAT_endEditing(_ sender:Any?) { 26 | 27 | } 28 | } 29 | #endif 30 | 31 | @main 32 | class CATAppDelegate: UIResponder, UIApplicationDelegate { 33 | 34 | var window: UIWindow? 35 | 36 | #if targetEnvironment(macCatalyst) 37 | 38 | static var appKitController:NSObject? 39 | 40 | class func fixNSSearchFieldFocusLockup() { 41 | /* 42 | 43 | In macOS 11 and macOS 12, tabbing to and from a search field in the toolbar 44 | will cause an endless loop. FB9724872 45 | 46 | This may be fixed in a future release, but for now, we can swizzle the issue 47 | away for another day. 48 | 49 | */ 50 | 51 | let m1 = class_getInstanceMethod(NSClassFromString("NSSearchFieldCell"), NSSelectorFromString("endEditing:")) 52 | let m2 = class_getInstanceMethod(NSClassFromString("NSSearchFieldCell"), NSSelectorFromString("CAT_endEditing:")) 53 | 54 | if let m1 = m1, let m2 = m2 { 55 | method_exchangeImplementations(m1, m2) 56 | } 57 | } 58 | 59 | class func loadAppKitIntegrationFramework() { 60 | 61 | if let frameworksPath = Bundle.main.privateFrameworksPath { 62 | let bundlePath = "\(frameworksPath)/AppKitIntegration.framework" 63 | do { 64 | try Bundle(path: bundlePath)?.loadAndReturnError() 65 | 66 | let bundle = Bundle(path: bundlePath)! 67 | NSLog("[APPKIT BUNDLE] Loaded Successfully") 68 | 69 | if let appKitControllerClass = bundle.classNamed("AppKitIntegration.AppKitController") as? NSObject.Type { 70 | appKitController = appKitControllerClass.init() 71 | 72 | NotificationCenter.default.addObserver(appKitController as Any, selector: #selector(_marzipan_setupWindow(_:)), name: NSNotification.Name("UISBHSDidCreateWindowForSceneNotification"), object: nil) 73 | } 74 | } 75 | catch { 76 | NSLog("[APPKIT BUNDLE] Error loading: \(error)") 77 | } 78 | } 79 | } 80 | #endif 81 | 82 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 83 | // Override point for customization after application launch. 84 | 85 | #if targetEnvironment(macCatalyst) 86 | 87 | CATAppDelegate.loadAppKitIntegrationFramework() 88 | CATAppDelegate.fixNSSearchFieldFocusLockup() 89 | 90 | #endif 91 | 92 | return true 93 | } 94 | 95 | } 96 | 97 | -------------------------------------------------------------------------------- /CatalystExample/App/Controllers/CATPreferencesController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import Foundation 8 | 9 | class CATPreferencesController { 10 | static let shared = CATPreferencesController() 11 | 12 | static let debugEnabledKey = "CATDebugEnabled" 13 | 14 | var enableDebugFeatures:Bool = UserDefaults.standard.bool(forKey: debugEnabledKey) 15 | } 16 | -------------------------------------------------------------------------------- /CatalystExample/App/Main Window/CATMainViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import UIKit 8 | 9 | class CATMainViewController: UIViewController, ObservableObject, UISplitViewControllerDelegate { 10 | 11 | let rootSplitViewController = UISplitViewController(style: .tripleColumn) 12 | 13 | public var listViewController:CATItemListViewController? 14 | public let detailViewController = CATItemViewController() 15 | public let sourceListViewController = CATSourceListViewController() 16 | 17 | let source = CATSourceFile(url: Bundle.main.url(forResource: "Example", withExtension: "json")!) 18 | 19 | init() { 20 | 21 | super.init(nibName: nil, bundle: nil) 22 | 23 | listViewController = CATItemListViewController() 24 | 25 | rootSplitViewController.primaryBackgroundStyle = .sidebar 26 | 27 | rootSplitViewController.preferredPrimaryColumnWidth = UIFloat(260) 28 | rootSplitViewController.minimumPrimaryColumnWidth = UIFloat(200) 29 | 30 | rootSplitViewController.delegate = self 31 | rootSplitViewController.preferredDisplayMode = .twoBesideSecondary 32 | rootSplitViewController.modalPresentationStyle = .overFullScreen 33 | 34 | #if targetEnvironment(macCatalyst) 35 | rootSplitViewController.presentsWithGesture = false 36 | #endif 37 | 38 | buildThreeColumnUI() 39 | 40 | addChild(rootSplitViewController) 41 | view.addSubview(rootSplitViewController.view) 42 | 43 | 44 | listViewController?.documentViewController = self 45 | sourceListViewController.documentViewController = self 46 | 47 | listViewController?.source = source 48 | detailViewController.source = source 49 | sourceListViewController.source = source 50 | sourceListViewController.listViewController = listViewController 51 | 52 | traitCollectionDidChange(traitCollection) 53 | } 54 | 55 | 56 | func buildThreeColumnUI() { 57 | let sidebarNC = UINavigationController(rootViewController: sourceListViewController) 58 | 59 | #if targetEnvironment(macCatalyst) 60 | sidebarNC.isNavigationBarHidden = true 61 | #else 62 | sidebarNC.navigationBar.prefersLargeTitles = true 63 | #endif 64 | 65 | let listNC = UINavigationController(rootViewController: listViewController!) 66 | #if targetEnvironment(macCatalyst) 67 | listNC.isNavigationBarHidden = true 68 | listNC.navigationBar.setBackgroundImage(UIImage(), for: .default) 69 | listNC.navigationBar.shadowImage = UIImage() 70 | #else 71 | listNC.navigationBar.prefersLargeTitles = true 72 | #endif 73 | 74 | let detailViewNC = UINavigationController(rootViewController: detailViewController) 75 | #if targetEnvironment(macCatalyst) 76 | detailViewNC.isNavigationBarHidden = true 77 | #else 78 | detailViewNC.navigationBar.setBackgroundImage(UIImage(), for: .default) 79 | detailViewNC.navigationBar.shadowImage = UIImage() 80 | #endif 81 | detailViewNC.isToolbarHidden = true 82 | 83 | rootSplitViewController.viewControllers = [sidebarNC, listNC, detailViewNC] 84 | } 85 | 86 | required init?(coder: NSCoder) { 87 | fatalError("init(coder:) has not been implemented") 88 | } 89 | 90 | // MARK: - Actions 91 | 92 | @objc func newItem(_ sender:Any) { 93 | let item = CATItem() 94 | source.add(item: item) 95 | } 96 | 97 | @objc func newFolder(_ sender:Any) { 98 | source.newFolder() 99 | } 100 | 101 | // MARK: - Layout 102 | 103 | override func viewDidLayoutSubviews() { 104 | rootSplitViewController.view.frame = view.bounds 105 | } 106 | 107 | // MARK: - Disallow rotation on iPhone 108 | 109 | override var supportedInterfaceOrientations: UIInterfaceOrientationMask { 110 | return UIDevice.current.userInterfaceIdiom == .phone ? .portrait : .all 111 | } 112 | 113 | // MARK: - Size Class Transition 114 | 115 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 116 | super.traitCollectionDidChange(previousTraitCollection) 117 | 118 | if traitCollection.horizontalSizeClass == .regular { 119 | buildThreeColumnUI() 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /CatalystExample/App/Main Window/CATWindowSceneDelegate+NSToolbar.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import UIKit 8 | 9 | #if targetEnvironment(macCatalyst) 10 | extension NSToolbarItem.Identifier { 11 | static let newItem = NSToolbarItem.Identifier("com.highcaffeinecontent.catalystexample.new") 12 | static let newFolder = NSToolbarItem.Identifier("com.highcaffeinecontent.catalystexample.newfolder") 13 | static let search = NSToolbarItem.Identifier("com.highcaffeinecontent.catalystexample.search") 14 | } 15 | 16 | extension CATWindowSceneDelegate : NSToolbarDelegate { 17 | 18 | func toolbarIdentifiers() -> [NSToolbarItem.Identifier] { 19 | return [.flexibleSpace, .newFolder, .primarySidebarTrackingSeparatorItemIdentifier, .flexibleSpace, .supplementarySidebarTrackingSeparatorItemIdentifier, .newItem, .flexibleSpace, .search] 20 | } 21 | 22 | func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 23 | return toolbarIdentifiers() 24 | } 25 | 26 | func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 27 | return toolbarIdentifiers() 28 | } 29 | 30 | func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { 31 | 32 | if itemIdentifier == .newItem { 33 | let barItem = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(create(_:))) 34 | let item = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: barItem) 35 | item.accessibilityLabel = NSLocalizedString("ITEM_NONE_SELECTED_NEW_BUTTON", comment: "") 36 | item.toolTip = NSLocalizedString("ITEM_NONE_SELECTED_NEW_BUTTON", comment: "") 37 | 38 | return item 39 | } 40 | else if itemIdentifier == .newFolder { 41 | let barItem = UIBarButtonItem(image: UIImage(systemName: "plus"), style: .plain, target: self, action: #selector(createFolder(_:))) 42 | let item = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: barItem) 43 | item.accessibilityLabel = NSLocalizedString("TOOLBAR_NEW_FOLDER_BUTTON", comment: "") 44 | item.toolTip = NSLocalizedString("TOOLBAR_NEW_FOLDER_BUTTON", comment: "") 45 | 46 | return item 47 | } 48 | else if itemIdentifier == .search { 49 | 50 | if let searchItem = CATAppDelegate.appKitController?.searchToolbarItem(sceneIdentifier:scene?.session.persistentIdentifier ?? UUID().uuidString, itemIdentifier: itemIdentifier, target: self, selector: #selector(search(_:))) { 51 | return searchItem 52 | } 53 | else { 54 | return NSToolbarItem(itemIdentifier: itemIdentifier) 55 | } 56 | } 57 | else { 58 | return NSToolbarItem(itemIdentifier: itemIdentifier) 59 | } 60 | } 61 | } 62 | #endif 63 | -------------------------------------------------------------------------------- /CatalystExample/App/Main Window/CATWindowSceneDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import UIKit 8 | import UniformTypeIdentifiers 9 | 10 | class CATWindowSceneDelegate : NSObject, UISceneDelegate, UIWindowSceneDelegate, UIDocumentPickerDelegate { 11 | 12 | var window: UIWindow? 13 | weak var scene:UIScene? 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | window = UIWindow(windowScene: scene as! UIWindowScene) 17 | self.scene = scene 18 | 19 | if let window = window { 20 | let rootViewController = CATMainViewController() 21 | window.rootViewController = rootViewController 22 | 23 | window.backgroundColor = .systemBackground 24 | 25 | reloadToolbar() 26 | 27 | window.makeKeyAndVisible() 28 | 29 | } 30 | } 31 | 32 | // MARK: - Toolbar Actions 33 | 34 | @objc func create(_ sender:Any) { 35 | if let documentViewController = window?.rootViewController as? CATMainViewController { 36 | documentViewController.newItem(sender) 37 | } 38 | } 39 | 40 | @objc func createFolder(_ sender:Any) { 41 | if let documentViewController = window?.rootViewController as? CATMainViewController { 42 | documentViewController.newFolder(sender) 43 | } 44 | } 45 | 46 | @objc func search(_ sender:Any) { 47 | if let nsSender = sender as? NSObject { 48 | if let string = nsSender.value(forKey: "stringValue") as? NSString { 49 | NSLog("[SEARCH]: \(string)") 50 | 51 | if let documentViewController = window?.rootViewController as? CATMainViewController { 52 | documentViewController.source.searchString = string as String 53 | } 54 | } 55 | } 56 | } 57 | 58 | // MARK: - Mac Toolbar 59 | 60 | func reloadToolbar() { 61 | #if targetEnvironment(macCatalyst) 62 | let toolbar = NSToolbar() 63 | toolbar.delegate = self 64 | toolbar.displayMode = .iconOnly 65 | 66 | window?.windowScene?.titlebar?.toolbar = toolbar 67 | window?.windowScene?.titlebar?.toolbarStyle = .automatic 68 | #endif 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /CatalystExample/CatalystExample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.com.highcaffeinecontent.Shared 10 | 11 | com.apple.security.files.user-selected.read-write 12 | 13 | com.apple.security.network.client 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /CatalystExample/Common.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import Foundation 8 | 9 | func CATPrettyFormatDate(_ date:Date) -> String { 10 | return CATPrettyFormatDate(date, dateStyle: .long, timeStyle: .short) 11 | } 12 | 13 | func CATPrettyFormatDate(_ date:Date, dateStyle:DateFormatter.Style, timeStyle:DateFormatter.Style) -> String { 14 | let formatter = DateFormatter() 15 | 16 | formatter.locale = Locale.current 17 | formatter.dateStyle = dateStyle 18 | formatter.timeStyle = timeStyle 19 | 20 | return formatter.string(from: date) 21 | } 22 | -------------------------------------------------------------------------------- /CatalystExample/Content View/CATItemViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import UIKit 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | @ObservedObject var item:CATItem = CATItem() 12 | 13 | var parent:CATItemViewController 14 | 15 | var body: some View { 16 | Text("MAIN_VIEW_HELLO_WORLD_LABEL") 17 | 18 | } 19 | } 20 | 21 | class CATItemViewController : UIViewController, ObservableObject { 22 | 23 | var hostingController:UIHostingController? 24 | var documentViewController:CATMainViewController? 25 | var source:CATSourceFile? 26 | 27 | override func viewDidLoad() { 28 | view.backgroundColor = .systemBackground 29 | } 30 | 31 | init() { 32 | super.init(nibName: nil, bundle: nil) 33 | 34 | hostingController = UIHostingController(rootView: ContentView(parent: self)) 35 | navigationItem.largeTitleDisplayMode = .never 36 | 37 | if let hostingController = hostingController { 38 | view.addSubview(hostingController.view) 39 | } 40 | 41 | if #available(macCatalyst 15.0, iOS 15.0, *) { 42 | focusGroupIdentifier = "com.example.details" 43 | } 44 | } 45 | 46 | // MARK: - 47 | 48 | @objc required dynamic init?(coder aDecoder: NSCoder) { 49 | fatalError("init(coder:) has not been implemented") 50 | } 51 | 52 | override func viewDidLayoutSubviews() { 53 | if let hostingController = hostingController { 54 | hostingController.view.frame = view.bounds.inset(by: view.safeAreaInsets) 55 | } 56 | } 57 | 58 | // MARK: - Keyboard Focus 59 | 60 | override func shouldUpdateFocus(in context: UIFocusUpdateContext) -> Bool { 61 | return false 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /CatalystExample/Data Model/CATElement.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import Foundation 8 | import MobileCoreServices 9 | 10 | class CATElementWrapper: NSObject, NSItemProviderWriting { 11 | static var writableTypeIdentifiersForItemProvider: [String] { return ["public.string"] } 12 | 13 | var element:CATElement 14 | 15 | init(element:CATElement) { 16 | self.element = element 17 | super.init() 18 | } 19 | 20 | // MARK: - 21 | 22 | func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? { 23 | 24 | let encoder = JSONEncoder() 25 | encoder.outputFormatting = .prettyPrinted 26 | 27 | let jsonData = try! encoder.encode(element) 28 | 29 | completionHandler(jsonData, nil) 30 | 31 | return nil 32 | } 33 | 34 | } 35 | 36 | class CATElement: Codable, Hashable { 37 | var identifier = UUID().uuidString 38 | 39 | init() { 40 | 41 | } 42 | 43 | // MARK: - Hashable 44 | 45 | static func == (lhs: CATElement, rhs: CATElement) -> Bool { 46 | if (lhs.identifier == rhs.identifier) { 47 | return true 48 | } 49 | 50 | return false 51 | } 52 | 53 | func hash(into hasher: inout Hasher) { 54 | hasher.combine(self) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /CatalystExample/Data Model/CATFolder.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import Foundation 8 | 9 | class CATFolder: CATElement, ObservableObject { 10 | 11 | @Published var title = NSLocalizedString("FOLDER_UNTITLED", comment: "") 12 | var itemIDs = Array() 13 | 14 | override init() { 15 | super.init() 16 | } 17 | 18 | // MARK: - Codable 19 | 20 | required init(from decoder: Decoder) throws { 21 | super.init() 22 | 23 | let container = try decoder.container(keyedBy: CodingKeys.self) 24 | 25 | identifier = (try? container.decode(String.self, forKey: .identifier)) ?? UUID().uuidString 26 | title = (try? container.decode(String.self, forKey: .title)) ?? NSLocalizedString("FOLDER_UNTITLED", comment: "") 27 | itemIDs = (try? container.decode(Array.self, forKey: .itemIDs)) ?? Array() 28 | 29 | } 30 | 31 | override func encode(to encoder: Encoder) throws { 32 | 33 | var container = encoder.container(keyedBy: CodingKeys.self) 34 | 35 | try container.encode(identifier, forKey: .identifier) 36 | try container.encode(title, forKey: .title) 37 | try container.encode(itemIDs, forKey: .itemIDs) 38 | } 39 | 40 | enum CodingKeys: String, CodingKey { 41 | case identifier 42 | case title 43 | case itemIDs 44 | } 45 | 46 | // MARK: - Hashable 47 | 48 | static func == (lhs: CATFolder, rhs: CATFolder) -> Bool { 49 | if (lhs.identifier == rhs.identifier) && (lhs.title == rhs.title) && (rhs.itemIDs == lhs.itemIDs) { 50 | return true 51 | } 52 | 53 | return false 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /CatalystExample/Data Model/CATItem.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import Foundation 8 | 9 | class CATItem: CATElement, ObservableObject { 10 | 11 | var title = NSLocalizedString("ITEM_TITLE_UNTITLED", comment: "") 12 | var body = "Lorem ipsum dolor sit amet" 13 | var created = Date() 14 | 15 | override init() { 16 | super.init() 17 | } 18 | 19 | // MARK: - Codable 20 | 21 | required init(from decoder: Decoder) throws { 22 | super.init() 23 | 24 | let container = try decoder.container(keyedBy: CodingKeys.self) 25 | 26 | identifier = try container.decode(String.self, forKey: .identifier) 27 | title = (try? container.decode(String.self, forKey: .title)) ?? NSLocalizedString("ITEM_TITLE_UNTITLED", comment: "") 28 | body = (try? container.decode(String.self, forKey: .body)) ?? "" 29 | 30 | created = (try? container.decode(Date.self, forKey: .created)) ?? Date() 31 | } 32 | 33 | override func encode(to encoder: Encoder) throws { 34 | 35 | var container = encoder.container(keyedBy: CodingKeys.self) 36 | 37 | try container.encode(identifier, forKey: .identifier) 38 | try container.encode(title, forKey: .title) 39 | try container.encode(body, forKey: .body) 40 | 41 | try container.encode(created, forKey: .created) 42 | } 43 | 44 | enum CodingKeys: String, CodingKey { 45 | case identifier 46 | case title 47 | case body 48 | 49 | case created 50 | } 51 | 52 | // MARK: - Search 53 | 54 | func search(searchString:String) -> Bool { 55 | 56 | let searchString = searchString.lowercased() 57 | 58 | if title.lowercased().contains(searchString) || body.lowercased().contains(searchString) { 59 | return true 60 | } 61 | 62 | return false 63 | } 64 | 65 | // MARK: - Hashable 66 | 67 | static func == (lhs: CATItem, rhs: CATItem) -> Bool { 68 | if (lhs.identifier == rhs.identifier) && (lhs.title == rhs.title) && (rhs.body == lhs.body) && (rhs.created == lhs.created) { 69 | return true 70 | } 71 | 72 | return false 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /CatalystExample/Data Model/CATSourceFile.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import UIKit 8 | 9 | extension Notification.Name { 10 | static let documentChanged = NSNotification.Name("CATSourceFile.modified") 11 | } 12 | 13 | class CATDataStore: Codable { 14 | 15 | var items = Array() 16 | var folders = Array() 17 | 18 | init() { 19 | 20 | } 21 | 22 | required init(from decoder: Decoder) throws { 23 | let container = try decoder.container(keyedBy: CodingKeys.self) 24 | 25 | items = (try? container.decode(Array.self, forKey: .items)) ?? Array() 26 | folders = (try? container.decode(Array.self, forKey: .folders)) ?? Array() 27 | } 28 | 29 | func encode(to encoder: Encoder) throws { 30 | 31 | var container = encoder.container(keyedBy: CodingKeys.self) 32 | 33 | try container.encode(items, forKey: .items) 34 | try container.encode(folders, forKey: .folders) 35 | } 36 | 37 | enum CodingKeys: String, CodingKey { 38 | case items 39 | case folders 40 | } 41 | } 42 | 43 | class CATSourceFile { 44 | 45 | var dataStore = CATDataStore() 46 | 47 | var searchString = "" { 48 | didSet { 49 | changed() 50 | } 51 | } 52 | 53 | static let CATFolderIndexAll = -1 54 | static let CATFolderIndexRecents = -2 55 | 56 | // MARK: - Load & Save the data store 57 | 58 | convenience init(url: URL) { 59 | self.init() 60 | 61 | do { 62 | let data = try Data(contentsOf: url) 63 | try load(fromData: data) 64 | } 65 | catch { 66 | NSLog("[LOAD] Error: \(error.localizedDescription)") 67 | } 68 | 69 | } 70 | 71 | func load(fromData data: Data) throws { 72 | let decoder = JSONDecoder() 73 | 74 | do { 75 | let decodedDataStore = try decoder.decode(CATDataStore.self, from: data) 76 | dataStore = decodedDataStore 77 | 78 | print("[ARCHIVE] Opened successfully") 79 | 80 | } catch { 81 | print("[ARCHIVE] Fatal: \(error.localizedDescription)") 82 | } 83 | } 84 | 85 | func save(to url: URL, completionHandler: ((Bool) -> Void)? = nil) { 86 | 87 | let _ = url.startAccessingSecurityScopedResource() 88 | 89 | let encoder = JSONEncoder() 90 | encoder.outputFormatting = .prettyPrinted 91 | 92 | let jsonData = try! encoder.encode(dataStore) 93 | 94 | do { 95 | try jsonData.write(to: url, options: .atomic) 96 | 97 | if let completionHandler = completionHandler { 98 | completionHandler(true) 99 | } 100 | } 101 | catch { 102 | print("[ARCHIVE] Error \(error)") 103 | 104 | if let completionHandler = completionHandler { 105 | completionHandler(false) 106 | } 107 | } 108 | } 109 | } 110 | 111 | extension CATSourceFile { 112 | // MARK: - Managing the data store 113 | 114 | func add(item:CATItem) { 115 | dataStore.items.append(item) 116 | 117 | changed() 118 | } 119 | 120 | func removeItem(withIdentifier:String) { 121 | dataStore.items.removeAll { potentialItem in 122 | return (potentialItem.identifier == withIdentifier) 123 | } 124 | 125 | changed() 126 | } 127 | 128 | func reorderFolder(at:UInt, to:UInt) { 129 | 130 | let target = dataStore.folders[Int(at)] 131 | dataStore.folders.remove(at: Int(at)) 132 | dataStore.folders.insert(target, at: Int(to)) 133 | 134 | changed() 135 | } 136 | 137 | func newFolder() { 138 | dataStore.folders.append(CATFolder()) 139 | 140 | changed() 141 | } 142 | 143 | // MARK: - Change Notifications 144 | 145 | func changed() { 146 | NotificationCenter.default.post(name: .documentChanged, object: nil) 147 | } 148 | 149 | // MARK: - Sorted Accessor 150 | 151 | func sortedItems(tagIndex:Int) -> Array { 152 | var itemsSorted = [CATItem]() 153 | 154 | if tagIndex >= dataStore.folders.count { 155 | return itemsSorted 156 | } 157 | 158 | if tagIndex == CATSourceFile.CATFolderIndexAll { 159 | 160 | for item in dataStore.items { 161 | 162 | if searchString != "" { 163 | if item.search(searchString: searchString) == false { 164 | continue 165 | } 166 | } 167 | 168 | itemsSorted.append(item) 169 | } 170 | } 171 | else if tagIndex == CATSourceFile.CATFolderIndexRecents { 172 | 173 | let oneDay = TimeInterval(60 * 60 * 24) 174 | 175 | for item in dataStore.items { 176 | if Date.timeIntervalSinceReferenceDate - item.created.timeIntervalSinceReferenceDate < (oneDay * 3) { 177 | 178 | if searchString != "" { 179 | if (item.title.lowercased().contains(searchString.lowercased()) == false) && (item.body.lowercased().contains(searchString.lowercased()) == false) { 180 | continue 181 | } 182 | } 183 | 184 | itemsSorted.append(item) 185 | } 186 | } 187 | } 188 | else { 189 | 190 | 191 | let folder = dataStore.folders[tagIndex] 192 | 193 | for item in dataStore.items { 194 | if folder.itemIDs.contains(item.identifier) { 195 | 196 | if searchString != "" { 197 | if (item.title.lowercased().contains(searchString.lowercased()) == false) && (item.body.lowercased().contains(searchString.lowercased()) == false) { 198 | continue 199 | } 200 | } 201 | 202 | itemsSorted.append(item) 203 | } 204 | } 205 | } 206 | 207 | itemsSorted.sort { A, B in 208 | return (A.created.compare(B.created) == .orderedDescending) 209 | } 210 | 211 | return itemsSorted 212 | } 213 | } 214 | 215 | -------------------------------------------------------------------------------- /CatalystExample/Middle Column/CATItemListViewCell.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import UIKit 8 | 9 | class CATItemListViewCell: UICollectionViewCell { 10 | 11 | var inactive = false 12 | 13 | var titleLabel = UILabel() 14 | var bodyLabel = UILabel() 15 | 16 | var modifiedLabel = UILabel() 17 | 18 | var stateAreaWidth = CGFloat.zero 19 | 20 | let focusRingView = UIView() 21 | let selectionRingView = UIView() 22 | 23 | override var isSelected: Bool { 24 | didSet { 25 | if isSelected == true { 26 | selectionRingView.alpha = 1 27 | } 28 | else { 29 | selectionRingView.alpha = 0 30 | } 31 | 32 | recolor() 33 | } 34 | } 35 | 36 | override var isHighlighted: Bool { 37 | didSet { 38 | recolor() 39 | } 40 | } 41 | 42 | var showFocusRing: Bool = false { 43 | didSet { 44 | focusRingView.isHidden = !showFocusRing 45 | 46 | recolor() 47 | } 48 | } 49 | 50 | // MARK: - 51 | 52 | 53 | override func prepareForReuse() { 54 | isSelected = false 55 | isHighlighted = false 56 | showFocusRing = false 57 | } 58 | 59 | func recolor() { 60 | 61 | var showWhiteLabels = false 62 | 63 | if (isSelected && !inactive) || showFocusRing { 64 | showWhiteLabels = true 65 | } 66 | 67 | if (!showFocusRing && isSelected) || isHighlighted { 68 | showWhiteLabels = false 69 | } 70 | 71 | if showWhiteLabels == true { 72 | titleLabel.textColor = .white 73 | bodyLabel.textColor = .white 74 | modifiedLabel.textColor = .white 75 | } 76 | else { 77 | titleLabel.textColor = .label 78 | bodyLabel.textColor = .secondaryLabel 79 | modifiedLabel.textColor = .secondaryLabel 80 | } 81 | } 82 | 83 | override init(frame: CGRect) { 84 | super.init(frame: frame) 85 | 86 | focusRingView.layer.cornerRadius = UIFloat(8) 87 | focusRingView.layer.cornerCurve = .continuous 88 | 89 | selectionRingView.layer.cornerRadius = UIFloat(8) 90 | selectionRingView.layer.cornerCurve = .continuous 91 | 92 | titleLabel.font = UIFont.boldSystemFont(ofSize: UIFloat(18)) 93 | bodyLabel.font = UIFont.systemFont(ofSize: UIFloat(16)) 94 | 95 | modifiedLabel.font = UIFont.systemFont(ofSize: UIFloat(15)) 96 | 97 | modifiedLabel.textAlignment = .right 98 | 99 | stateAreaWidth = ("31/12/2021" as NSString).size(withAttributes: [.font : UIFont.systemFont(ofSize: UIFloat(15))]).width + UIFloat(13) 100 | 101 | recolor() 102 | 103 | selectionRingView.alpha = 0 104 | selectionRingView.backgroundColor = .systemFill 105 | addSubview(selectionRingView) 106 | 107 | focusRingView.isHidden = true 108 | focusRingView.backgroundColor = tintColor 109 | addSubview(focusRingView) 110 | 111 | contentView.addSubview(titleLabel) 112 | contentView.addSubview(bodyLabel) 113 | contentView.addSubview(modifiedLabel) 114 | 115 | if #available(macCatalyst 15.0, iOS 15.0, *) { 116 | focusEffect = nil 117 | } 118 | } 119 | 120 | required init?(coder: NSCoder) { 121 | fatalError("init(coder:) has not been implemented") 122 | } 123 | 124 | // MARK: - 125 | 126 | override func layoutSubviews() { 127 | 128 | let padding = UIFloat(13) 129 | let insetFrame = bounds.insetBy(dx: padding, dy: 0) 130 | 131 | contentView.frame = insetFrame.insetBy(dx: padding, dy: 0) 132 | 133 | selectionRingView.frame = bounds.insetBy(dx: padding, dy: 0) 134 | focusRingView.frame = selectionRingView.frame 135 | 136 | /* */ 137 | 138 | let contentRegion = contentView.bounds.divided(atDistance: stateAreaWidth, from: .maxXEdge) 139 | let rightRegion = contentRegion.slice.inset(by: UIEdgeInsets(top: padding, left: 0, bottom: padding, right: 0)) 140 | let leftRegion = contentRegion.remainder.inset(by: UIEdgeInsets(top: padding, left: padding, bottom: padding, right: 0)) 141 | 142 | let leftLabelRegion = leftRegion.divided(atDistance: leftRegion.height/2, from: .minYEdge) 143 | let rightLabelRegion = rightRegion.divided(atDistance: rightRegion.height/2, from: .minYEdge) 144 | 145 | titleLabel.frame = leftLabelRegion.slice 146 | bodyLabel.frame = leftLabelRegion.remainder 147 | 148 | modifiedLabel.frame = rightLabelRegion.slice 149 | } 150 | 151 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 152 | inactive = (traitCollection.activeAppearance == .inactive) 153 | recolor() 154 | } 155 | 156 | // MARK: - Keyboard Focus 157 | 158 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 159 | 160 | super.didUpdateFocus(in: context, with: coordinator) 161 | 162 | if context.nextFocusedItem === self { 163 | showFocusRing = true 164 | } 165 | else if context.previouslyFocusedItem === self { 166 | showFocusRing = false 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /CatalystExample/Middle Column/CATItemListViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import UIKit 8 | 9 | class CATItemListViewController: UICollectionViewController { 10 | 11 | static let cellIdentifier = "Cell" 12 | var diffableDataSource:UICollectionViewDiffableDataSource? 13 | var selectedIdentifier = UUID().uuidString 14 | 15 | var documentViewController:CATMainViewController? { 16 | didSet { 17 | reload() 18 | } 19 | } 20 | 21 | var source:CATSourceFile? { 22 | didSet { 23 | reload() 24 | } 25 | } 26 | 27 | var currentFolderIndex = CATSourceFile.CATFolderIndexAll { 28 | didSet { 29 | 30 | if currentFolderIndex == CATSourceFile.CATFolderIndexAll { 31 | title = NSLocalizedString("SIDEBAR_ALL_ITEMS", comment: "") 32 | } 33 | else if currentFolderIndex == CATSourceFile.CATFolderIndexRecents { 34 | title = NSLocalizedString("SIDEBAR_RECENTS", comment: "") 35 | } 36 | else { 37 | title = source?.dataStore.folders[currentFolderIndex].title 38 | } 39 | 40 | DispatchQueue.main.async { 41 | self.reload() 42 | } 43 | } 44 | } 45 | 46 | // MARK: - 47 | 48 | init() { 49 | 50 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 51 | heightDimension: .absolute(UIFloat(80))) 52 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 53 | 54 | let group = NSCollectionLayoutGroup.vertical(layoutSize: itemSize, 55 | subitems: [item]) 56 | 57 | let section = NSCollectionLayoutSection(group: group) 58 | let layout = UICollectionViewCompositionalLayout(section: section) 59 | 60 | super.init(collectionViewLayout: layout) 61 | 62 | diffableDataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, ctx in 63 | 64 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CATItemListViewController.cellIdentifier, for: indexPath) as! CATItemListViewCell 65 | 66 | if let items = self.source?.sortedItems(tagIndex: self.currentFolderIndex) { 67 | let item = items[indexPath.item] 68 | 69 | cell.titleLabel.text = item.title 70 | cell.bodyLabel.text = item.body 71 | cell.modifiedLabel.text = CATPrettyFormatDate(item.created, dateStyle: .short, timeStyle: .none) 72 | } 73 | 74 | return cell 75 | }) 76 | 77 | #if !targetEnvironment(macCatalyst) 78 | collectionView.backgroundColor = .systemGroupedBackground 79 | #endif 80 | 81 | collectionView.backgroundColor = .systemBackground 82 | 83 | collectionView.register(CATItemListViewCell.self, forCellWithReuseIdentifier: CATItemListViewController.cellIdentifier) 84 | collectionView.dataSource = diffableDataSource 85 | collectionView.delegate = self 86 | 87 | if #available(iOS 15.0, macOS 15.0, *) { 88 | collectionView.allowsFocus = true 89 | } 90 | 91 | collectionView.contentInset.top = UIFloat(13) 92 | 93 | NotificationCenter.default.addObserver(forName: .documentChanged, object: nil, queue: nil) { [weak self] _ in 94 | DispatchQueue.main.async { 95 | 96 | 97 | if self!.currentFolderIndex >= 0 { 98 | if let count = self!.source?.dataStore.folders.count { 99 | 100 | if self!.currentFolderIndex < count { 101 | if let tag = self!.source?.dataStore.folders[self!.currentFolderIndex] { 102 | self!.title = tag.title 103 | } 104 | } 105 | else { 106 | self!.title = "" 107 | } 108 | } 109 | } 110 | 111 | self!.reload() 112 | } 113 | } 114 | 115 | NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil) 116 | NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) 117 | 118 | #if !targetEnvironment(macCatalyst) 119 | let searchController = UISearchController() 120 | 121 | searchController.searchResultsUpdater = self 122 | searchController.definesPresentationContext = true 123 | searchController.delegate = self 124 | searchController.obscuresBackgroundDuringPresentation = false 125 | 126 | navigationItem.searchController = searchController 127 | #endif 128 | } 129 | 130 | required init?(coder: NSCoder) { 131 | fatalError("init(coder:) has not been implemented") 132 | } 133 | 134 | override func viewDidLoad() { 135 | super.viewDidLoad() 136 | 137 | self.clearsSelectionOnViewWillAppear = true 138 | } 139 | 140 | override func viewWillAppear(_ animated: Bool) { 141 | 142 | let barItem = UIBarButtonItem(barButtonSystemItem: .compose, target: documentViewController, action: NSSelectorFromString("newItem:")) 143 | 144 | if UIDevice.current.userInterfaceIdiom != .phone { 145 | navigationItem.title = NSLocalizedString("SIDEBAR_ALL_ITEMS", comment: "") 146 | } 147 | navigationItem.rightBarButtonItem = barItem 148 | 149 | 150 | } 151 | 152 | // MARK: - Keyboard Avoidance 153 | 154 | @objc func adjustForKeyboard(notification: Notification) { 155 | guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } 156 | 157 | let keyboardScreenEndFrame = keyboardValue.cgRectValue 158 | let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window) 159 | 160 | if notification.name == UIResponder.keyboardWillHideNotification { 161 | collectionView.contentInset = .zero 162 | } else { 163 | collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardViewEndFrame.height - view.safeAreaInsets.bottom, right: 0) 164 | } 165 | 166 | collectionView.scrollIndicatorInsets = collectionView.contentInset 167 | } 168 | 169 | // MARK: - Diffable Data Source 170 | 171 | func reload() { 172 | 173 | var snapshot = NSDiffableDataSourceSnapshot() 174 | 175 | snapshot.appendSections(["items"]) 176 | 177 | if let items = source?.sortedItems(tagIndex: currentFolderIndex) { 178 | snapshot.appendItems(items) 179 | } 180 | 181 | guard let diffableDataSource = diffableDataSource else { return } 182 | 183 | diffableDataSource.apply(snapshot, animatingDifferences: false, completion: { [weak self] in 184 | self!.selectItemWithIdentifier(self!.selectedIdentifier) 185 | }) 186 | } 187 | 188 | func selectItemWithIdentifier(_ identifier:String) { 189 | 190 | var intendedIndexPath = IndexPath(item: 0, section: 0) 191 | var found = false 192 | 193 | if let items = source?.sortedItems(tagIndex: self.currentFolderIndex) { 194 | for i in 0 ..< items.count { 195 | if items[i].identifier == identifier { 196 | intendedIndexPath.item = i 197 | found = true 198 | break 199 | } 200 | } 201 | } 202 | 203 | if found == true { 204 | guard intendedIndexPath.section < collectionView.numberOfSections else { return } 205 | guard intendedIndexPath.item < collectionView.numberOfItems(inSection: intendedIndexPath.section) else { return } 206 | 207 | collectionView.selectItem(at: intendedIndexPath, animated: false, scrollPosition: []) 208 | } 209 | } 210 | 211 | func pushItemWithIdentifier(_ identifier:String) { 212 | 213 | var intendedIndexPath = IndexPath(item: 0, section: 0) 214 | var found = false 215 | 216 | if let items = source?.sortedItems(tagIndex: self.currentFolderIndex) { 217 | for i in 0 ..< items.count { 218 | if items[i].identifier == identifier { 219 | intendedIndexPath.item = i 220 | found = true 221 | break 222 | } 223 | } 224 | } 225 | 226 | if found == true { 227 | guard intendedIndexPath.section < collectionView.numberOfSections else { return } 228 | guard intendedIndexPath.item < collectionView.numberOfItems(inSection: intendedIndexPath.section) else { return } 229 | 230 | collectionView(collectionView, didSelectItemAt: intendedIndexPath) 231 | } 232 | } 233 | 234 | // MARK: - List View 235 | 236 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 237 | if let source = source { 238 | let sortedItems = source.sortedItems(tagIndex: self.currentFolderIndex) 239 | if indexPath.item < sortedItems.count { 240 | let item = sortedItems[indexPath.item] 241 | selectedIdentifier = item.identifier 242 | 243 | guard let detailViewController = documentViewController?.detailViewController else { return } 244 | detailViewController.source = source 245 | 246 | if UIDevice.current.userInterfaceIdiom == .phone || view.window?.traitCollection.horizontalSizeClass == .compact { 247 | navigationController?.pushViewController(detailViewController, animated: true) 248 | } 249 | } 250 | } 251 | } 252 | 253 | override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 254 | cell.selectedBackgroundView = nil 255 | } 256 | 257 | // MARK: - Contextual Menus 258 | 259 | override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { 260 | 261 | let config = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { elementArray -> UIMenu? in 262 | return self.contextMenuForIndexPath(indexPath: indexPath) 263 | } 264 | 265 | return config 266 | } 267 | 268 | func contextMenuForIndexPath(indexPath:IndexPath) -> UIMenu { 269 | let symcfg = UIImage.SymbolConfiguration(pointSize: UIFloat(18), weight: .bold) 270 | 271 | var deleteActionsCommand = Array() 272 | 273 | do { 274 | let action = UIAction(title: NSLocalizedString("CONTEXT_DELETE", comment:""), image: UIImage(systemName: "trash.fill", withConfiguration: symcfg), identifier: nil) { (UIAction) in 275 | 276 | if let source = self.source { 277 | if let sortedItems = self.source?.sortedItems(tagIndex: self.currentFolderIndex) { 278 | 279 | if indexPath.item < sortedItems.count { 280 | let item = sortedItems[(indexPath.item)] 281 | let identifier = item.identifier 282 | 283 | source.removeItem(withIdentifier: identifier) 284 | } 285 | } 286 | } 287 | 288 | } 289 | action.attributes = .destructive 290 | deleteActionsCommand.append(action) 291 | } 292 | 293 | 294 | let deleteMenu = UIMenu(title: "", image: nil, identifier: UIMenu.Identifier(rawValue: "SIDEBAR_DELETE"), options: .displayInline, children: deleteActionsCommand) 295 | 296 | return UIMenu(title: "", image: nil, identifier: nil, options: [], children: [deleteMenu]) 297 | } 298 | 299 | // MARK: - Keyboard Focus 300 | 301 | override func collectionView(_ collectionView: UICollectionView, selectionFollowsFocusForItemAt indexPath: IndexPath) -> Bool { 302 | return UIDevice.current.userInterfaceIdiom == .mac || view.window?.traitCollection.horizontalSizeClass == .regular 303 | } 304 | } 305 | 306 | extension CATItemListViewController: UISearchResultsUpdating, UISearchControllerDelegate { 307 | func updateSearchResults(for searchController: UISearchController) { 308 | if let source = self.source { 309 | source.searchString = searchController.searchBar.text ?? "" 310 | } 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /CatalystExample/Number+Scaling.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | /* 8 | 9 | Number+Scaling 10 | 11 | This function lets you wrap all UI-facing metrics in UIFloat() 12 | so that they will scale appropriately for iOS and macOS idioms 13 | without you having to have multiple metrics 14 | 15 | */ 16 | import UIKit 17 | 18 | public let supportsMacIdiom = !(UIDevice.current.userInterfaceIdiom == .pad) 19 | 20 | @inlinable func UIFloat(_ value: CGFloat) -> CGFloat 21 | { 22 | #if targetEnvironment(macCatalyst) 23 | return round((value == 0.5) ? 0.5 : value * (supportsMacIdiom ? 0.77 : 1.0)) 24 | #else 25 | return value 26 | #endif 27 | } 28 | -------------------------------------------------------------------------------- /CatalystExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "ios", 6 | "reference" : "systemBlueColor" 7 | }, 8 | "idiom" : "universal" 9 | } 10 | ], 11 | "info" : { 12 | "author" : "xcode", 13 | "version" : 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /CatalystExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /CatalystExample/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CatalystExample/Resources/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /CatalystExample/Resources/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2580 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica-Bold;\f1\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | \paperw12240\paperh15840\margl1440\margr1440\vieww9000\viewh8400\viewkind0 6 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\partightenfactor0 7 | 8 | \f0\b\fs24 \cf0 Engineering: 9 | \f1\b0 \ 10 | Some people\ 11 | \ 12 | 13 | \f0\b Human Interface Design: 14 | \f1\b0 \ 15 | Some other people\ 16 | \ 17 | 18 | \f0\b Testing: 19 | \f1\b0 \ 20 | Hopefully not nobody\ 21 | \ 22 | 23 | \f0\b Documentation: 24 | \f1\b0 \ 25 | Whoever\ 26 | \ 27 | 28 | \f0\b With special thanks to: 29 | \f1\b0 \ 30 | Mom\ 31 | } -------------------------------------------------------------------------------- /CatalystExample/Resources/Example.json: -------------------------------------------------------------------------------- 1 | { 2 | "folders" : [ 3 | { 4 | "title": "Folder A", 5 | "itemIDs" : [ 6 | "2E6F2460-4B99-4287-8B42-C99991023E21" 7 | ] 8 | }, 9 | { 10 | "title": "Folder B", 11 | "itemIDs" : [ 12 | "2E6F2460-4B99-4287-8B42-C99991023E22" 13 | ] 14 | } 15 | ], 16 | "items" : [ 17 | { 18 | "number" : 1, 19 | "closed" : false, 20 | "title" : "First", 21 | "created" : 544025779.74363601, 22 | "identifier" : "2E6F2460-4B99-4287-8B42-C99991023E21", 23 | "modified" : 644463201.65309298, 24 | "body" : "Lorem ipsum dolor sit amet", 25 | "due" : 644025779.74363601 26 | }, 27 | { 28 | "number" : 2, 29 | "closed" : false, 30 | "title" : "Second", 31 | "created" : 644025779.74363601, 32 | "identifier" : "2E6F2460-4B99-4287-8B42-C99991023E22", 33 | "modified" : 644463201.65309298, 34 | "body" : "Lorem ipsum dolor sit amet", 35 | "due" : 644025779.74363601 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /CatalystExample/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | CatalystExample 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSRequiresIPhoneOS 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | UISceneConfigurations 30 | 31 | UIWindowSceneSessionRoleApplication 32 | 33 | 34 | UISceneConfigurationName 35 | Main 36 | UISceneDelegateClassName 37 | $(PRODUCT_MODULE_NAME).CATWindowSceneDelegate 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /CatalystExample/Resources/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | "SIDEBAR_TITLE_LIBRARY" = "Library"; 8 | "SIDEBAR_ALL_ITEMS" = "All Items"; 9 | "SIDEBAR_RECENTS" = "Recent Items"; 10 | 11 | "SIDEBAR_TITLE_FOLDERS" = "Folders"; 12 | 13 | "ITEM_NONE_SELECTED_NEW_BUTTON" = "New Item"; 14 | "ITEM_TITLE_UNTITLED" = "New Item"; 15 | 16 | "SOURCE_VIEW_TITLE" = "Example"; 17 | 18 | "MAIN_VIEW_HELLO_WORLD_LABEL" = "Hello World!"; 19 | "MAIN_VIEW_DO_THING_BUTTON" = "Do thing"; 20 | 21 | "TOOLBAR_SEARCH_PLACEHOLDER" = "Search Items"; 22 | 23 | "CONTEXT_DELETE" = "Delete Item"; 24 | 25 | "MENU_ITEM" = "Item"; 26 | "MENU_ITEM_NEW_ITEM" = "New Item"; 27 | 28 | "TOOLBAR_NEW_FOLDER_BUTTON" = "New Folder"; 29 | "FOLDER_UNTITLED" = "New Folder"; 30 | -------------------------------------------------------------------------------- /CatalystExample/Sidebar/CATSourceListViewController+Reordering.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import UIKit 8 | 9 | extension CATSourceListViewController: UICollectionViewDragDelegate, UICollectionViewDropDelegate { 10 | 11 | func shouldMoveItemAt(indexPath: IndexPath) -> Bool { 12 | 13 | if indexPath.isEmpty { 14 | return false 15 | } 16 | 17 | if indexPath.section == 1 && indexPath.item > 0 { 18 | return true 19 | } 20 | 21 | return false 22 | } 23 | 24 | 25 | override func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool { 26 | return shouldMoveItemAt(indexPath: indexPath) 27 | } 28 | 29 | func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { 30 | 31 | if shouldMoveItemAt(indexPath: indexPath) == false { 32 | return [] 33 | } 34 | 35 | if let folders = source?.dataStore.folders { 36 | let folder = folders[(indexPath.item - 1)] 37 | let context = CATElementWrapper(element: folder) 38 | 39 | let provider = NSItemProvider(object: context) 40 | let item = UIDragItem(itemProvider: provider) 41 | 42 | session.localContext = context 43 | return [item] 44 | } 45 | 46 | return [] 47 | } 48 | 49 | func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool { 50 | return (session.localDragSession?.localContext != nil) 51 | } 52 | 53 | 54 | func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal { 55 | 56 | if let destinationIndexPath = destinationIndexPath { 57 | if !shouldMoveItemAt(indexPath: destinationIndexPath) { 58 | return UICollectionViewDropProposal(operation:.cancel, intent: .unspecified) 59 | } 60 | } 61 | 62 | if session.localDragSession?.localContext != nil { 63 | 64 | if let wrapper = session.localDragSession?.localContext as? CATElementWrapper { 65 | 66 | if let _ = wrapper.element as? CATFolder { 67 | 68 | } 69 | else if let _ = wrapper.element as? CATItem { 70 | return UICollectionViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath) 71 | } 72 | } 73 | } 74 | 75 | return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) 76 | } 77 | 78 | override func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath { 79 | 80 | if proposedIndexPath.isEmpty { 81 | return proposedIndexPath 82 | } 83 | 84 | if proposedIndexPath.section < 1 { 85 | return IndexPath(item: 1, section: 1) 86 | } 87 | 88 | if proposedIndexPath.item < 1 { 89 | return IndexPath(item: 1, section: proposedIndexPath.section) 90 | } 91 | 92 | return proposedIndexPath 93 | } 94 | 95 | 96 | override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 97 | let sourceIndex = UInt(sourceIndexPath.item - 1) 98 | let destinationIndex = UInt(destinationIndexPath.item - 1) 99 | 100 | if let source = source { 101 | source.reorderFolder(at:sourceIndex, to: destinationIndex) 102 | } 103 | } 104 | 105 | func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) { 106 | 107 | switch coordinator.proposal.operation { 108 | case .move: 109 | 110 | for _ in coordinator.session.items { 111 | if let wrapper = coordinator.session.localDragSession?.localContext as? CATElementWrapper { 112 | if let _ = wrapper.element as? CATFolder { 113 | for dropItem in coordinator.items { 114 | if let sourceIndexPath = dropItem.sourceIndexPath, let destination = coordinator.destinationIndexPath { 115 | 116 | let sourceIndex = UInt(sourceIndexPath.item - 1) 117 | let destinationIndex = UInt(destination.item - 1) 118 | 119 | if sourceIndex != destinationIndex && destination.section == 1 { 120 | 121 | 122 | if let source = source { 123 | source.reorderFolder(at:sourceIndex, to: destinationIndex) 124 | } 125 | 126 | coordinator.drop(dropItem.dragItem, toItemAt: destination) 127 | } 128 | } 129 | } 130 | } 131 | } 132 | } 133 | default: 134 | for dropItem in coordinator.items { 135 | if let sourceIndexPath = dropItem.sourceIndexPath { 136 | coordinator.drop(dropItem.dragItem, toItemAt: sourceIndexPath) 137 | } 138 | } 139 | break 140 | } 141 | 142 | } 143 | 144 | func collectionView(_ collectionView: UICollectionView, dropSessionDidEnd session: UIDropSession) { 145 | reload() 146 | } 147 | 148 | func collectionView(_ collectionView: UICollectionView, dropSessionDidExit session: UIDropSession) { 149 | 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /CatalystExample/Sidebar/CATSourceListViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 Steven Troughton-Smith (@stroughtonsmith) 3 | Provided as sample code to do with as you wish. 4 | No license or attribution required. 5 | */ 6 | 7 | import UIKit 8 | 9 | class CATSourceListViewController: UICollectionViewController { 10 | 11 | var listViewController:CATItemListViewController? 12 | var documentViewController:CATMainViewController? 13 | 14 | static let cellIdentifier = "Cell" 15 | var diffableDataSource:UICollectionViewDiffableDataSource? 16 | 17 | var selectedIdentifier = "all" 18 | 19 | var source:CATSourceFile? { 20 | didSet { 21 | reload() 22 | } 23 | } 24 | 25 | init() { 26 | 27 | let layout = UICollectionViewCompositionalLayout() { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in 28 | 29 | var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar) 30 | configuration.showsSeparators = false 31 | configuration.headerMode = .firstItemInSection 32 | 33 | let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment) 34 | 35 | return section 36 | } 37 | 38 | super.init(collectionViewLayout: layout) 39 | 40 | diffableDataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, ctx in 41 | 42 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CATSourceListViewController.cellIdentifier, for: indexPath) 43 | 44 | var content = UIListContentConfiguration.sidebarCell() 45 | content.textProperties.allowsDefaultTighteningForTruncation = false 46 | 47 | if indexPath.section == 0 { 48 | 49 | switch indexPath.row { 50 | case 0: 51 | content = UIListContentConfiguration.sidebarHeader() 52 | 53 | content.text = NSLocalizedString("SIDEBAR_TITLE_LIBRARY", comment:"") 54 | case 1: 55 | content.text = NSLocalizedString("SIDEBAR_ALL_ITEMS", comment:"") 56 | content.image = UIImage(systemName: "doc.text") 57 | case 2: 58 | content.text = NSLocalizedString("SIDEBAR_RECENTS", comment:"") 59 | content.image = UIImage(systemName: "clock") 60 | default: 61 | break 62 | } 63 | 64 | cell.contentConfiguration = content 65 | } 66 | else { 67 | if indexPath.row == 0 { 68 | 69 | content = UIListContentConfiguration.sidebarHeader() 70 | content.text = NSLocalizedString("SIDEBAR_TITLE_FOLDERS", comment:"") 71 | } 72 | else 73 | { 74 | if let folder = self.source?.dataStore.folders[(indexPath.item - 1)] { 75 | content.text = folder.title 76 | content.image = UIImage(systemName: "folder") 77 | } 78 | } 79 | 80 | cell.contentConfiguration = content 81 | } 82 | 83 | return cell 84 | }) 85 | 86 | 87 | #if !targetEnvironment(macCatalyst) 88 | collectionView.backgroundColor = .systemGroupedBackground 89 | #endif 90 | 91 | collectionView.selectionFollowsFocus = true 92 | 93 | collectionView.register(UICollectionViewListCell.self, forCellWithReuseIdentifier: CATSourceListViewController.cellIdentifier) 94 | 95 | collectionView.dataSource = diffableDataSource 96 | collectionView.delegate = self 97 | 98 | collectionView.dragInteractionEnabled = true 99 | collectionView.dragDelegate = self 100 | collectionView.dropDelegate = self 101 | 102 | navigationItem.largeTitleDisplayMode = .always 103 | 104 | NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil) 105 | NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) 106 | 107 | NotificationCenter.default.addObserver(forName: .documentChanged, object: nil, queue: nil) { [weak self] _ in 108 | DispatchQueue.main.async { 109 | self!.reload() 110 | } 111 | } 112 | 113 | title = NSLocalizedString("SOURCE_VIEW_TITLE", comment: ""); 114 | } 115 | 116 | required init?(coder: NSCoder) { 117 | fatalError("init(coder:) has not been implemented") 118 | } 119 | 120 | override func viewDidLoad() { 121 | super.viewDidLoad() 122 | 123 | clearsSelectionOnViewWillAppear = false 124 | } 125 | 126 | // MARK: - Diffable Data Source 127 | 128 | func reload() { 129 | 130 | var snapshot = NSDiffableDataSourceSnapshot() 131 | 132 | snapshot.appendSections(["library"]) 133 | snapshot.appendItems(["library.header", "all", "recents"]) 134 | 135 | if let folders = self.source?.dataStore.folders { 136 | snapshot.appendSections(["folders"]) 137 | snapshot.appendItems(["folders.header"]) 138 | snapshot.appendItems(folders) 139 | 140 | diffableDataSource?.apply(snapshot, animatingDifferences: false) 141 | { [weak self] in 142 | self!.selectItemWithIdentifier(self!.selectedIdentifier) 143 | } 144 | } 145 | } 146 | 147 | func selectItemWithIdentifier(_ identifier:String) { 148 | 149 | var intendedIndexPath = IndexPath(item: 0, section: 0) 150 | var found = false 151 | 152 | 153 | if identifier == "all" { 154 | intendedIndexPath = IndexPath(item: 1, section: 0) 155 | found = true 156 | } 157 | else if identifier == "recents" { 158 | intendedIndexPath = IndexPath(item: 2, section: 0) 159 | found = true 160 | } 161 | 162 | if found == false { 163 | if let folders = self.source?.dataStore.folders { 164 | for i in 0 ..< folders.count { 165 | if folders[i].identifier == identifier { 166 | intendedIndexPath.item = (i + 1) 167 | intendedIndexPath.section = 1 168 | found = true 169 | break 170 | } 171 | } 172 | } 173 | } 174 | 175 | 176 | if found == true { 177 | collectionView.selectItem(at: intendedIndexPath, animated: false, scrollPosition: []) 178 | } 179 | } 180 | 181 | // MARK: - List View 182 | 183 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 184 | 185 | if let listViewController = listViewController { 186 | 187 | if let selectedIndexPath = collectionView.indexPathsForSelectedItems?.first { 188 | if selectedIndexPath.section == 1 { 189 | if let folder = self.source?.dataStore.folders[(selectedIndexPath.item - 1)] { 190 | 191 | selectedIdentifier = folder.identifier 192 | } 193 | } 194 | else { 195 | if selectedIndexPath.item == 1 { 196 | selectedIdentifier = "all" 197 | } 198 | else if selectedIndexPath.item == 2 { 199 | selectedIdentifier = "recents" 200 | } 201 | } 202 | } 203 | 204 | if indexPath.section == 0 { 205 | if indexPath.item == 1 { 206 | listViewController.currentFolderIndex = CATSourceFile.CATFolderIndexAll 207 | } 208 | else { 209 | listViewController.currentFolderIndex = CATSourceFile.CATFolderIndexRecents 210 | } 211 | } 212 | else { 213 | listViewController.currentFolderIndex = (indexPath.item - 1) 214 | } 215 | } 216 | 217 | guard let listViewController = documentViewController?.listViewController else { return } 218 | 219 | if UIDevice.current.userInterfaceIdiom == .phone || view.window?.traitCollection.horizontalSizeClass == .compact { 220 | navigationController?.pushViewController(listViewController, animated: true) 221 | } 222 | } 223 | 224 | override func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { 225 | if indexPath.item == 0 { 226 | return false 227 | } 228 | else { 229 | 230 | return true 231 | } 232 | } 233 | 234 | override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { 235 | return (indexPath.item != 0) 236 | } 237 | 238 | // MARK: - Keyboard Avoidance 239 | 240 | @objc func adjustForKeyboard(notification: Notification) { 241 | guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } 242 | 243 | let keyboardScreenEndFrame = keyboardValue.cgRectValue 244 | let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window) 245 | 246 | if notification.name == UIResponder.keyboardWillHideNotification { 247 | collectionView.contentInset = .zero 248 | } else { 249 | collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardViewEndFrame.height - view.safeAreaInsets.bottom, right: 0) 250 | } 251 | 252 | collectionView.scrollIndicatorInsets = collectionView.contentInset 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Advanced Catalyst Example 2 | 3 | This is an example of a Catalyst app using a three-column layout, with a primary toolbar. 4 | 5 | It includes topics such as: 6 | - Drag & drop reordering of the sidebar items 7 | - Scaling UI metrics between Mac Idiom and iPad Idiom 8 | - Using Codable to load a data store from disk and present it 9 | - Using an AppKit bundle to access APIs not available to Catalyst, like NSSearchToolbarItem 10 | - Mixing a SwiftUI detail view into a UIKit app structure 11 | - Using the UIKit Focus Engine to provide keyboard navigation 12 | - Providing a Credits file for your About window 13 | 14 | ### Screenshot 15 | ![](https://hccdata.s3.amazonaws.com/gh_catalystapp_v2.jpg) 16 | -------------------------------------------------------------------------------- /catalystapp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B01C38A126628E8E0027D8A8 /* CATPreferencesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B01C38A026628E8E0027D8A8 /* CATPreferencesController.swift */; }; 11 | B01C38A3266291C60027D8A8 /* CATAppDelegate+MenuBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B01C38A2266291C60027D8A8 /* CATAppDelegate+MenuBuilder.swift */; }; 12 | B023052D2663104300776EA0 /* Example.json in Resources */ = {isa = PBXBuildFile; fileRef = B023052C2663104300776EA0 /* Example.json */; }; 13 | B037A0362660821500FD3DB1 /* CATElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = B037A0352660821500FD3DB1 /* CATElement.swift */; }; 14 | B037A0382660883900FD3DB1 /* CATSourceListViewController+Reordering.swift in Sources */ = {isa = PBXBuildFile; fileRef = B037A0372660883900FD3DB1 /* CATSourceListViewController+Reordering.swift */; }; 15 | B03AD6882669C015002904A8 /* CATWindowSceneDelegate+NSToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B03AD6872669C015002904A8 /* CATWindowSceneDelegate+NSToolbar.swift */; }; 16 | B087F477265F20EA00FE6CD7 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = B087F476265F20EA00FE6CD7 /* Common.swift */; }; 17 | B09975672669D7D200338AE1 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = B09975662669D7D100338AE1 /* Credits.rtf */; }; 18 | B0AD66AB265D417B00338173 /* CATItemListViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0AD66AA265D417B00338173 /* CATItemListViewCell.swift */; }; 19 | B0AD66AD265D5EA600338173 /* CATFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0AD66AC265D5EA600338173 /* CATFolder.swift */; }; 20 | B0AD66BC265D646F00338173 /* AppKitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0AD66BB265D646F00338173 /* AppKitController.swift */; }; 21 | B0AD66C0265D648A00338173 /* AppKitIntegration.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = B0AD66B3265D646100338173 /* AppKitIntegration.framework */; platformFilter = maccatalyst; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 22 | B0CD6D47265CA4C900640087 /* CATMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0CD6D46265CA4C900640087 /* CATMainViewController.swift */; }; 23 | B0D2F5C12517F4F0000FC64C /* CATAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0D2F5C02517F4F0000FC64C /* CATAppDelegate.swift */; }; 24 | B0D2F5C72517F4F0000FC64C /* CATSourceFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0D2F5C62517F4F0000FC64C /* CATSourceFile.swift */; }; 25 | B0D2F5CC2517F4F2000FC64C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B0D2F5CB2517F4F2000FC64C /* Assets.xcassets */; }; 26 | B0D2F5CF2517F4F2000FC64C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B0D2F5CD2517F4F2000FC64C /* LaunchScreen.storyboard */; }; 27 | B0D2F5DF2517F6E9000FC64C /* CATItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0D2F5DE2517F6E9000FC64C /* CATItem.swift */; }; 28 | B0D2F5E22517F990000FC64C /* CATItemViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0D2F5E12517F990000FC64C /* CATItemViewController.swift */; }; 29 | B0D2F5E625180210000FC64C /* CATSourceListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0D2F5E525180210000FC64C /* CATSourceListViewController.swift */; }; 30 | B0D2F5E92518021E000FC64C /* CATItemListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0D2F5E82518021E000FC64C /* CATItemListViewController.swift */; }; 31 | B0D2F5EC251802A7000FC64C /* Number+Scaling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0D2F5EB251802A7000FC64C /* Number+Scaling.swift */; }; 32 | B0D2F5EF25180583000FC64C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B0D2F5EE25180583000FC64C /* Localizable.strings */; }; 33 | B0D2F5F625180704000FC64C /* CATWindowSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0D2F5F525180704000FC64C /* CATWindowSceneDelegate.swift */; }; 34 | /* End PBXBuildFile section */ 35 | 36 | /* Begin PBXContainerItemProxy section */ 37 | B0AD66BD265D647D00338173 /* PBXContainerItemProxy */ = { 38 | isa = PBXContainerItemProxy; 39 | containerPortal = B0D2F5B52517F4F0000FC64C /* Project object */; 40 | proxyType = 1; 41 | remoteGlobalIDString = B0AD66B2265D646100338173; 42 | remoteInfo = AppKitIntegration; 43 | }; 44 | /* End PBXContainerItemProxy section */ 45 | 46 | /* Begin PBXCopyFilesBuildPhase section */ 47 | B0AD66BF265D648200338173 /* CopyFiles */ = { 48 | isa = PBXCopyFilesBuildPhase; 49 | buildActionMask = 2147483647; 50 | dstPath = ""; 51 | dstSubfolderSpec = 10; 52 | files = ( 53 | B0AD66C0265D648A00338173 /* AppKitIntegration.framework in CopyFiles */, 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | /* End PBXCopyFilesBuildPhase section */ 58 | 59 | /* Begin PBXFileReference section */ 60 | B01C38A026628E8E0027D8A8 /* CATPreferencesController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATPreferencesController.swift; sourceTree = ""; }; 61 | B01C38A2266291C60027D8A8 /* CATAppDelegate+MenuBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CATAppDelegate+MenuBuilder.swift"; sourceTree = ""; }; 62 | B023052C2663104300776EA0 /* Example.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Example.json; sourceTree = ""; }; 63 | B037A0352660821500FD3DB1 /* CATElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATElement.swift; sourceTree = ""; }; 64 | B037A0372660883900FD3DB1 /* CATSourceListViewController+Reordering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CATSourceListViewController+Reordering.swift"; sourceTree = ""; }; 65 | B03AD6872669C015002904A8 /* CATWindowSceneDelegate+NSToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CATWindowSceneDelegate+NSToolbar.swift"; sourceTree = ""; }; 66 | B087F476265F20EA00FE6CD7 /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; }; 67 | B09975652669D67600338AE1 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 68 | B09975662669D7D100338AE1 /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 69 | B0AD66AA265D417B00338173 /* CATItemListViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATItemListViewCell.swift; sourceTree = ""; }; 70 | B0AD66AC265D5EA600338173 /* CATFolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATFolder.swift; sourceTree = ""; }; 71 | B0AD66B3265D646100338173 /* AppKitIntegration.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppKitIntegration.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 72 | B0AD66B6265D646100338173 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 73 | B0AD66BB265D646F00338173 /* AppKitController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppKitController.swift; sourceTree = ""; }; 74 | B0CD6D46265CA4C900640087 /* CATMainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATMainViewController.swift; sourceTree = ""; }; 75 | B0D2F5BD2517F4F0000FC64C /* CatalystExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CatalystExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 76 | B0D2F5C02517F4F0000FC64C /* CATAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATAppDelegate.swift; sourceTree = ""; }; 77 | B0D2F5C62517F4F0000FC64C /* CATSourceFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATSourceFile.swift; sourceTree = ""; }; 78 | B0D2F5CB2517F4F2000FC64C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 79 | B0D2F5CE2517F4F2000FC64C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 80 | B0D2F5D02517F4F2000FC64C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 81 | B0D2F5D72517F4F9000FC64C /* CatalystExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CatalystExample.entitlements; sourceTree = ""; }; 82 | B0D2F5DE2517F6E9000FC64C /* CATItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATItem.swift; sourceTree = ""; }; 83 | B0D2F5E12517F990000FC64C /* CATItemViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATItemViewController.swift; sourceTree = ""; }; 84 | B0D2F5E525180210000FC64C /* CATSourceListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATSourceListViewController.swift; sourceTree = ""; }; 85 | B0D2F5E82518021E000FC64C /* CATItemListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATItemListViewController.swift; sourceTree = ""; }; 86 | B0D2F5EB251802A7000FC64C /* Number+Scaling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Number+Scaling.swift"; sourceTree = ""; }; 87 | B0D2F5EE25180583000FC64C /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; 88 | B0D2F5F525180704000FC64C /* CATWindowSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATWindowSceneDelegate.swift; sourceTree = ""; }; 89 | /* End PBXFileReference section */ 90 | 91 | /* Begin PBXFrameworksBuildPhase section */ 92 | B0AD66B0265D646100338173 /* Frameworks */ = { 93 | isa = PBXFrameworksBuildPhase; 94 | buildActionMask = 2147483647; 95 | files = ( 96 | ); 97 | runOnlyForDeploymentPostprocessing = 0; 98 | }; 99 | B0D2F5BA2517F4F0000FC64C /* Frameworks */ = { 100 | isa = PBXFrameworksBuildPhase; 101 | buildActionMask = 2147483647; 102 | files = ( 103 | ); 104 | runOnlyForDeploymentPostprocessing = 0; 105 | }; 106 | /* End PBXFrameworksBuildPhase section */ 107 | 108 | /* Begin PBXGroup section */ 109 | B02ED7732664549E00ACA79A /* Main Window */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | B0D2F5F525180704000FC64C /* CATWindowSceneDelegate.swift */, 113 | B03AD6872669C015002904A8 /* CATWindowSceneDelegate+NSToolbar.swift */, 114 | B0CD6D46265CA4C900640087 /* CATMainViewController.swift */, 115 | ); 116 | path = "Main Window"; 117 | sourceTree = ""; 118 | }; 119 | B02ED774266454AE00ACA79A /* Controllers */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | B0D2F5C02517F4F0000FC64C /* CATAppDelegate.swift */, 123 | B01C38A2266291C60027D8A8 /* CATAppDelegate+MenuBuilder.swift */, 124 | B01C38A026628E8E0027D8A8 /* CATPreferencesController.swift */, 125 | ); 126 | path = Controllers; 127 | sourceTree = ""; 128 | }; 129 | B0AD66B4265D646100338173 /* AppKitIntegration */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | B0AD66BB265D646F00338173 /* AppKitController.swift */, 133 | B0AD66B6265D646100338173 /* Info.plist */, 134 | ); 135 | path = AppKitIntegration; 136 | sourceTree = ""; 137 | }; 138 | B0CD6D41265C9DA900640087 /* App */ = { 139 | isa = PBXGroup; 140 | children = ( 141 | B02ED774266454AE00ACA79A /* Controllers */, 142 | B02ED7732664549E00ACA79A /* Main Window */, 143 | ); 144 | path = App; 145 | sourceTree = ""; 146 | }; 147 | B0CD6D42265C9DB100640087 /* Sidebar */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | B0D2F5E525180210000FC64C /* CATSourceListViewController.swift */, 151 | B037A0372660883900FD3DB1 /* CATSourceListViewController+Reordering.swift */, 152 | ); 153 | path = Sidebar; 154 | sourceTree = ""; 155 | }; 156 | B0CD6D43265C9DBB00640087 /* Middle Column */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | B0D2F5E82518021E000FC64C /* CATItemListViewController.swift */, 160 | B0AD66AA265D417B00338173 /* CATItemListViewCell.swift */, 161 | ); 162 | path = "Middle Column"; 163 | sourceTree = ""; 164 | }; 165 | B0CD6D44265C9DC900640087 /* Content View */ = { 166 | isa = PBXGroup; 167 | children = ( 168 | B0D2F5E12517F990000FC64C /* CATItemViewController.swift */, 169 | ); 170 | path = "Content View"; 171 | sourceTree = ""; 172 | }; 173 | B0CD6D45265C9DE700640087 /* Data Model */ = { 174 | isa = PBXGroup; 175 | children = ( 176 | B037A0352660821500FD3DB1 /* CATElement.swift */, 177 | B0D2F5DE2517F6E9000FC64C /* CATItem.swift */, 178 | B0AD66AC265D5EA600338173 /* CATFolder.swift */, 179 | B0D2F5C62517F4F0000FC64C /* CATSourceFile.swift */, 180 | ); 181 | path = "Data Model"; 182 | sourceTree = ""; 183 | }; 184 | B0D2F5B42517F4F0000FC64C = { 185 | isa = PBXGroup; 186 | children = ( 187 | B09975652669D67600338AE1 /* README.md */, 188 | B0D2F5BF2517F4F0000FC64C /* CatalystExample */, 189 | B0AD66B4265D646100338173 /* AppKitIntegration */, 190 | B0D2F5BE2517F4F0000FC64C /* Products */, 191 | ); 192 | sourceTree = ""; 193 | }; 194 | B0D2F5BE2517F4F0000FC64C /* Products */ = { 195 | isa = PBXGroup; 196 | children = ( 197 | B0D2F5BD2517F4F0000FC64C /* CatalystExample.app */, 198 | B0AD66B3265D646100338173 /* AppKitIntegration.framework */, 199 | ); 200 | name = Products; 201 | sourceTree = ""; 202 | }; 203 | B0D2F5BF2517F4F0000FC64C /* CatalystExample */ = { 204 | isa = PBXGroup; 205 | children = ( 206 | B0D2F5EB251802A7000FC64C /* Number+Scaling.swift */, 207 | B087F476265F20EA00FE6CD7 /* Common.swift */, 208 | B0D2F5D72517F4F9000FC64C /* CatalystExample.entitlements */, 209 | B0CD6D41265C9DA900640087 /* App */, 210 | B0CD6D45265C9DE700640087 /* Data Model */, 211 | B0CD6D42265C9DB100640087 /* Sidebar */, 212 | B0CD6D43265C9DBB00640087 /* Middle Column */, 213 | B0CD6D44265C9DC900640087 /* Content View */, 214 | B0D2F5F12518058A000FC64C /* Resources */, 215 | ); 216 | path = CatalystExample; 217 | sourceTree = ""; 218 | }; 219 | B0D2F5F12518058A000FC64C /* Resources */ = { 220 | isa = PBXGroup; 221 | children = ( 222 | B0D2F5EE25180583000FC64C /* Localizable.strings */, 223 | B0D2F5CB2517F4F2000FC64C /* Assets.xcassets */, 224 | B0D2F5CD2517F4F2000FC64C /* LaunchScreen.storyboard */, 225 | B0D2F5D02517F4F2000FC64C /* Info.plist */, 226 | B023052C2663104300776EA0 /* Example.json */, 227 | B09975662669D7D100338AE1 /* Credits.rtf */, 228 | ); 229 | path = Resources; 230 | sourceTree = ""; 231 | }; 232 | /* End PBXGroup section */ 233 | 234 | /* Begin PBXHeadersBuildPhase section */ 235 | B0AD66AE265D646100338173 /* Headers */ = { 236 | isa = PBXHeadersBuildPhase; 237 | buildActionMask = 2147483647; 238 | files = ( 239 | ); 240 | runOnlyForDeploymentPostprocessing = 0; 241 | }; 242 | /* End PBXHeadersBuildPhase section */ 243 | 244 | /* Begin PBXNativeTarget section */ 245 | B0AD66B2265D646100338173 /* AppKitIntegration */ = { 246 | isa = PBXNativeTarget; 247 | buildConfigurationList = B0AD66B8265D646100338173 /* Build configuration list for PBXNativeTarget "AppKitIntegration" */; 248 | buildPhases = ( 249 | B0AD66AE265D646100338173 /* Headers */, 250 | B0AD66AF265D646100338173 /* Sources */, 251 | B0AD66B0265D646100338173 /* Frameworks */, 252 | B0AD66B1265D646100338173 /* Resources */, 253 | ); 254 | buildRules = ( 255 | ); 256 | dependencies = ( 257 | ); 258 | name = AppKitIntegration; 259 | productName = AppKitIntegration; 260 | productReference = B0AD66B3265D646100338173 /* AppKitIntegration.framework */; 261 | productType = "com.apple.product-type.framework"; 262 | }; 263 | B0D2F5BC2517F4F0000FC64C /* CatalystExample */ = { 264 | isa = PBXNativeTarget; 265 | buildConfigurationList = B0D2F5D32517F4F2000FC64C /* Build configuration list for PBXNativeTarget "CatalystExample" */; 266 | buildPhases = ( 267 | B0D2F5B92517F4F0000FC64C /* Sources */, 268 | B0D2F5BA2517F4F0000FC64C /* Frameworks */, 269 | B0D2F5BB2517F4F0000FC64C /* Resources */, 270 | B0AD66BF265D648200338173 /* CopyFiles */, 271 | ); 272 | buildRules = ( 273 | ); 274 | dependencies = ( 275 | B0AD66BE265D647D00338173 /* PBXTargetDependency */, 276 | ); 277 | name = CatalystExample; 278 | productName = issues; 279 | productReference = B0D2F5BD2517F4F0000FC64C /* CatalystExample.app */; 280 | productType = "com.apple.product-type.application"; 281 | }; 282 | /* End PBXNativeTarget section */ 283 | 284 | /* Begin PBXProject section */ 285 | B0D2F5B52517F4F0000FC64C /* Project object */ = { 286 | isa = PBXProject; 287 | attributes = { 288 | CLASSPREFIX = ISS; 289 | LastSwiftUpdateCheck = 1220; 290 | LastUpgradeCheck = 1220; 291 | TargetAttributes = { 292 | B0AD66B2265D646100338173 = { 293 | CreatedOnToolsVersion = 12.5; 294 | LastSwiftMigration = 1250; 295 | }; 296 | B0D2F5BC2517F4F0000FC64C = { 297 | CreatedOnToolsVersion = 12.2; 298 | }; 299 | }; 300 | }; 301 | buildConfigurationList = B0D2F5B82517F4F0000FC64C /* Build configuration list for PBXProject "catalystapp" */; 302 | compatibilityVersion = "Xcode 9.3"; 303 | developmentRegion = en; 304 | hasScannedForEncodings = 0; 305 | knownRegions = ( 306 | en, 307 | Base, 308 | ); 309 | mainGroup = B0D2F5B42517F4F0000FC64C; 310 | productRefGroup = B0D2F5BE2517F4F0000FC64C /* Products */; 311 | projectDirPath = ""; 312 | projectRoot = ""; 313 | targets = ( 314 | B0D2F5BC2517F4F0000FC64C /* CatalystExample */, 315 | B0AD66B2265D646100338173 /* AppKitIntegration */, 316 | ); 317 | }; 318 | /* End PBXProject section */ 319 | 320 | /* Begin PBXResourcesBuildPhase section */ 321 | B0AD66B1265D646100338173 /* Resources */ = { 322 | isa = PBXResourcesBuildPhase; 323 | buildActionMask = 2147483647; 324 | files = ( 325 | ); 326 | runOnlyForDeploymentPostprocessing = 0; 327 | }; 328 | B0D2F5BB2517F4F0000FC64C /* Resources */ = { 329 | isa = PBXResourcesBuildPhase; 330 | buildActionMask = 2147483647; 331 | files = ( 332 | B09975672669D7D200338AE1 /* Credits.rtf in Resources */, 333 | B0D2F5CF2517F4F2000FC64C /* LaunchScreen.storyboard in Resources */, 334 | B0D2F5EF25180583000FC64C /* Localizable.strings in Resources */, 335 | B0D2F5CC2517F4F2000FC64C /* Assets.xcassets in Resources */, 336 | B023052D2663104300776EA0 /* Example.json in Resources */, 337 | ); 338 | runOnlyForDeploymentPostprocessing = 0; 339 | }; 340 | /* End PBXResourcesBuildPhase section */ 341 | 342 | /* Begin PBXSourcesBuildPhase section */ 343 | B0AD66AF265D646100338173 /* Sources */ = { 344 | isa = PBXSourcesBuildPhase; 345 | buildActionMask = 2147483647; 346 | files = ( 347 | B0AD66BC265D646F00338173 /* AppKitController.swift in Sources */, 348 | ); 349 | runOnlyForDeploymentPostprocessing = 0; 350 | }; 351 | B0D2F5B92517F4F0000FC64C /* Sources */ = { 352 | isa = PBXSourcesBuildPhase; 353 | buildActionMask = 2147483647; 354 | files = ( 355 | B037A0362660821500FD3DB1 /* CATElement.swift in Sources */, 356 | B01C38A3266291C60027D8A8 /* CATAppDelegate+MenuBuilder.swift in Sources */, 357 | B0D2F5E22517F990000FC64C /* CATItemViewController.swift in Sources */, 358 | B0D2F5C72517F4F0000FC64C /* CATSourceFile.swift in Sources */, 359 | B087F477265F20EA00FE6CD7 /* Common.swift in Sources */, 360 | B0CD6D47265CA4C900640087 /* CATMainViewController.swift in Sources */, 361 | B01C38A126628E8E0027D8A8 /* CATPreferencesController.swift in Sources */, 362 | B0AD66AB265D417B00338173 /* CATItemListViewCell.swift in Sources */, 363 | B0AD66AD265D5EA600338173 /* CATFolder.swift in Sources */, 364 | B0D2F5F625180704000FC64C /* CATWindowSceneDelegate.swift in Sources */, 365 | B0D2F5EC251802A7000FC64C /* Number+Scaling.swift in Sources */, 366 | B0D2F5E92518021E000FC64C /* CATItemListViewController.swift in Sources */, 367 | B0D2F5C12517F4F0000FC64C /* CATAppDelegate.swift in Sources */, 368 | B0D2F5E625180210000FC64C /* CATSourceListViewController.swift in Sources */, 369 | B037A0382660883900FD3DB1 /* CATSourceListViewController+Reordering.swift in Sources */, 370 | B0D2F5DF2517F6E9000FC64C /* CATItem.swift in Sources */, 371 | B03AD6882669C015002904A8 /* CATWindowSceneDelegate+NSToolbar.swift in Sources */, 372 | ); 373 | runOnlyForDeploymentPostprocessing = 0; 374 | }; 375 | /* End PBXSourcesBuildPhase section */ 376 | 377 | /* Begin PBXTargetDependency section */ 378 | B0AD66BE265D647D00338173 /* PBXTargetDependency */ = { 379 | isa = PBXTargetDependency; 380 | platformFilter = maccatalyst; 381 | target = B0AD66B2265D646100338173 /* AppKitIntegration */; 382 | targetProxy = B0AD66BD265D647D00338173 /* PBXContainerItemProxy */; 383 | }; 384 | /* End PBXTargetDependency section */ 385 | 386 | /* Begin PBXVariantGroup section */ 387 | B0D2F5CD2517F4F2000FC64C /* LaunchScreen.storyboard */ = { 388 | isa = PBXVariantGroup; 389 | children = ( 390 | B0D2F5CE2517F4F2000FC64C /* Base */, 391 | ); 392 | name = LaunchScreen.storyboard; 393 | sourceTree = ""; 394 | }; 395 | /* End PBXVariantGroup section */ 396 | 397 | /* Begin XCBuildConfiguration section */ 398 | B0AD66B9265D646100338173 /* Debug */ = { 399 | isa = XCBuildConfiguration; 400 | buildSettings = { 401 | CLANG_ENABLE_MODULES = YES; 402 | CODE_SIGN_STYLE = Automatic; 403 | COMBINE_HIDPI_IMAGES = YES; 404 | CURRENT_PROJECT_VERSION = 1; 405 | DEFINES_MODULE = YES; 406 | DEVELOPMENT_TEAM = 2ZDN69KUUV; 407 | DYLIB_COMPATIBILITY_VERSION = 1; 408 | DYLIB_CURRENT_VERSION = 1; 409 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 410 | INFOPLIST_FILE = AppKitIntegration/Info.plist; 411 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 412 | LD_RUNPATH_SEARCH_PATHS = ( 413 | "$(inherited)", 414 | "@executable_path/../Frameworks", 415 | "@loader_path/Frameworks", 416 | ); 417 | MACOSX_DEPLOYMENT_TARGET = 11.3; 418 | PRODUCT_BUNDLE_IDENTIFIER = com.highcaffeinecontent.AppKitIntegration; 419 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 420 | SDKROOT = macosx; 421 | SKIP_INSTALL = YES; 422 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 423 | SWIFT_VERSION = 5.0; 424 | VERSIONING_SYSTEM = "apple-generic"; 425 | VERSION_INFO_PREFIX = ""; 426 | }; 427 | name = Debug; 428 | }; 429 | B0AD66BA265D646100338173 /* Release */ = { 430 | isa = XCBuildConfiguration; 431 | buildSettings = { 432 | CLANG_ENABLE_MODULES = YES; 433 | CODE_SIGN_STYLE = Automatic; 434 | COMBINE_HIDPI_IMAGES = YES; 435 | CURRENT_PROJECT_VERSION = 1; 436 | DEFINES_MODULE = YES; 437 | DEVELOPMENT_TEAM = 2ZDN69KUUV; 438 | DYLIB_COMPATIBILITY_VERSION = 1; 439 | DYLIB_CURRENT_VERSION = 1; 440 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 441 | INFOPLIST_FILE = AppKitIntegration/Info.plist; 442 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 443 | LD_RUNPATH_SEARCH_PATHS = ( 444 | "$(inherited)", 445 | "@executable_path/../Frameworks", 446 | "@loader_path/Frameworks", 447 | ); 448 | MACOSX_DEPLOYMENT_TARGET = 11.3; 449 | PRODUCT_BUNDLE_IDENTIFIER = com.highcaffeinecontent.AppKitIntegration; 450 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 451 | SDKROOT = macosx; 452 | SKIP_INSTALL = YES; 453 | SWIFT_VERSION = 5.0; 454 | VERSIONING_SYSTEM = "apple-generic"; 455 | VERSION_INFO_PREFIX = ""; 456 | }; 457 | name = Release; 458 | }; 459 | B0D2F5D12517F4F2000FC64C /* Debug */ = { 460 | isa = XCBuildConfiguration; 461 | buildSettings = { 462 | ALWAYS_SEARCH_USER_PATHS = NO; 463 | CLANG_ANALYZER_NONNULL = YES; 464 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 465 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 466 | CLANG_CXX_LIBRARY = "libc++"; 467 | CLANG_ENABLE_MODULES = YES; 468 | CLANG_ENABLE_OBJC_ARC = YES; 469 | CLANG_ENABLE_OBJC_WEAK = YES; 470 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 471 | CLANG_WARN_BOOL_CONVERSION = YES; 472 | CLANG_WARN_COMMA = YES; 473 | CLANG_WARN_CONSTANT_CONVERSION = YES; 474 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 475 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 476 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 477 | CLANG_WARN_EMPTY_BODY = YES; 478 | CLANG_WARN_ENUM_CONVERSION = YES; 479 | CLANG_WARN_INFINITE_RECURSION = YES; 480 | CLANG_WARN_INT_CONVERSION = YES; 481 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 482 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 483 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 484 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 485 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 486 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 487 | CLANG_WARN_STRICT_PROTOTYPES = YES; 488 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 489 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 490 | CLANG_WARN_UNREACHABLE_CODE = YES; 491 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 492 | COPY_PHASE_STRIP = NO; 493 | DEBUG_INFORMATION_FORMAT = dwarf; 494 | ENABLE_STRICT_OBJC_MSGSEND = YES; 495 | ENABLE_TESTABILITY = YES; 496 | GCC_C_LANGUAGE_STANDARD = gnu11; 497 | GCC_DYNAMIC_NO_PIC = NO; 498 | GCC_NO_COMMON_BLOCKS = YES; 499 | GCC_OPTIMIZATION_LEVEL = 0; 500 | GCC_PREPROCESSOR_DEFINITIONS = ( 501 | "DEBUG=1", 502 | "$(inherited)", 503 | ); 504 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 505 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 506 | GCC_WARN_UNDECLARED_SELECTOR = YES; 507 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 508 | GCC_WARN_UNUSED_FUNCTION = YES; 509 | GCC_WARN_UNUSED_VARIABLE = YES; 510 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 511 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 512 | MTL_FAST_MATH = YES; 513 | ONLY_ACTIVE_ARCH = YES; 514 | SDKROOT = iphoneos; 515 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 516 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 517 | }; 518 | name = Debug; 519 | }; 520 | B0D2F5D22517F4F2000FC64C /* Release */ = { 521 | isa = XCBuildConfiguration; 522 | buildSettings = { 523 | ALWAYS_SEARCH_USER_PATHS = NO; 524 | CLANG_ANALYZER_NONNULL = YES; 525 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 526 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 527 | CLANG_CXX_LIBRARY = "libc++"; 528 | CLANG_ENABLE_MODULES = YES; 529 | CLANG_ENABLE_OBJC_ARC = YES; 530 | CLANG_ENABLE_OBJC_WEAK = YES; 531 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 532 | CLANG_WARN_BOOL_CONVERSION = YES; 533 | CLANG_WARN_COMMA = YES; 534 | CLANG_WARN_CONSTANT_CONVERSION = YES; 535 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 536 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 537 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 538 | CLANG_WARN_EMPTY_BODY = YES; 539 | CLANG_WARN_ENUM_CONVERSION = YES; 540 | CLANG_WARN_INFINITE_RECURSION = YES; 541 | CLANG_WARN_INT_CONVERSION = YES; 542 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 543 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 544 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 545 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 546 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 547 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 548 | CLANG_WARN_STRICT_PROTOTYPES = YES; 549 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 550 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 551 | CLANG_WARN_UNREACHABLE_CODE = YES; 552 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 553 | COPY_PHASE_STRIP = NO; 554 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 555 | ENABLE_NS_ASSERTIONS = NO; 556 | ENABLE_STRICT_OBJC_MSGSEND = YES; 557 | GCC_C_LANGUAGE_STANDARD = gnu11; 558 | GCC_NO_COMMON_BLOCKS = YES; 559 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 560 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 561 | GCC_WARN_UNDECLARED_SELECTOR = YES; 562 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 563 | GCC_WARN_UNUSED_FUNCTION = YES; 564 | GCC_WARN_UNUSED_VARIABLE = YES; 565 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 566 | MTL_ENABLE_DEBUG_INFO = NO; 567 | MTL_FAST_MATH = YES; 568 | SDKROOT = iphoneos; 569 | SWIFT_COMPILATION_MODE = wholemodule; 570 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 571 | VALIDATE_PRODUCT = YES; 572 | }; 573 | name = Release; 574 | }; 575 | B0D2F5D42517F4F2000FC64C /* Debug */ = { 576 | isa = XCBuildConfiguration; 577 | buildSettings = { 578 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 579 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 580 | CODE_SIGN_ENTITLEMENTS = CatalystExample/CatalystExample.entitlements; 581 | CODE_SIGN_IDENTITY = "Apple Development"; 582 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 583 | CODE_SIGN_STYLE = Automatic; 584 | CURRENT_PROJECT_VERSION = 1; 585 | DEVELOPMENT_TEAM = 2ZDN69KUUV; 586 | INFOPLIST_FILE = CatalystExample/Resources/Info.plist; 587 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 588 | LD_RUNPATH_SEARCH_PATHS = ( 589 | "$(inherited)", 590 | "@executable_path/Frameworks", 591 | ); 592 | MARKETING_VERSION = 1.0; 593 | PRODUCT_BUNDLE_IDENTIFIER = com.highcaffeinecontent.catalystexample; 594 | PRODUCT_NAME = "$(TARGET_NAME)"; 595 | PROVISIONING_PROFILE_SPECIFIER = ""; 596 | "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; 597 | RUN_CLANG_STATIC_ANALYZER = YES; 598 | SUPPORTS_MACCATALYST = YES; 599 | SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; 600 | SWIFT_VERSION = 5.0; 601 | TARGETED_DEVICE_FAMILY = "1,2,6"; 602 | }; 603 | name = Debug; 604 | }; 605 | B0D2F5D52517F4F2000FC64C /* Release */ = { 606 | isa = XCBuildConfiguration; 607 | buildSettings = { 608 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 609 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 610 | CODE_SIGN_ENTITLEMENTS = CatalystExample/CatalystExample.entitlements; 611 | CODE_SIGN_IDENTITY = "Apple Development"; 612 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 613 | CODE_SIGN_STYLE = Automatic; 614 | CURRENT_PROJECT_VERSION = 1; 615 | DEVELOPMENT_TEAM = 2ZDN69KUUV; 616 | INFOPLIST_FILE = CatalystExample/Resources/Info.plist; 617 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 618 | LD_RUNPATH_SEARCH_PATHS = ( 619 | "$(inherited)", 620 | "@executable_path/Frameworks", 621 | ); 622 | MARKETING_VERSION = 1.0; 623 | PRODUCT_BUNDLE_IDENTIFIER = com.highcaffeinecontent.catalystexample; 624 | PRODUCT_NAME = "$(TARGET_NAME)"; 625 | PROVISIONING_PROFILE_SPECIFIER = ""; 626 | "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; 627 | RUN_CLANG_STATIC_ANALYZER = YES; 628 | SUPPORTS_MACCATALYST = YES; 629 | SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; 630 | SWIFT_VERSION = 5.0; 631 | TARGETED_DEVICE_FAMILY = "1,2,6"; 632 | }; 633 | name = Release; 634 | }; 635 | /* End XCBuildConfiguration section */ 636 | 637 | /* Begin XCConfigurationList section */ 638 | B0AD66B8265D646100338173 /* Build configuration list for PBXNativeTarget "AppKitIntegration" */ = { 639 | isa = XCConfigurationList; 640 | buildConfigurations = ( 641 | B0AD66B9265D646100338173 /* Debug */, 642 | B0AD66BA265D646100338173 /* Release */, 643 | ); 644 | defaultConfigurationIsVisible = 0; 645 | defaultConfigurationName = Release; 646 | }; 647 | B0D2F5B82517F4F0000FC64C /* Build configuration list for PBXProject "catalystapp" */ = { 648 | isa = XCConfigurationList; 649 | buildConfigurations = ( 650 | B0D2F5D12517F4F2000FC64C /* Debug */, 651 | B0D2F5D22517F4F2000FC64C /* Release */, 652 | ); 653 | defaultConfigurationIsVisible = 0; 654 | defaultConfigurationName = Release; 655 | }; 656 | B0D2F5D32517F4F2000FC64C /* Build configuration list for PBXNativeTarget "CatalystExample" */ = { 657 | isa = XCConfigurationList; 658 | buildConfigurations = ( 659 | B0D2F5D42517F4F2000FC64C /* Debug */, 660 | B0D2F5D52517F4F2000FC64C /* Release */, 661 | ); 662 | defaultConfigurationIsVisible = 0; 663 | defaultConfigurationName = Release; 664 | }; 665 | /* End XCConfigurationList section */ 666 | }; 667 | rootObject = B0D2F5B52517F4F0000FC64C /* Project object */; 668 | } 669 | -------------------------------------------------------------------------------- /catalystapp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /catalystapp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /catalystapp.xcodeproj/xcshareddata/xcschemes/AppKitIntegration.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /catalystapp.xcodeproj/xcshareddata/xcschemes/CatalystExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | --------------------------------------------------------------------------------