在网上下载了很多MacOS端的APP开源项目和代码,发现基本都是通过storyBoard或xib加载UI;使用storyBoard或xib也是有坑的,具体参考MacOS-MacAPP使用Main.storyboard启动视图程序踩坑
但是我想和iPhone一样在AppDelegate中创建主UIWindow,然后设置自定义的rootViewController,如下图:
在网上找了很久,发现可参考的资料太少了,但是功夫不负有心人啊,博主最终解决了
我们如何通过纯代码而不依赖storyboard/xib加载UI主界面呢?
1、删除项目中的Main.storyboard或者xib文件
要在项目Info.plist中删除Main storyboard file base name:指定应用启动时加载的storyboard文件名;Main nib file base name:指定应用启动时加载的xib文件名
Xcode11之后,除了与以前一样,还要在项目Info.plist中删除SceneDelegate的StoryboardName
具体可参考博客iOS-Xcode11: 删除默认Main.storyBoard, 自定义UIWindow不能在AppDelegate中处理,新增SceneDelegate代理
2、完全纯代码需要修改main.m文件,具体可参考博客iOS-main.m文件
这里查看macOS的APPmain.m文件代码如下:
#import <Cocoa/Cocoa.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
}
return NSApplicationMain(argc, argv);
}
启动项目运行断点如下:
我们需要自行创建应用NSApplication和代理AppDelegate,然后设置代理,并启动运行应用
#import <Cocoa/Cocoa.h>
#import "AppDelegate.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
//创建应用
NSApplication *application = [NSApplication sharedApplication];
//创建代理
AppDelegate *appDelegate = [[AppDelegate alloc]init];
//配置应用代理
[application setDelegate:appDelegate];
//运行应用
[application run];
}
return NSApplicationMain(argc, argv);
}
如果这里不自己创建应用设置代理,会发现启动运行程序,断点压根就不会进入AppDelegate中的方法- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
而通过Main.storyboard或者xib文件加载的UI却会进入,因此他们是默认做了创建应用和设置代理并且运行应用的操作;他们执行main方法,APP运行时首先创建NSApplication实例加载storyboard/xib文件,创建storyboard/xib文件中自定义的菜单/window。NSApplication是AppDelegate代理。因此会执行AppDelegate中的applicationDidFinishLaunching方法进行自定义的一些初始化
3、在AppDelegate中设置自定义的NSWindow
#import "AppDelegate.h"
@interface AppDelegate ()
@property (strong, nonatomic) NSWindow *window;
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSRect frame = CGRectMake(0, 0, 300, 400);
NSUInteger style = NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSWindowStyleMaskResizable;
/*
contentRect: frame
styleMask: 窗口风格
backing: 窗口绘制缓存模式
defer: 延迟创建还是立马创建
*/
self.window = [[NSWindow alloc] initWithContentRect:frame styleMask:style backing:NSBackingStoreBuffered defer:YES];
self.window.title = @"My Window";
self.window.backgroundColor = [NSColor redColor];
//窗口居中
[self.window center];
//窗口显示
[self.window makeKeyAndOrderFront:self];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
// Insert code here to tear down your application
}
@end
在这里,创建NSWindow类和iOS中的创建UIView还是有何大区别的,它不仅仅只需要设置frame(initWithFrame:方法),还需要设置styleMask参数确认窗口样式风格(- (instancetype)initWithContentRect:(NSRect)contentRect styleMask:(NSWindowStyleMask)style backing:(NSBackingStoreType)backingStoreType defer:(BOOL)flag NS_DESIGNATED_INITIALIZER)
(1)contentRect:frame
(2)styleMask:窗口风格
typedef NS_OPTIONS(NSUInteger, NSWindowStyleMask) {
NSWindowStyleMaskBorderless = 0, //没有顶部titilebar边框
NSWindowStyleMaskTitled = 1 << 0, //有顶部titilebar边框
NSWindowStyleMaskClosable = 1 << 1, //带有关闭按钮
NSWindowStyleMaskMiniaturizable = 1 << 2, //带有最小化按钮
NSWindowStyleMaskResizable = 1 << 3, //恢复按钮
/* Specifies a window with textured background. Textured windows generally don't draw a top border line under the titlebar/toolbar. To get that line, use the NSUnifiedTitleAndToolbarWindowMask mask.
*/
NSWindowStyleMaskTexturedBackground API_DEPRECATED("Textured window style should no longer be used", macos(10.2, 11.0)) = 1 << 8, //带纹理背景的window,文字,标题栏没有边框线。如果需要线,要使用 NSUnifiedTitleAndToolbarWindowMask
/* Specifies a window whose titlebar and toolbar have a unified look - that is, a continuous background. Under the titlebar and toolbar a horizontal separator line will appear.
*/
NSWindowStyleMaskUnifiedTitleAndToolbar = 1 << 12, //标题栏和toolBar 下有统一的分割线
/* When set, the window will appear full screen. This mask is automatically toggled when toggleFullScreen: is called.
*/
NSWindowStyleMaskFullScreen API_AVAILABLE(macos(10.7)) = 1 << 14, //全屏显示
/* If set, the contentView will consume the full size of the window; it can be combined with other window style masks, but is only respected for windows with a titlebar.
Utilizing this mask opts-in to layer-backing. Utilize the contentLayoutRect or auto-layout contentLayoutGuide to layout views underneath the titlebar/toolbar area.
*/
NSWindowStyleMaskFullSizeContentView API_AVAILABLE(macos(10.10)) = 1 << 15, //contentView会充满整个窗口
/* 下面样式只适用于NSPanel及其子类 */
/* The following are only applicable for NSPanel (or a subclass thereof)
*/
NSWindowStyleMaskUtilityWindow = 1 << 4,
NSWindowStyleMaskDocModalWindow = 1 << 6,
NSWindowStyleMaskNonactivatingPanel = 1 << 7, // Specifies that a panel that does not activate the owning application
NSWindowStyleMaskHUDWindow API_AVAILABLE(macos(10.6)) = 1 << 13 // Specifies a heads up display panel //用于头部显示的panel
};
(3)backing:窗口绘制的缓存模式
/* Types of window backing stores.
*/
typedef NS_ENUM(NSUInteger, NSBackingStoreType) {
/* NSBackingStoreRetained and NSBackingStoreNonretained have effectively been synonyms of NSBackingStoreBuffered since OS X Mountain Lion. Please switch to the equivalent NSBackingStoreBuffered.
*/
NSBackingStoreRetained API_DEPRECATED_WITH_REPLACEMENT("NSBackingStoreBuffered", macos(10.0,10.13)) = 0, // 兼容老系统参数,基本很少用到
NSBackingStoreNonretained API_DEPRECATED_WITH_REPLACEMENT("NSBackingStoreBuffered", macos(10.0,10.13)) = 1, //不缓存直接绘制
NSBackingStoreBuffered = 2, //缓存绘制
};
(4)defer:表示延迟创建还是立即创建
4、运行结果如下:
当然,上述只是使用纯代码很简单的创建了一个NSWindow作为代码创建的简单示例,因此并不是很完善,而且是直接通过手动创建NSWindow管理视图的,可扩展性维护性也不是很强,并不推荐
我们可以对比storyboard/xib文件加载启动视图,界面上并没有菜单栏,因此我们还需要自己添加菜单栏
storyboard
xib
优化升级
我们需要的效果如上:(1)自定义菜单栏;(2)创建WindowController和ViewController 等不同场景Scene分层管理
窗口控制器,视图控制器主要关系如下:
1、创建MainWindowController,继承自NSWindowController,在init方法中配置它的window和根视图contentViewController
#import "MainWindowController.h"
#import "MainViewController.h"
@interface MainWindowController ()
@property (nonatomic, strong)MainViewController *viewController;
@end
@implementation MainWindowController
- (MainViewController *)viewController {
if (!_viewController) {
_viewController = [[MainViewController alloc]init];
}
return _viewController;
}
- (instancetype)init{
if (self == [super init]) {
/*窗口控制器NSWindowController
1、实际项目中不推荐手动创建管理NSWindow,手动创建需要维护NSWindowController和NSWindow之间的双向引用关系,带来管理复杂性
2、xib加载NSWindow
【1】显示window过程:(1)NSApplication运行后加载storyboard/xib文件(2)创建window对象(3)APP启动完成,使当前window成为keyWindow
【2】关闭window过程:(1)执行NSWindow的close方法(2)最后执行orderOut方法
3、storyboard加载NSWindow
【1】执行完NSWindow的init方法,没有依次执行orderFront,makeKey方法,直接执行makeKeyAndOrderFront方法(等价同时执行orderFront和makeKey方法)
【2】window显示由NSWindowController执行showWindow方法显示
4、NSWindowController和NSWindow关系;互相引用:NSWindowController强引用NSWindow,NSWindow非强引用持有NSWindowController的指针
【1】NSWindow.h中
@property (nullable, weak) __kindof NSWindowController *windowController;
【2】NSWindowController.h中
@property (nullable, strong) NSWindow *window;
*/
NSRect frame = CGRectMake(0, 0, 600, 400);
NSUInteger style = NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSWindowStyleMaskResizable;
self.window = [[NSWindow alloc]initWithContentRect:frame styleMask:style backing:NSBackingStoreBuffered defer:YES];
self.window.title = @"My Window";
//设置window
self.window.windowController = self;
[self.window setRestorable:NO];
//设置contentViewController
self.contentViewController = self.viewController;
// [self.window.contentView addSubview:self.viewController.view];
[self.window center];
}
return self;
}
- (void)windowDidLoad {
[super windowDidLoad];
// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
}
@end
NSWindowController和NSWindow关系;互相引用:NSWindowController强引用NSWindow,NSWindow非强引用持有NSWindowController的指针
(1)NSWindow.h中
@property (nullable, weak) __kindof NSWindowController *windowController;
(2)NSWindowController.h中
@property (nullable, strong) NSWindow *window;
因此实际项目中不推荐手动创建管理NSWindow,手动创建需要维护NSWindowController和NSWindow之间的双向引用关系,推荐NSWindow由独立的NSWindowController去管理
2、创建MainViewController,继承自NSViewController
#import "MainViewController.h"
@interface MainViewController ()
@end
@implementation MainViewController
- (instancetype)initWithNibName:(NSNibName)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
NSRect frame = CGRectMake(0, 0, 600, 400);
NSView *view = [[NSView alloc]initWithFrame:frame];
self.view = view;
[self setSUbViews];
return self;
}
- (void)setSUbViews {
NSButton *button = [NSButton buttonWithTitle:@"Show " target:self action:@selector(showView:)];
button.frame = CGRectMake(200, 50, 100, 60);
[button setButtonType:NSPushOnPushOffButton];
button.bezelStyle = NSRoundedBezelStyle;
[self.view addSubview:button];
}
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)showView:(NSButton *)button{
NSLog(@"点击我");
}
@end
窗口必须有一个根视图,即contentView
窗口控制器NSWindowController和NSWindow之间互为引用关系,NSWindow的内容视图contentView为NSView;当NSWindowController配置了contentViewController同时,NSViewController的view最终就是NSWindowController的window的contentView,而view所在的window的就是NSWindowController的window。
3、在AppDelegate中
#import "AppDelegate.h"
#import "MainWindowController.h"
@interface AppDelegate ()
@property (nonatomic, strong)MainWindowController *windowController;
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
[self.windowController showWindow:self];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
// Insert code here to tear down your application
}
- (MainWindowController *)windowController {
if (!_windowController) {
_windowController = [[MainWindowController alloc]init];
}
return _windowController;
}
@end
4、在main.m中创建菜单,创建应用,设置代理
#import <Cocoa/Cocoa.h>
#import "AppDelegate.h"
NSMenu *mainMenu() {
NSMenu *mainMenu = [NSMenu new];
//应用和File菜单
NSMenuItem *mainAppMainItem = [[NSMenuItem alloc]initWithTitle:@"Application" action:nil keyEquivalent:@""];
NSMenuItem *mainFileMenuItem = [[NSMenuItem alloc]initWithTitle:@"File" action:nil keyEquivalent:@""];
[mainMenu addItem:mainAppMainItem];
[mainMenu addItem:mainFileMenuItem];
//应用的子菜单
NSMenu *appMenu = [NSMenu new];
mainAppMainItem.submenu = appMenu;
NSMenu *appServiceMenu = [NSMenu new];
NSApp.servicesMenu = appServiceMenu;
[appMenu addItemWithTitle:@"About" action:nil keyEquivalent:@""];
[appMenu addItem:[NSMenuItem separatorItem]];
[appMenu addItemWithTitle:@"Preferences..." action:nil keyEquivalent:@""];
[appMenu addItem:[NSMenuItem separatorItem]];
[appMenu addItemWithTitle:@"Hide" action:@selector(hide:) keyEquivalent:@"h"];
NSMenuItem *hideOthersItem = [[NSMenuItem alloc]initWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"];
hideOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand + NSEventModifierFlagOption;
[appMenu addItem:hideOthersItem];
[appMenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@"h"];
[appMenu addItem:[NSMenuItem separatorItem]];
[appMenu addItemWithTitle:@"Services" action:nil keyEquivalent:@""].submenu = appServiceMenu;
[appMenu addItem:[NSMenuItem separatorItem]];
[appMenu addItemWithTitle:@"Quit" action:@selector(terminate:) keyEquivalent:@"q"];
//File的子菜单
NSMenu *fileMenu = [[NSMenu alloc]initWithTitle:@"File"];
mainFileMenuItem.submenu = fileMenu;
[fileMenu addItemWithTitle:@"New..." action:@selector(newDocument:) keyEquivalent:@"n"];
return mainMenu;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
//创建应用
NSApplication *application = [NSApplication sharedApplication];
//创建代理
AppDelegate *appDelegate = [[AppDelegate alloc]init];
//配置应用代理
[application setDelegate:appDelegate];
//配置菜单
application.mainMenu = mainMenu();
//运行应用
[application run];
}
return NSApplicationMain(argc, argv);
}
最后运行效果
当然为了不用自定义菜单栏,我们也可以通过Main.storyboard或者xib文件加载启动UI,但是我们需要删除MainMenu.xib中的window,或者删除Main.storyboard下默认创建的所有NSWindowController和NSViewController Scene(选中,直接按下键盘上的回退键即❎键),如下,这样仅仅是为了保持使用系统菜单
还需要特别注意的是需要在MainWindowController.m中增加如下代码:和方法initWithWindowNibName效果一样,这样就是通过加载nib文件来找到对应的NSWindowController
//通过加载xib方式
- (NSString*)windowNibName {
return @"MainWindowController";// this name tells AppKit which nib file to use
}