Creating a Cocoa application without NIB files

前端 未结 11 738
余生分开走
余生分开走 2020-12-04 10:24

Yes, I know this goes against the whole MVC principle!

However, I\'m just trying to whip up a pretty trivial application - and I\'ve pretty much implemented it. Howe

11条回答
  •  遥遥无期
    2020-12-04 10:35

    Swift 4 version with NSToolbar and NSMenu (with event handlers instead of delegates):

    File main.swift:

    autoreleasepool {
       // Even if we loading application manually we need to setup `Info.plist` key:
       // NSPrincipalClass
       // NSApplication
       // Otherwise Application will be loaded in `low resolution` mode.
       let app = Application.shared
       app.setActivationPolicy(.regular)
       app.run()
    }
    

    File: Application.swift

    class Application: NSApplication {
    
       private lazy var mainWindowController = MainWindowController()
       private lazy var mainAppMenu = MainMenu()
    
       override init() {
          super.init()
          setupUI()
          setupHandlers()
       }
    
       required init?(coder: NSCoder) {
          super.init(coder: coder) // This will never called.
       }
    }
    
    extension Application: NSApplicationDelegate {
    
       func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
          return true
       }
    
       func applicationDidFinishLaunching(_ aNotification: Notification) {
          mainWindowController.showWindow(nil)
       }
    
    }
    
    extension Application {
    
       private func setupUI() {
          mainMenu = mainAppMenu
       }
    
       private func setupHandlers() {
          delegate = self
          mainAppMenu.eventHandler = { [weak self] in
             switch $0 {
             case .quit:
                self?.terminate(nil)
             }
          }
       }
    
    }
    

    File MainWindowController.swift

    class MainWindowController: NSWindowController {
    
       private (set) lazy var viewController = MainViewController()
       private (set) lazy var mainToolbar = MainToolbar(identifier: NSToolbar.Identifier("ua.com.wavelabs.Decoder:mainToolbar"))
    
       init() {
          let window = NSWindow(contentRect: CGRect(x: 400, y: 200, width: 800, height: 600),
                                styleMask: [.titled, .closable, .resizable, .miniaturizable],
                                backing: .buffered,
                                defer: true)
          super.init(window: window)
    
    
          let frameSize = window.contentRect(forFrameRect: window.frame).size
          viewController.view.setFrameSize(frameSize)
          window.contentViewController = viewController
    
          window.titleVisibility = .hidden
          window.toolbar = mainToolbar
    
          setupHandlers()
       }
    
       required init?(coder: NSCoder) {
          super.init(coder: coder)
       }
    }
    
    extension MainWindowController {
    
       private func setupHandlers() {
          mainToolbar.eventHandler = {
             print($0)
          }
       }
    }
    

    File MainViewController.swift

    class MainViewController: NSViewController {
    
       init() {
          super.init(nibName: nil, bundle: nil)
       }
    
       required init?(coder: NSCoder) {
          fatalError("init(coder:) has not been implemented")
       }
    
       override func loadView() {
          view = NSView()
          view.wantsLayer = true
          view.layer?.backgroundColor = NSColor.magenta.cgColor
       }
    }
    

    File MainToolbar.swift

    class MainToolbar: NSToolbar {
    
       enum Event: Int {
          case toggleSidePanel
       }
    
       let toolbarDelegate = GenericDelegate()
    
       var eventHandler: ((MainToolbar.Event) -> Void)?
    
       override init(identifier: NSToolbar.Identifier) {
          super.init(identifier: identifier)
          setupUI()
          setupHandlers()
       }
    }
    
    extension MainToolbar {
    
       private func setupUI() {
          allowsUserCustomization = true
          autosavesConfiguration = true
          displayMode = .iconOnly
          toolbarDelegate.allowedItemIdentifiers = [.space, .flexibleSpace]
          toolbarDelegate.selectableItemIdentifiers = [.space, .flexibleSpace]
          toolbarDelegate.defaultItemIdentifiers = Event.toolbarIDs + [.flexibleSpace]
       }
    
       private func setupHandlers() {
          delegate = toolbarDelegate
          toolbarDelegate.makeItemCallback = { [unowned self] id, _ in
             guard let event = Event(id: id) else {
                return nil
             }
             return self.makeToolbarItem(event: event)
          }
       }
    
       private func makeToolbarItem(event: Event) -> NSToolbarItem {
          let item = NSToolbarItem(itemIdentifier: event.itemIdentifier)
          item.setHandler { [weak self] in
             guard let event = Event(id: event.itemIdentifier) else {
                return
             }
             self?.eventHandler?(event)
          }
          item.label = event.label
          item.paletteLabel = event.paletteLabel
          if event.image != nil {
             item.image = event.image
          } else if event.view != nil {
             item.view = event.view
          }
          return item
       }
    }
    
    extension MainToolbar.Event {
    
       init?(id: NSToolbarItem.Identifier) {
          guard let event = (MainToolbar.Event.allValues.filter { $0.itemIdentifier == id }).first else {
             return nil
          }
          self = event
       }
    
       static var allValues: [MainToolbar.Event] {
          return [toggleSidePanel]
       }
    
       static var toolbarIDs: [NSToolbarItem.Identifier] {
          return [toggleSidePanel].map { $0.itemIdentifier }
       }
    
       var itemIdentifier: NSToolbarItem.Identifier {
          switch self {
          case .toggleSidePanel: return NSToolbarItem.Identifier("ua.com.wavalabs.toolbar.toggleSidePanel")
          }
       }
    
       var label: String {
          switch self {
          case .toggleSidePanel: return "Toggle Side Panel"
          }
       }
    
       var view: NSView? {
          return nil
       }
    
       var image: NSImage? {
          switch self {
          case .toggleSidePanel: return NSImage(named: NSImage.Name.folder)
          }
       }
    
       var paletteLabel: String {
          return label
       }
    }
    

    File MainMenu.swift

    class MainMenu: NSMenu {
    
       enum Event {
          case quit
       }
    
       var eventHandler: ((Event) -> Void)?
    
       private lazy var applicationName = ProcessInfo.processInfo.processName
    
       init() {
          super.init(title: "")
          setupUI()
       }
    
       required init(coder decoder: NSCoder) {
          super.init(coder: decoder)
       }
    
    }
    
    
    extension MainMenu {
    
       private func setupUI() {
    
          let appMenuItem = NSMenuItem()
          appMenuItem.submenu = appMenu
    
          addItem(appMenuItem)
       }
    
       private var appMenu: NSMenu {
          let menu = NSMenu(title: "")
          menu.addItem(title: "Quit \(applicationName)", keyEquivalent: "q") { [unowned self] in
             self.eventHandler?(.quit)
          }
          return menu
       }
    
    }
    

    Convenience extensions.

    File NSMenu.swift

    extension NSMenu {
    
       @discardableResult
       public func addItem(title: String, keyEquivalent: String, handler: NSMenuItem.Handler?) -> NSMenuItem {
          let item = addItem(withTitle: title, action: nil, keyEquivalent: keyEquivalent)
          item.setHandler(handler)
          return item
       }
    
    }
    

    File NSMenuItem.swift

    extension NSMenuItem {
    
       public typealias Handler = (() -> Void)
    
       convenience init(title: String, keyEquivalent: String, handler: Handler?) {
          self.init(title: title, action: nil, keyEquivalent: keyEquivalent)
          setHandler(handler)
       }
    
       public func setHandler(_ handler: Handler?) {
          target = self
          action = #selector(wavelabsActionHandler(_:))
          if let handler = handler {
             ObjCAssociation.setCopyNonAtomic(value: handler, to: self, forKey: &OBJCAssociationKeys.actionHandler)
          }
       }
    
    }
    
    extension NSMenuItem {
    
       private struct OBJCAssociationKeys {
          static var actionHandler = "com.wavelabs.actionHandler"
       }
    
       @objc private func wavelabsActionHandler(_ sender: NSControl) {
          guard sender == self else {
             return
          }
          if let handler: Handler = ObjCAssociation.value(from: self, forKey: &OBJCAssociationKeys.actionHandler) {
             handler()
          }
       }
    }
    

    File NSToolbar.swift

    extension NSToolbar {
    
       class GenericDelegate: NSObject, NSToolbarDelegate {
    
          var selectableItemIdentifiers: [NSToolbarItem.Identifier] = []
          var defaultItemIdentifiers: [NSToolbarItem.Identifier] = []
          var allowedItemIdentifiers: [NSToolbarItem.Identifier] = []
    
          var eventHandler: ((Event) -> Void)?
          var makeItemCallback: ((_ itemIdentifier: NSToolbarItem.Identifier, _ willBeInserted: Bool) -> NSToolbarItem?)?
       }
    }
    
    extension NSToolbar.GenericDelegate {
    
       enum Event {
          case willAddItem(item: NSToolbarItem, index: Int)
          case didRemoveItem(item: NSToolbarItem)
       }
    }
    
    extension NSToolbar.GenericDelegate {
    
       func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
                    willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
          return makeItemCallback?(itemIdentifier, flag)
       }
    
       func toolbarDefaultItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] {
          return defaultItemIdentifiers
       }
    
       func toolbarAllowedItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] {
          return allowedItemIdentifiers
       }
    
       func toolbarSelectableItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] {
          return selectableItemIdentifiers
       }
    
       // MARK: Notifications
    
       func toolbarWillAddItem(_ notification: Notification) {
          if let toolbarItem = notification.userInfo?["item"] as? NSToolbarItem,
             let index = notification.userInfo?["newIndex"] as? Int {
             eventHandler?(.willAddItem(item: toolbarItem, index: index))
          }
       }
    
       func toolbarDidRemoveItem(_ notification: Notification) {
          if let toolbarItem = notification.userInfo?["item"] as? NSToolbarItem {
             eventHandler?(.didRemoveItem(item: toolbarItem))
          }
       }
    }
    

    File NSToolbarItem.swift

    extension NSToolbarItem {
    
       public typealias Handler = (() -> Void)
    
       public func setHandler(_ handler: Handler?) {
          target = self
          action = #selector(wavelabsActionHandler(_:))
          if let handler = handler {
             ObjCAssociation.setCopyNonAtomic(value: handler, to: self, forKey: &OBJCAssociationKeys.actionHandler)
          }
       }
    
    }
    
    extension NSToolbarItem {
    
       private struct OBJCAssociationKeys {
          static var actionHandler = "com.wavelabs.actionHandler"
       }
    
       @objc private func wavelabsActionHandler(_ sender: NSControl) {
          guard sender == self else {
             return
          }
          if let handler: Handler = ObjCAssociation.value(from: self, forKey: &OBJCAssociationKeys.actionHandler) {
             handler()
          }
       }
    }
    

    File ObjCAssociation.swift

    public struct ObjCAssociation {
    
       public static func value(from object: AnyObject, forKey key: UnsafeRawPointer) -> T? {
          return objc_getAssociatedObject(object, key) as? T
       }
    
       public static func setAssign(value: T?, to object: Any, forKey key: UnsafeRawPointer) {
          objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_ASSIGN)
       }
       public static func setRetainNonAtomic(value: T?, to object: Any, forKey key: UnsafeRawPointer) {
          objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
       }
       public static func setCopyNonAtomic(value: T?, to object: Any, forKey key: UnsafeRawPointer) {
          objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_COPY_NONATOMIC)
       }
       public static func setRetain(value: T?, to object: Any, forKey key: UnsafeRawPointer) {
          objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN)
       }
       public static func setCopy(value: T?, to object: Any, forKey key: UnsafeRawPointer) {
          objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_COPY)
       }
    }
    

提交回复
热议问题