前言
學過 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。
輸入項目名稱MacOSAPP,Language選擇Swift,User Innterface選擇XIB(待會刪除)
刪除Main interface默認配置
創建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,你的項目終于跑起來了。
結束語
由于需要手動設置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/