创建无 Storyboard & XIB 的 macOS 应用

Mac OS
前言

学过 iOS 的都知道,一般不会使用 Main Storyboard 来创建企业级的 APP,而是在 Appdelegate 中手写实现创建 UIWindow 和设置 rootViewController,在切换到 MacOS 开发过程中,习惯性得以同样的方式创建 MacOS App,不过马上我就遇到了障碍。

首先我把Info.plist中的Main interface配置项删除了,并且在applicationDidFinishLaunching中创建了自定义的NSWindow,并且按照Mac OS的方式设置为keyWindow并且显示,手起刀落直接Command+R,发现什么也没有发生,没有跑出任何界面,甚至连在Appdelegate中断点也没有跑。于是开始Google、百度。搜索到的
大部分教程都只是阐明 NSWindowController、NSWindow、NSViewController 之前的关系,以及如何使用各个类。Mac OS的资料本来就少,而设置无Storyboard并且无XIB创建Mac OS的教程就更少了,不过最后还是找到了问题解决的答案。

相比较iOS项目目录,Mac OS项目没有main.m的入口文件,而在Appdelegate中多了一个@NSApplicationMain的注解,默认Storyboard或XIB会关联设置Appdelegate,而如果删除则没有设置入口。导致APP Run起来以后,并没有调用APPdelegate。如果需要创建
无 Storyboard&XIB 的 macOS 应用就需要手写Main入口。知道原因了那么我们就可以撸起袖子开干了。

创建Mac OS应用

打开Xcode->File -> New Project,选择APP。

创建Mac OS应用

输入项目名称MacOSAPP,Language选择Swift,User Innterface选择XIB(待会删除)

创建Mac OS应用

删除Main interface默认配置

删除Info.plist配置项

创建Main.swift

在项目目录下创建一个main.swift文件,创建MainMenu和设置APPdelegate为入口。

import Foundation
import Cocoa

func mainMenu() -> NSMenu {
    let    mainMenu             =    NSMenu()
    let    mainAppMenuItem      =    NSMenuItem(title: "Application", action: nil, keyEquivalent: "")
    let    mainFileMenuItem     =    NSMenuItem(title: "File", action: nil, keyEquivalent: "")
    mainMenu.addItem(mainAppMenuItem)
    mainMenu.addItem(mainFileMenuItem)
    
    let    appMenu              =    NSMenu()
    mainAppMenuItem.submenu     =    appMenu
    
    let    appServicesMenu      =    NSMenu()
    NSApp.servicesMenu          =    appServicesMenu
    appMenu.addItem(withTitle: "About", action: nil, keyEquivalent: "")
    appMenu.addItem(NSMenuItem.separator())
    appMenu.addItem(withTitle: "Preferences...", action: nil, keyEquivalent: ",")
    appMenu.addItem(NSMenuItem.separator())
    appMenu.addItem(withTitle: "Hide", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
    appMenu.addItem({ ()->NSMenuItem in
        let m = NSMenuItem(title: "Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)), keyEquivalent: "h")
        m.keyEquivalentModifierMask = NSEvent.ModifierFlags([.command, .option])
        return m
        }())
    appMenu.addItem(withTitle: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: "")
    
    appMenu.addItem(NSMenuItem.separator())
    appMenu.addItem(withTitle: "Services", action: nil, keyEquivalent: "").submenu    =    appServicesMenu
    appMenu.addItem(NSMenuItem.separator())
    appMenu.addItem(withTitle: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
    
    let    fileMenu             =    NSMenu(title: "File")
    mainFileMenuItem.submenu    =    fileMenu
    fileMenu.addItem(withTitle: "New...", action: #selector(NSDocumentController.newDocument(_:)), keyEquivalent: "n")
    
    return mainMenu
}

autoreleasepool {
    let app =   NSApplication.shared //创建应用
    let delegate = AppDelegate()
    app.delegate =  delegate //配置应用代理
    app.mainMenu = mainMenu() //配置菜单,mainMenu 函数需要前向定义,否则编译错误
    app.run() //启动应用
}

如果不需要menu可以将改部分代码去除。

配置NSWindowController、NSWindow、NSViewController

由于是纯代码工程.所以我们需要手动创建自己的WindowController和ViewController.然后,在AppDelegate.swift里面对WindowController进行实例化.注意注释掉@NSApplicationMain.最后使用WindowController的showWindow方法把这个窗口显示出来。

打开AppDelegate.swift文件,在applicationDidFinishLaunching中设置自定义的NSWindow。代码大概如下:

var mainWindowController: NSWindowController!
    
    lazy var window: NSWindow = {
        let w = NSWindow(contentRect: NSMakeRect(0, 0, 1300 , 520), styleMask: [.titled, .resizable, .miniaturizable, .closable, .fullSizeContentView], backing: .buffered, defer: false)
        w.center()
        w.backgroundColor = NSColor(calibratedRed: 0, green: 0, blue: 0, alpha: 1)
        w.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(CGWindowLevelKey.overlayWindow)))
        w.minSize = NSMakeSize(320, 240)

        return w
    }()

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Insert code here to initialize your application
        mainWindowController = NSWindowController(window: window)
        mainWindowController.showWindow(nil)
        mainWindowController.window?.makeKeyAndOrderFront(nil)
        
        NSApplication.shared.mainWindow?.title = "Hello world"
        let scanViewCtrl = ScanViewController()
        window.contentViewController = scanViewCtrl
    }

ScanViewController.siwft

import Foundation

class ScanViewController: NSViewController {
    lazy var label: NSTextField = {
        let v = NSTextField(labelWithString: "Press the button")
        v.translatesAutoresizingMaskIntoConstraints = false

        return v
    }()


    lazy var button: NSButton = {
        let v = NSButton(frame: .zero)
        v.translatesAutoresizingMaskIntoConstraints = false

        return v
    }()

    override func loadView() {
        // 设置 ViewController 大小同 mainWindow
        guard let windowRect = NSApplication.shared.mainWindow?.frame else { return }
        view = NSView(frame: windowRect)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(label)
        view.addSubview(button)

        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -20),

            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 20),
            button.heightAnchor.constraint(equalToConstant: 30),
            button.widthAnchor.constraint(equalToConstant: 100)
            ])

        button.title = "Click me"
        button.target = self
        button.action = #selector(onClickme)
    }
    
    @objc func onClickme(_ sender: NSButton) {
        label.textColor = .red
        label.stringValue = "Yeah!"
    }
}

NSWindow、NSViewController、NSView之间的层级关系如下:

+--------------------------------------------------------------+
|                           NSWindow                           |
|  +--------------------------------------------------------+  |
|  |                    NSViewController                    |  |
|  |  +--------------------------------------------------+  |  |
|  |  |                      NSView                      |  |  |
|  |  +--------------------------------------------------+  |  |
|  +--------------------------------------------------------+  |
+--------------------------------------------------------------+

由于Mac OS是多窗口的,所以在Mac OS中还加入了NSWindowController用于管理Window,其关系可以类似iOS中UIViewController和UIView的关系。

跑起来

轻松的按下 Command+R,你的项目终于跑起来了。


Run结果

结束语

由于需要手动设置Menu,其实建议还是建议Main interface使用XIB的方式,这种方式默认配置好了Menu和其他关联设置。也可以在APPdelegate中自定义设置NSWindow。减少了不必要的麻烦。本教程本着求真的态度分析和示例了如何使用纯代码创建MacOS应用。如果有更好的办法,欢迎大家在下面讨论交流。

参考

https://mikulove.com/2017/06/30/macos-xue-xi-bi-ji-shi-yong-chun-dai-ma-gou-jian-mac-ying-yong/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343