Mac Catalyst - macOS 控件

顶部状态栏菜单

(这里以QQ体验版为例)


MacCatalyst - Menu Bar.png
// 在AppDelegate.m里实现以下方法

#if TARGET_OS_MACCATALYST
- (void)buildMenuWithBuilder:(id<UIMenuBuilder>)builder
{
    // 移除“显示(View)”菜单
    [builder removeMenuForIdentifier:UIMenuView];
    // 移除“格式(Format)”菜单
    [builder removeMenuForIdentifier:UIMenuFormat];
    // 移除“服务(Services)”菜单
    [builder removeMenuForIdentifier:UIMenuServices];
    // 移除“新建(New)”菜单
    [builder removeMenuForIdentifier:UIMenuNewScene];
    
    // 添加"偏好设置",以下采用的是自定义的方案(后面再提供另一种自定义"偏好设置"菜单项的方法,免去翻译的麻烦)
    [builder removeMenuForIdentifier:UIMenuPreferences];
    UIKeyCommand *preferencesItem =
    [UIKeyCommand commandWithTitle:@"偏好设置" // 或:@"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];// 多级子菜单
    
    // 添加"应用"菜单
    UIKeyCommand *newsItem =
    [UIKeyCommand commandWithTitle:@"动态"
                             image:nil
                            action:@selector(newsMenuItemClick:)
                             input:@"1"
                     modifierFlags:UIKeyModifierCommand
                      propertyList:nil];
    
    UIKeyCommand *createGroupsItem =
    [UIKeyCommand commandWithTitle:@"创建群聊"
                             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:@"应用"
                    image:nil
               identifier:@"com.qq.menu.app"
                  options:UIMenuOptionsDestructive
                 children:@[newsItem,createGroupsItem,addFriendItem]];
    
    [builder insertSiblingMenu:appMenu afterMenuForIdentifier:UIMenuEdit];
}

- (void)orderFrontStandardAboutPanel:(UICommand *)sender 
{
    // 如果需要自定义"关于"窗口,就实现此方法
}

- (void)orderFrontPreferencesPanel:(UIKeyCommand *)sender
{
    // 点击了"偏好设置"
}

// 其他Action方法不举例了...
#endif

另一种自定义"偏好设置"菜单项的方法,可免去翻译的麻烦:

File -> New -> File... (或Command + N)添加一个Settings Bundle

MacCatalyst - Settings Bundle.png

然后在AppDelegate.m里实现以下方法(不需要上面创建UIMenu的代码)

- (void)orderFrontPreferencesPanel:(UIKeyCommand *)sender
{
    // 显示自定义的"偏好设置"窗口
}
MacCatalyst - Preferences.png

生成的语言:

构建菜单时,菜单使用的语言跟随启动时的系统语言(macOS原生只会生成英文的菜单),使用 [[UIMenuSystem mainSystem] setNeedsRebuild] 后菜单将重置并调用- (void)buildMenuWithBuilder:(id<UIMenuBuilder>)builder方法,但是应用程序启动时系统是什么语言,生成的菜单就是使用什么语言的,就算你切换了别的系统语言

继承关系:

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协议:

// (待补充)
@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的区别(代码示例)

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:

MacCatalyst - Sibling Menu.png

Child Menu:

MacCatalyst - Child Menu.png

UIMenuElement/UICommand/UIKeyCommand:

// 标题
@property (nonatomic, copy) NSString *title API_AVAILABLE(ios(13.0));
// 显示在标题左边的图标
@property (nullable, nonatomic, copy) UIImage *image API_AVAILABLE(ios(13.0));
// 鼠标在上面停留一会后出现的文本提示(效果见下图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是一样的时候,使用此属性来区分命令,可看作为标识符
@property (nullable, nonatomic, readonly) id propertyList API_AVAILABLE(ios(13.0));
// 状态(效果见下图UIMenuElementAttributes)
@property (nonatomic) UIMenuElementAttributes attributes API_AVAILABLE(ios(13.0));
// 标题左边的状态图标(效果见下图UIMenuElementState)
@property (nonatomic) UIMenuElementState state API_AVAILABLE(ios(13.0));

Discoverability Title(例:discoverabilityTitle = @"Discoverability Title";)

MacCatalyst - Discoverability Title.png

UIMenuElementAttributes(UIMenuElementAttributesHidden变为不可见了):

MacCatalyst - UIMenuElementAttributes.png

UIMenuElementState:

MacCatalyst - UIMenuElementState.png
// 快捷方式使用不同的修饰键执行不同的方法,比如Command + 1执行A方法,Command + Shift + 1执行B方法
@property (nonatomic, readonly) NSArray<UICommandAlternate *> *alternates API_AVAILABLE(ios(13.0));

// 在上面的例子的基础上修改:

// 添加"应用"菜单
NSMutableArray *alternates = NSMutableArray.new;
[alternates addObject:[UICommandAlternate alternateWithTitle:@"动态1"
                                                      action:@selector(newsMenuItemClick1:)
                                               modifierFlags:UIKeyModifierShift]];
[alternates addObject:[UICommandAlternate alternateWithTitle:@"动态2"
                                                      action:@selector(newsMenuItemClick2:)
                                               modifierFlags:UIKeyModifierControl]];

UIKeyCommand *newsItem =
[UIKeyCommand commandWithTitle:@"动态"
                         image:nil
                        action:@selector(newsMenuItemClick:)
                         input:@"1"
                 modifierFlags:UIKeyModifierCommand
                  propertyList:nil
                    alternates:alternates];

默认情况(快捷方式:Command + 1):

MacCatalyst - Key Command Alternates Defalut.png

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

MacCatalyst - Key Command Alternates Shift.png

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

MacCatalyst - Key Command Alternates Control.png

鼠标跟踪

UIHoverGestureRecognizer * hover = [[UIHoverGestureRecognizer alloc]initWithTarget:self action:@selector(hoveringWithRecognizer:)];
[View addGestureRecognizer:hover];

- (void)hoveringWithRecognizer:(UIHoverGestureRecognizer *)recognizer
{
    switch (recognizer.state) {
        case (UIGestureRecognizerStateBegan):
            NSLog(@"鼠标进入了区域");
            break;
        case (UIGestureRecognizerStateEnded):
            NSLog(@"鼠标离开了区域");
            break;
        default:
            break;
    }
}

窗口设置

- (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)
    // 但能通过设置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
}
MacCatalyst - Toolbar Hidden.png

工具栏

需要先导入AppKit框架

#if TARGET_OS_MACCATALYST
#import <AppKit/AppKit.h>
#endif

遵循<NSToolbarDelegate>协议

#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 {
    
    // 根据item标识返回每个具体的NSToolbarItem对象实例(设置title或image后target需要重新设置,否则出现不能点击的问题)
    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
MacCatalyst - Toolbar.png

右键菜单

需要遵循<UIContextMenuInteractionDelegate>协议,且在视图对象初始化时调用-addInteraction:方法

UIContextMenuInteraction *interaction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
[view addInteraction:interaction];

然后实现此方法

- (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"];
        // 视图对象初始化时调用
        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
MacCatalyst - Mouse Right Menu.png

示例代码:https://github.com/LeungKinKeung/MacCatalyst

如果需要使用更多AppKit的控件,请参阅《Mac Catalyst - macOS AppKit 插件》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容