有一款软件叫SimPholders,可以访问iOS开发模拟器的沙盒文件位置,最近,模仿这个功能,开发了一个小型的macOS APP可以一键访问沙盒位置,已经上架到APP Store,记录一下开发过程和上架过程。
一键直达沙盒:iSandBox-APP Store
0、初始化
xcode新建工程,并且run起来,会发现和iOS项目结构类似
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
可以通过代码的方式自定义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.
我这个App需要的窗口只有一个,所以不再详细阐述NSViewController/NSWindowContorller的用法
2、Dock菜单
在info.plist里加LSUIElement为YES可以让App启动后,图标不出现在Dock栏。
右击Dock栏会有默认菜单列表
如果要自定义右键的菜单列表,则在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");
}
效果如下
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"];
效果如下
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,通过拼接路径,可以获取到应用列表
有了应用的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里,如下显示
点击跳转到沙盒目录
- (void)openAppDocument:(ApplicationMenuItem *)menu
{
HTAppInfo *appInfo = menu.app;
NSURL *appUrl = [self getAppDocumentUrl:appInfo];
if (appUrl) {
[[NSWorkspace sharedWorkspace] openURL:appUrl];
}
}
6、上架篇
向App Store的提审过程,被拒了两次,第一次是因为上架的APP必须是沙盒App,所以在项目内要添加沙盒相关配置
另外一个原因,是因为macOS从mojava版本后,有了深色模式,所以状态栏必须要有深色模式的图标
将以上问题处理完毕后顺利上架,整个提审上架过程与iOS差不多。