頂部狀態(tài)欄菜單
(這里以QQ體驗版為例)

// 在AppDelegate.m里實現(xiàn)以下方法
#if TARGET_OS_MACCATALYST
- (void)buildMenuWithBuilder:(id<UIMenuBuilder>)builder
{
// 移除“顯示(View)”菜單
[builder removeMenuForIdentifier:UIMenuView];
// 移除“格式(Format)”菜單
[builder removeMenuForIdentifier:UIMenuFormat];
// 移除“服務(wù)(Services)”菜單
[builder removeMenuForIdentifier:UIMenuServices];
// 移除“新建(New)”菜單
[builder removeMenuForIdentifier:UIMenuNewScene];
// 添加"偏好設(shè)置",以下采用的是自定義的方案(后面再提供另一種自定義"偏好設(shè)置"菜單項的方法,免去翻譯的麻煩)
[builder removeMenuForIdentifier:UIMenuPreferences];
UIKeyCommand *preferencesItem =
[UIKeyCommand commandWithTitle:@"偏好設(shè)置" // 或:@"Preferences…"
image:nil
action:@selector(orderFrontPreferencesPanel:)
input:@","
modifierFlags:UIKeyModifierCommand
propertyList:nil];
UIMenu *preferencesMenu =
[UIMenu menuWithTitle:@"Preferences"
image:nil
identifier:UIMenuPreferences
options:UIMenuOptionsDisplayInline
children:@[preferencesItem]];
[builder insertSiblingMenu:preferencesMenu afterMenuForIdentifier:UIMenuAbout];
//[builder insertChildMenu:preferencesMenu atEndOfMenuForIdentifier:UIMenuAbout];// 多級子菜單
// 添加"應(yīng)用"菜單
UIKeyCommand *newsItem =
[UIKeyCommand commandWithTitle:@"動態(tài)"
image:nil
action:@selector(newsMenuItemClick:)
input:@"1"
modifierFlags:UIKeyModifierCommand
propertyList:nil];
UIKeyCommand *createGroupsItem =
[UIKeyCommand commandWithTitle:@"創(chuàng)建群聊"
image:nil
action:@selector(createGroupsMenuItemClick:)
input:@"2"
modifierFlags:UIKeyModifierCommand
propertyList:nil];
UIKeyCommand *addFriendItem =
[UIKeyCommand commandWithTitle:@"加好友/群"
image:nil
action:@selector(addFriendMenuItemClick:)
input:@"3"
modifierFlags:UIKeyModifierCommand
propertyList:nil];
UIMenu *appMenu =
[UIMenu menuWithTitle:@"應(yīng)用"
image:nil
identifier:@"com.qq.menu.app"
options:UIMenuOptionsDestructive
children:@[newsItem,createGroupsItem,addFriendItem]];
[builder insertSiblingMenu:appMenu afterMenuForIdentifier:UIMenuEdit];
}
- (void)orderFrontStandardAboutPanel:(UICommand *)sender
{
// 如果需要自定義"關(guān)于"窗口,就實現(xiàn)此方法
}
- (void)orderFrontPreferencesPanel:(UIKeyCommand *)sender
{
// 點擊了"偏好設(shè)置"
}
// 其他Action方法不舉例了...
#endif
另一種自定義"偏好設(shè)置"菜單項的方法,可免去翻譯的麻煩:
File -> New -> File... (或Command + N)添加一個Settings Bundle

然后在AppDelegate.m里實現(xiàn)以下方法(不需要上面創(chuàng)建UIMenu的代碼)
- (void)orderFrontPreferencesPanel:(UIKeyCommand *)sender
{
// 顯示自定義的"偏好設(shè)置"窗口
}

生成的語言:
構(gòu)建菜單時,菜單使用的語言跟隨啟動時的系統(tǒng)語言(macOS原生只會生成英文的菜單),使用 [[UIMenuSystem mainSystem] setNeedsRebuild] 后菜單將重置并調(diào)用- (void)buildMenuWithBuilder:(id<UIMenuBuilder>)builder方法,但是應(yīng)用程序啟動時系統(tǒng)是什么語言,生成的菜單就是使用什么語言的,就算你切換了別的系統(tǒng)語言
繼承關(guān)系:
UIMenuElement < UIMenu
UIMenuElement < UICommand < UIKeyCommand(相比UICommand多了個非全局的快捷鍵功能)
注意菜單項的propertyList(可以看作菜單項的唯一標識符)為空時action是不可以一樣的,否則報錯:
inserted menu has duplicate submenu, command or key command, or a key command is missing input or action
UIMenuBuilder協(xié)議:
// (待補充)
@property (nonatomic, readonly) UIMenuSystem *system;
// 獲取菜單項,字符串枚舉在UIMenu.h里
- (nullable UIMenu *)menuForIdentifier:(UIMenuIdentifier)identifier NS_SWIFT_NAME(menu(for:));
// (待補充)
- (nullable UIAction *)actionForIdentifier:(UIActionIdentifier)identifier NS_SWIFT_NAME(action(for:));
// 通過action或propertyList查找菜單項
- (nullable UICommand *)commandForAction:(SEL)action propertyList:(nullable id)propertyList NS_REFINED_FOR_SWIFT;
// 替換菜單
- (void)replaceMenuForIdentifier:(UIMenuIdentifier)replacedIdentifier withMenu:(UIMenu *)replacementMenu NS_SWIFT_NAME(replace(menu:with:));
// 遍歷子菜單
- (void)replaceChildrenOfMenuForIdentifier:(UIMenuIdentifier)parentIdentifier
fromChildrenBlock:(NSArray<UIMenuElement *> *(NS_NOESCAPE ^)(NSArray<UIMenuElement *> *))childrenBlock NS_SWIFT_NAME(replaceChildren(ofMenu:from:));
// 以展開的形式把菜單插入到指定位置(詳情見下圖)
- (void)insertSiblingMenu:(UIMenu *)siblingMenu beforeMenuForIdentifier:(UIMenuIdentifier)siblingIdentifier NS_SWIFT_NAME(insertSibling(_:beforeMenu:));
- (void)insertSiblingMenu:(UIMenu *)siblingMenu afterMenuForIdentifier:(UIMenuIdentifier)siblingIdentifier NS_SWIFT_NAME(insertSibling(_:afterMenu:));
// 以收起的形式把菜單插入到指定位置(詳情見下圖)
- (void)insertChildMenu:(UIMenu *)childMenu atStartOfMenuForIdentifier:(UIMenuIdentifier)parentIdentifier NS_SWIFT_NAME(insertChild(_:atStartOfMenu:));
- (void)insertChildMenu:(UIMenu *)childMenu atEndOfMenuForIdentifier:(UIMenuIdentifier)parentIdentifier NS_SWIFT_NAME(insertChild(_:atEndOfMenu:));
// 移除指定菜單,字符串枚舉在UIMenu.h里
- (void)removeMenuForIdentifier:(UIMenuIdentifier)removedIdentifier NS_SWIFT_NAME(remove(menu:));
Sibling Menu和Child Menu的區(qū)別(代碼示例)
UICommand *children1 =
[UICommand commandWithTitle:@"Children 1"
image:nil
action:@selector(children1Click:)
propertyList:nil];
UICommand *children2 =
[UICommand commandWithTitle:@"Children 2"
image:nil
action:@selector(children2Click:)
propertyList:nil];
UIMenu *menu =
[UIMenu menuWithTitle:@"Menu"
image:nil
identifier:@"com.qq.menu.testMenu"
options:UIMenuOptionsDisplayInline
children:@[children1,children2]];
//[builder insertSiblingMenu:menu afterMenuForIdentifier:UIMenuAbout]; // Sibling Menu
//[builder insertChildMenu:menu atEndOfMenuForIdentifier:UIMenuAbout]; // Child Menu
Sibling Menu:

Child Menu:

UIMenuElement/UICommand/UIKeyCommand:
// 標題
@property (nonatomic, copy) NSString *title API_AVAILABLE(ios(13.0));
// 顯示在標題左邊的圖標
@property (nullable, nonatomic, copy) UIImage *image API_AVAILABLE(ios(13.0));
// 鼠標在上面停留一會后出現(xiàn)的文本提示(效果見下圖Discoverability Title)
@property (nullable, nonatomic, copy) NSString *discoverabilityTitle API_AVAILABLE(ios(9.0));
// 菜單項的propertyList為空時action是不可以一樣的
@property (nullable, nonatomic, readonly) SEL action;
// 快捷鍵(a~Z、1~9...)
@property (nullable, nonatomic, readonly) NSString *input;
// 快捷鍵的修飾鍵(Command、Shift、Option...)
@property (nonatomic, readonly) UIKeyModifierFlags modifierFlags;
// 假如action是一樣的時候,使用此屬性來區(qū)分命令,可看作為標識符
@property (nullable, nonatomic, readonly) id propertyList API_AVAILABLE(ios(13.0));
// 狀態(tài)(效果見下圖UIMenuElementAttributes)
@property (nonatomic) UIMenuElementAttributes attributes API_AVAILABLE(ios(13.0));
// 標題左邊的狀態(tài)圖標(效果見下圖UIMenuElementState)
@property (nonatomic) UIMenuElementState state API_AVAILABLE(ios(13.0));
Discoverability Title(例:discoverabilityTitle = @"Discoverability Title";)

UIMenuElementAttributes(UIMenuElementAttributesHidden變?yōu)椴豢梢娏耍?/p>

UIMenuElementState:

// 快捷方式使用不同的修飾鍵執(zhí)行不同的方法,比如Command + 1執(zhí)行A方法,Command + Shift + 1執(zhí)行B方法
@property (nonatomic, readonly) NSArray<UICommandAlternate *> *alternates API_AVAILABLE(ios(13.0));
// 在上面的例子的基礎(chǔ)上修改:
// 添加"應(yīng)用"菜單
NSMutableArray *alternates = NSMutableArray.new;
[alternates addObject:[UICommandAlternate alternateWithTitle:@"動態(tài)1"
action:@selector(newsMenuItemClick1:)
modifierFlags:UIKeyModifierShift]];
[alternates addObject:[UICommandAlternate alternateWithTitle:@"動態(tài)2"
action:@selector(newsMenuItemClick2:)
modifierFlags:UIKeyModifierControl]];
UIKeyCommand *newsItem =
[UIKeyCommand commandWithTitle:@"動態(tài)"
image:nil
action:@selector(newsMenuItemClick:)
input:@"1"
modifierFlags:UIKeyModifierCommand
propertyList:nil
alternates:alternates];
默認情況(快捷方式:Command + 1):

按下Shift鍵(快捷方式:Command + Shift + 1):

按下Control鍵(快捷方式:Command + Control + 1):

鼠標跟蹤
UIHoverGestureRecognizer * hover = [[UIHoverGestureRecognizer alloc]initWithTarget:self action:@selector(hoveringWithRecognizer:)];
[View addGestureRecognizer:hover];
- (void)hoveringWithRecognizer:(UIHoverGestureRecognizer *)recognizer
{
switch (recognizer.state) {
case (UIGestureRecognizerStateBegan):
NSLog(@"鼠標進入了區(qū)域");
break;
case (UIGestureRecognizerStateEnded):
NSLog(@"鼠標離開了區(qū)域");
break;
default:
break;
}
}
窗口設(shè)置
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions API_AVAILABLE(ios(13.0)){
#if TARGET_OS_MACCATALYST
UIWindowScene *windowScene = (UIWindowScene *)scene;
// 需要乘以1.3才是實際顯示在桌面上的大小
// minimumSize不能少于515 × 370(需要乘以1.3)
// 但能通過設(shè)置maximumSize無視不能小于515 × 370(需要乘以1.3)的限制
windowScene.sizeRestrictions.minimumSize =
windowScene.sizeRestrictions.maximumSize = CGSizeMake(200 * 1.3, 130 * 1.3);
UITitlebar *titlebar = windowScene.titlebar;
// 不要顯示窗口工具欄的標題
titlebar.titleVisibility = UITitlebarTitleVisibilityHidden;
// 不要顯示窗口頂部的工具欄,需隱藏窗口工具欄的標題才能生效
titlebar.toolbar.visible = NO;
#endif
}

工具欄
需要先導(dǎo)入AppKit框架
#if TARGET_OS_MACCATALYST
#import <AppKit/AppKit.h>
#endif
遵循<NSToolbarDelegate>協(xié)議
#if TARGET_OS_MACCATALYST
@interface SceneDelegate () <NSToolbarDelegate>
#else
@interface SceneDelegate ()
#endif
然后在Scene Delegate里添加代碼,例:
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions API_AVAILABLE(ios(13.0)) {
#if TARGET_OS_MACCATALYST
UITitlebar *titlebar = self.window.windowScene.titlebar;
NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"DefaultToolbar"];
titlebar.toolbar = toolbar;
toolbar.delegate = self;
toolbar.displayMode = NSToolbarDisplayModeIconOnly;
#endif
}
#if TARGET_OS_MACCATALYST
-(NSArray<NSToolbarItemIdentifier> *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar{
return @[@"ToolbarAddItem",@"ToolbarRemoveItem"];
}
-(NSArray<NSToolbarItemIdentifier> *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar{
return @[@"ToolbarAddItem",@"ToolbarRemoveItem"];
}
- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag {
// 根據(jù)item標識返回每個具體的NSToolbarItem對象實例(設(shè)置title或image后target需要重新設(shè)置,否則出現(xiàn)不能點擊的問題)
NSToolbarItem *toolbarItem = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
toolbarItem.bordered = YES;
if ([itemIdentifier isEqualToString:@"ToolbarAddItem"]) {
toolbarItem.title = @"添加";
toolbarItem.target = self;
toolbarItem.action = @selector(toolbarAddItemClicked:);
} else if ([itemIdentifier isEqualToString:@"ToolbarRemoveItem"]) {
toolbarItem.title = @"移除";
toolbarItem.target = self;
toolbarItem.action = @selector(toolbarRemoveItemClicked:);
} else {
toolbarItem = nil;
}
return toolbarItem;
}
- (void)toolbarAddItemClicked:(NSToolbarItem *)sender{
NSLog(@"點擊了 %@",sender.title);
}
- (void)toolbarRemoveItemClicked:(NSToolbarItem *)sender{
NSLog(@"點擊了 %@",sender.title);
}
#endif

右鍵菜單
需要遵循<UIContextMenuInteractionDelegate>協(xié)議,且在視圖對象初始化時調(diào)用-addInteraction:方法
UIContextMenuInteraction *interaction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
[view addInteraction:interaction];
然后實現(xiàn)此方法
- (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction configurationForMenuAtLocation:(CGPoint)location;
例子:
@interface ViewController ()<UITableViewDelegate,UITableViewDataSource,UIContextMenuInteractionDelegate>
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableview.delegate = self;
self.tableview.dataSource = self;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 10;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"1"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:0 reuseIdentifier:@"1"];
// 視圖對象初始化時調(diào)用
if (@available(iOS 13.0, *)) {
UIContextMenuInteraction *interaction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
[cell addInteraction:interaction];
}
}
cell.textLabel.text = [NSString stringWithFormat:@"Row = %ld",indexPath.row];
return cell;
}
- (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction configurationForMenuAtLocation:(CGPoint)location API_AVAILABLE(ios(13.0))
{
return [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:nil actionProvider:^UIMenu * _Nullable(NSArray<UIMenuElement *> * _Nonnull suggestedActions) {
// 一般使用UIAction或UICommand對象
NSMutableArray <UIMenuElement *>*children = NSMutableArray.new;
[children addObject:[UIAction actionWithTitle:@"菜單項1" image:nil identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
NSLog(@"點擊了菜單項1");
}]];
[children addObject:[UICommand commandWithTitle:@"菜單項2" image:nil action:@selector(menuItem2Clicked:) propertyList:nil]];
return [UIMenu menuWithTitle:@"Menu" children:children];
}];
}
- (void)menuItem2Clicked:(UICommand *)sender API_AVAILABLE(ios(13.0))
{
NSLog(@"點擊了菜單項2");
}
@end

示例代碼:https://github.com/LeungKinKeung/MacCatalyst
如果需要使用更多AppKit的控件,請參閱《Mac Catalyst - macOS AppKit 插件》