macOS APP从零到上架

macOS APP从零到上架

Posted by Ted on November 6, 2019

有一款软件叫SimPholders,可以访问iOS开发模拟器的沙盒文件位置,最近,模仿这个功能,开发了一个小型的macOS APP可以一键访问沙盒位置,已经上架到APP Store,记录一下开发过程和上架过程。

一键直达沙盒:iSandBox-APP Store

img

0、初始化

xcode新建工程,并且run起来,会发现和iOS项目结构类似

img

AppDelegate:里面有App启动和终止的代理方法:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
}


- (void)applicationWillTerminate:(NSNotification *)aNotification {
    // Insert code here to tear down your application
}

ViewController:继承自NSViewController,是项目启动后的第一个窗口视图。

1、NSViewController/NSWindowContorller

在iOS上,APP只有一个Window,所有的View都在这个唯一的Window上,所以我们不需要管理Window,但是,在macOS上可以有多个窗口Window,所以相对应的有NSWindow和NSWindowContorller这样的类来管理Window。这里的Window指的是左上角有扩大缩小关闭按钮的窗口。

通过Main.storybord的箭头导向,指向的是主Window,然后将第一个页面指向为ViewController。我们一般在ViewController内管理我们自己的View

img

可以通过代码的方式自定义WindowController和ViewController

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
     FirstWindowController *firstWindowC = [[FirstWindowController alloc]init];
     NSUInteger style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable ;
     NSWindow *window0 = [[NSWindow alloc]initWithContentRect:CGRectMake(0, 0, 500, 200) styleMask:style backing:NSBackingStoreBuffered defer:YES];
    firstWindowC = [[FirstWindowController alloc]initWithWindow:window0];
    [firstWindowC.window center];
    [firstWindowC.window orderFront:nil];
    firstWindowC.window.backgroundColor = [NSColor redColor];
    
    MainViewController *vc = [[MainViewController alloc]init];
    NSView *view = [[NSView alloc]initWithFrame:CGRectMake(0, 0, 200, 100)];
    view.wantsLayer = YES;
    view.layer.backgroundColor = [NSColor yellowColor].CGColor;
    vc.view = view;
    firstWindowC.window.contentViewController = vc;
}

有一点需要注意的是,如果MainViewController的初始化不是通过☑️Xib来初始化,会报错:

 -[NSNib _initWithNibNamed:bundle:options:] could not load the nibName: MainViewController in bundle (null).

尝试在控制台打印这个 VC 的 view,也无法得到相关信息。原因在于macOS 中创建 NSViewController 不会自动创建 view.View默认也不会创建layer,所以需要自定义View.

img

我这个App需要的窗口只有一个,所以不再详细阐述NSViewController/NSWindowContorller的用法

2、Dock菜单

在info.plist里加LSUIElement为YES可以让App启动后,图标不出现在Dock栏。

右击Dock栏会有默认菜单列表

img

如果要自定义右键的菜单列表,则在appdelegate里面添加方法

-(NSMenu *)applicationDockMenu:(NSApplication *)sender{
    NSMenu * menu = [[NSMenu alloc]initWithTitle:@"Menu"];
    
    // title是名称,action是点击后操作,keyEquivalent是快捷键
    NSMenuItem * item1 = [[NSMenuItem alloc]initWithTitle:@"菜单1" action:@selector(click) keyEquivalent:@""];
    item1.target = self;
    NSMenuItem * item2 = [[NSMenuItem alloc]initWithTitle:@"菜单2" action:@selector(click) keyEquivalent:@""];
    item2.target = self;
    NSMenuItem * item3 = [[NSMenuItem alloc]initWithTitle:@"菜单3" action:@selector(click) keyEquivalent:@""];
    NSMenu * subMenu = [[NSMenu alloc]initWithTitle:@"subMenu"];
    NSMenuItem * item4 = [[NSMenuItem alloc]initWithTitle:@"菜单4" action:@selector(click) keyEquivalent:@""];
    item4.target = self;
    [subMenu addItem:item4];
    [menu addItem:item1];
    [menu addItem:item2];
    [menu addItem:item3];
    [menu setSubmenu:subMenu forItem:item3];
    return menu;
}

- (void)click{
    NSLog(@"did click");
}

效果如下

img

3、状态栏

状态栏的菜单是我这个APP最重要的UI,因为沙盒APP都要显示在这里。

@property (nonatomic, strong) NSStatusItem *statusItem;  // 状态栏配置
@property (nonatomic, strong) NSMenu *mainMenu; // 状态栏图标点击后的菜单显示

状态栏图标的配置

- (void)customStatusItem{
    _statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
    
    // status栏的图片,16*16pt
    _statusItem.button.image = [NSImage imageNamed:@"status_bar"];
    
    // 点击后的status栏的图片,一般用白色的
    _statusItem.button.alternateImage = [NSImage imageNamed:@"status_bar_white"];
    
    _statusItem.menu = self.mainMenu;
}

菜单栏配置

    NSMenuItem *aboutItem  = [[NSMenuItem alloc] initWithTitle:@"关于iSandBox" action:@selector(appAbout) keyEquivalent:@""];
    aboutItem.tag = about_Tag;
    aboutItem.target = self;
    [self.mainMenu addItem:aboutItem];
  
    [self.mainMenu addItem:[NSMenuItem separatorItem]];
    
    [self.mainMenu addItemWithTitle:@"退出" action:@selector(terminate:) keyEquivalent:@"q"];

效果如下

img

4、获取模拟器

在mac的终端执行

xcrun simctl list -j devices

能够获取到如下的信息

{
  "devices" : {
    "com.apple.CoreSimulator.SimRuntime.iOS-13-1" : [
      {
        "state" : "Shutdown",
        "isAvailable" : true,
        "name" : "iPhone 8",
        "udid" : "12BD0613-9BFF-4305-B20B-898A8221A9FB"
      },
      {
        "state" : "Shutdown",
        "isAvailable" : true,
        "name" : "iPhone 8 Plus",
        "udid" : "4F454B1A-5CE6-4CAD-A47F-6CFE7DFDBA1D"
      },
      {
        "state" : "Shutdown",
        "isAvailable" : true,
        "name" : "iPhone 11",
        "udid" : "6A579513-24EF-4983-BB68-644F4195551D"
      },
      {
        "state" : "Booted",
        "isAvailable" : true,
        "name" : "iPhone 11 Pro",
        "udid" : "433B9894-56CC-430E-A9FB-C16A773551C9"
      },
      {
        "state" : "Shutdown",
        "isAvailable" : true,
        ...

能够获取到模拟器的状态和Udid。

在代码中,我们不能使用这样的命令来获取,因为xcrun实际上相当于是快捷方式,必现找到xcode路径,找到simctl的实际path

        NSTask *task = [NSTask new];
        NSString *path = [NSString stringWithFormat:@"%@/Contents/Developer/usr/bin/simctl",xcodeURL.path];
        
        [task setLaunchPath:path];
        [task setArguments: @[@"list", @"-j", @"devices"]];
        
        NSPipe *output = [NSPipe new];
        task.standardOutput = output;

        [task launch];
        [task waitUntilExit];

        NSData *data = output.fileHandleForReading.readDataToEndOfFile;
        NSDictionary *resultJson = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];

resultJson就是模拟器列表的字典数据。

5、获取应用

udid有什么用呢,通过udid我们就能获取到应用列表,应用列表在下面这个路径

file:///Users/haozhicao/Library/Developer/CoreSimulator/Devices/4F454B1A-5CE6-4CAD-A47F-6CFE7DFDBA1D/data/Containers/Bundle/Application/

其中,4F454B1A-5CE6-4CAD-A47F-6CFE7DFDBA1D就是udid,通过拼接路径,可以获取到应用列表

img

有了应用的path,我们就能获取到应用的info.pliset,从而获取相关信息

        NSURL *appInfoPath = [_url URLByAppendingPathComponent:@"Info.plist"];
        NSDictionary *infoDict = [NSDictionary dictionaryWithContentsOfURL:appInfoPath];
        NSString *bundleId = infoDict[@"CFBundleIdentifier"];
        NSString *bundleDisplayName = infoDict[@"CFBundleDisplayName"] ?: infoDict[@"CFBundleName"] ;
        NSString *bundleShortVersion = infoDict[@"CFBundleShortVersionString"];
        NSString *bundleVersion = infoDict[@"CFBundleVersion"];
        NSString *icon = ((NSArray *)infoDict[@"CFBundleIcons"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"]).firstObject;

图标、应用名称、版本号都可以获取到。

将获取到的信息,自定义一个NSMenuItem插入到mainMenu里,如下显示

img

点击跳转到沙盒目录

- (void)openAppDocument:(ApplicationMenuItem *)menu
{
    HTAppInfo *appInfo = menu.app;
    NSURL *appUrl = [self getAppDocumentUrl:appInfo];
    if (appUrl) {
        [[NSWorkspace sharedWorkspace] openURL:appUrl];
    }
}

img

6、上架篇

向App Store的提审过程,被拒了两次,第一次是因为上架的APP必须是沙盒App,所以在项目内要添加沙盒相关配置

img

另外一个原因,是因为macOS从mojava版本后,有了深色模式,所以状态栏必须要有深色模式的图标

img

将以上问题处理完毕后顺利上架,整个提审上架过程与iOS差不多。