創(chuàng)建無(wú) Storyboard & XIB 的 macOS 應(yīng)用

Mac OS
前言

學(xué)過(guò) iOS 的都知道,一般不會(huì)使用 Main Storyboard 來(lái)創(chuàng)建企業(yè)級(jí)的 APP,而是在 Appdelegate 中手寫(xiě)實(shí)現(xiàn)創(chuàng)建 UIWindow 和設(shè)置 rootViewController,在切換到 MacOS 開(kāi)發(fā)過(guò)程中,習(xí)慣性得以同樣的方式創(chuàng)建 MacOS App,不過(guò)馬上我就遇到了障礙。

首先我把Info.plist中的Main interface配置項(xiàng)刪除了,并且在applicationDidFinishLaunching中創(chuàng)建了自定義的NSWindow,并且按照Mac OS的方式設(shè)置為keyWindow并且顯示,手起刀落直接Command+R,發(fā)現(xiàn)什么也沒(méi)有發(fā)生,沒(méi)有跑出任何界面,甚至連在Appdelegate中斷點(diǎn)也沒(méi)有跑。于是開(kāi)始Google、百度。搜索到的
大部分教程都只是闡明 NSWindowController、NSWindow、NSViewController 之前的關(guān)系,以及如何使用各個(gè)類(lèi)。Mac OS的資料本來(lái)就少,而設(shè)置無(wú)Storyboard并且無(wú)XIB創(chuàng)建Mac OS的教程就更少了,不過(guò)最后還是找到了問(wèn)題解決的答案。

相比較iOS項(xiàng)目目錄,Mac OS項(xiàng)目沒(méi)有main.m的入口文件,而在Appdelegate中多了一個(gè)@NSApplicationMain的注解,默認(rèn)Storyboard或XIB會(huì)關(guān)聯(lián)設(shè)置Appdelegate,而如果刪除則沒(méi)有設(shè)置入口。導(dǎo)致APP Run起來(lái)以后,并沒(méi)有調(diào)用APPdelegate。如果需要?jiǎng)?chuàng)建
無(wú) Storyboard&XIB 的 macOS 應(yīng)用就需要手寫(xiě)Main入口。知道原因了那么我們就可以擼起袖子開(kāi)干了。

創(chuàng)建Mac OS應(yīng)用

打開(kāi)Xcode->File -> New Project,選擇APP。

創(chuàng)建Mac OS應(yīng)用

輸入項(xiàng)目名稱(chēng)MacOSAPP,Language選擇Swift,User Innterface選擇XIB(待會(huì)刪除)

創(chuàng)建Mac OS應(yīng)用

刪除Main interface默認(rèn)配置

刪除Info.plist配置項(xiàng)

創(chuàng)建Main.swift

在項(xiàng)目目錄下創(chuàng)建一個(gè)main.swift文件,創(chuàng)建MainMenu和設(shè)置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 //創(chuàng)建應(yīng)用
    let delegate = AppDelegate()
    app.delegate =  delegate //配置應(yīng)用代理
    app.mainMenu = mainMenu() //配置菜單,mainMenu 函數(shù)需要前向定義,否則編譯錯(cuò)誤
    app.run() //啟動(dòng)應(yīng)用
}

如果不需要menu可以將改部分代碼去除。

配置NSWindowController、NSWindow、NSViewController

由于是純代碼工程.所以我們需要手動(dòng)創(chuàng)建自己的WindowController和ViewController.然后,在AppDelegate.swift里面對(duì)WindowController進(jìn)行實(shí)例化.注意注釋掉@NSApplicationMain.最后使用WindowController的showWindow方法把這個(gè)窗口顯示出來(lái)。

打開(kāi)AppDelegate.swift文件,在applicationDidFinishLaunching中設(shè)置自定義的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() {
        // 設(shè)置 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之間的層級(jí)關(guān)系如下:

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

由于Mac OS是多窗口的,所以在Mac OS中還加入了NSWindowController用于管理Window,其關(guān)系可以類(lèi)似iOS中UIViewController和UIView的關(guān)系。

跑起來(lái)

輕松的按下 Command+R,你的項(xiàng)目終于跑起來(lái)了。


Run結(jié)果

結(jié)束語(yǔ)

由于需要手動(dòng)設(shè)置Menu,其實(shí)建議還是建議Main interface使用XIB的方式,這種方式默認(rèn)配置好了Menu和其他關(guān)聯(lián)設(shè)置。也可以在APPdelegate中自定義設(shè)置NSWindow。減少了不必要的麻煩。本教程本著求真的態(tài)度分析和示例了如何使用純代碼創(chuàng)建MacOS應(yīng)用。如果有更好的辦法,歡迎大家在下面討論交流。

參考

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容