MacOS-MacAPP loads the UI main interface through pure code without relying on storyboard/xib

I downloaded a lot of MacOS-side APP open source projects and codes on the Internet, and found that the UI is basically loaded through storyBoard or xib; using storyBoard or xib also has pitfalls. For details, refer to MacOS-MacAPP using Main.storyboard to start the view program stepping on pitfalls

But I want to create the main UIWindow in AppDelegate like the iPhone, and then set a custom rootViewController, as shown below:

After searching on the Internet for a long time, I found that there are too few reference materials, but the hard work pays off, the blogger finally solved it

How do we load the UI main interface through pure code without relying on storyboard/xib?

1. Delete the Main.storyboard or xib file in the project

To delete the Main storyboard file base name in the project Info.plist: Specify the storyboard file name loaded when the app starts; Main nib file base name: Specify the xib file name loaded when the app starts

After Xcode11, in addition to the same as before, delete the StoryboardName of SceneDelegate in the project Info.plist

For details, please refer to the blog iOS-Xcode11: Delete the default Main.storyBoard, custom UIWindow cannot be processed in AppDelegate, add SceneDelegate agent

2. Completely pure code needs to modify the main.m file. For details, please refer to the blog iOS-main.m file

Check out the APPmain.m file code of macOS here:

#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);
}

Start the project to run the breakpoint as follows:

We need to create the application NSApplication and proxy AppDelegate by ourselves, then set the proxy, and start and run the application

#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);
}

If you don’t create an application setting proxy here, you will find that when you start and run the program, the breakpoint will not enter the method in AppDelegate at all - (void)applicationDidFinishLaunching:(NSNotification *)aNotification;

However, the UI loaded through the Main.storyboard or xib file will enter, so they create the application, set the agent and run the application by default; they execute the main method, and when the APP runs, it first creates an NSApplication instance to load the storyboard/xib file. Create a custom menu/window in the storyboard/xib file. NSApplication is the AppDelegate proxy. Therefore, the applicationDidFinishLaunching method in AppDelegate will be executed to perform some custom initialization

3. Set a custom NSWindow in AppDelegate

#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

Here, there is still a big difference between creating an NSWindow class and creating a UIView in iOS. It not only needs to set the frame (initWithFrame: method), but also needs to set the styleMask parameter to confirm the window style (- (instancetype)initWithContentRect:(NSRect )contentRect styleMask:(NSWindowStyleMask)style backing:(NSBackingStoreType)backingStoreType defer:(BOOL)flag NS_DESIGNATED_INITIALIZER)

(1)contentRect:frame

(2) styleMask: window style

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: cache mode for window drawing

/* 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: Indicates delayed creation or immediate creation

4. The running results are as follows:

Of course, the above is just a simple example of using pure code to create an NSWindow as a code creation, so it is not perfect, and the NSWindow management view is directly created manually, and the scalability and maintainability are not very strong. recommend

We can compare the storyboard/xib file to load the startup view. There is no menu bar on the interface, so we need to add the menu bar ourselves

storyboard

xib

Optimization and upgrade

The effect we need is as above: (1) Customize the menu bar; (2) Create different scene Scene hierarchical management such as WindowController and ViewController

The main relationship between window controller and view controller is as follows:

1. Create MainWindowController, inherit from NSWindowController, and configure its window and root view contentViewController in the init method

#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 and NSWindow relationship; mutual reference: NSWindowController strongly references NSWindow, NSWindow non-strong reference holds a pointer to NSWindowController

(1) NSWindow.h

@property (nullable, weak) __kindof NSWindowController *windowController;

(2) in NSWindowController.h

@property (nullable, strong) NSWindow *window;

Therefore, it is not recommended to manually create and manage NSWindow in actual projects. Manual creation needs to maintain the bidirectional reference relationship between NSWindowController and NSWindow. It is recommended that NSWindow be managed by an independent NSWindowController.

2. Create MainViewController, inherited from 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

The window must have a root view, contentView

The window controller NSWindowController and NSWindow are mutually referenced, and the content view contentView of NSWindow is NSView; when NSWindowController is configured with contentViewController, the view of NSViewController is finally the contentView of the window of NSWindowController, and the window where the view is located is the window of NSWindowController.

3. In 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. Create a menu in main.m, create an application, and set a proxy

#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);
}

Final running effect

Of course, in order not to customize the menu bar, we can also load the startup UI through the Main.storyboard or xib file, but we need to delete the window in MainMenu.xib, or delete all the NSWindowController and NSViewController Scenes created by default under Main.storyboard (selected, Directly press the back key on the keyboard (❎ key), as follows, this is just to keep using the system menu

It is also necessary to pay special attention to the need to add the following code in MainWindowController.m: the same effect as the method initWithWindowNibName, so that the corresponding NSWindowController can be found by loading the nib file

//通过加载xib方式
- (NSString*)windowNibName {
    return @"MainWindowController";// this name tells AppKit which nib file to use
}

Guess you like

Origin blog.csdn.net/MinggeQingchun/article/details/116307236