MacOS-MacAPP通过纯代码不依赖storyboard/xib加载UI主界面

在网上下载了很多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
}

猜你喜欢

转载自blog.csdn.net/MinggeQingchun/article/details/116307236
今日推荐