287 lines
11 KiB
Objective-C
287 lines
11 KiB
Objective-C
//
|
|
// ContextMenu.m
|
|
// reactnativeuimenu
|
|
//
|
|
// Created by Matthew Iannucci on 10/6/19.
|
|
// Copyright © 2019 Matthew Iannucci. All rights reserved.
|
|
//
|
|
|
|
#import "ContextMenuView.h"
|
|
#import <React/UIView+React.h>
|
|
|
|
@interface ContextMenuView ()
|
|
|
|
- (UIMenuElement*) createMenuElementForAction:(ContextMenuAction *)action atIndexPath:(NSUInteger) idx API_AVAILABLE(ios(13.0));
|
|
|
|
@end
|
|
|
|
@implementation ContextMenuView {
|
|
BOOL _cancelled;
|
|
UIView *_customView;
|
|
}
|
|
|
|
- (id) init {
|
|
self = [super init];
|
|
|
|
if (@available(iOS 14.0, *)) {
|
|
self.contextMenuInteractionEnabled = true;
|
|
} else {
|
|
// Fallback on earlier versions
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex
|
|
{
|
|
if ([subview.nativeID isEqualToString:@"ContextMenuPreview"]) {
|
|
_customView = subview;
|
|
return;
|
|
}
|
|
|
|
[super insertReactSubview:subview atIndex:atIndex];
|
|
|
|
if (@available(iOS 13.0, *)) {
|
|
UIContextMenuInteraction* contextInteraction =
|
|
[[UIContextMenuInteraction alloc] initWithDelegate:self];
|
|
|
|
[subview addInteraction:contextInteraction];
|
|
}
|
|
}
|
|
|
|
- (void)removeReactSubview:(UIView *)subview
|
|
{
|
|
[super removeReactSubview:subview];
|
|
}
|
|
|
|
- (void)didUpdateReactSubviews
|
|
{
|
|
[super didUpdateReactSubviews];
|
|
}
|
|
|
|
- (void)layoutSubviews
|
|
{
|
|
[super layoutSubviews];
|
|
}
|
|
|
|
- (nullable UIContextMenuConfiguration *)contextMenuInteraction:(nonnull UIContextMenuInteraction *)interaction configurationForMenuAtLocation:(CGPoint)location API_AVAILABLE(ios(13.0)) {
|
|
if (_disabled) {
|
|
return nil;
|
|
}
|
|
|
|
return [UIContextMenuConfiguration
|
|
configurationWithIdentifier:nil
|
|
previewProvider:_customView == nil ? nil : ^(){
|
|
UIViewController* viewController = [[UIViewController alloc] init];
|
|
viewController.preferredContentSize = self->_customView.frame.size;
|
|
viewController.view = self->_customView;
|
|
return viewController;
|
|
}
|
|
actionProvider:^UIMenu * _Nullable(NSArray<UIMenuElement *> * _Nonnull suggestedActions) {
|
|
NSMutableArray* actions = [[NSMutableArray alloc] init];
|
|
|
|
[self.actions enumerateObjectsUsingBlock:^(ContextMenuAction* thisAction, NSUInteger idx, BOOL *stop) {
|
|
UIMenuElement *menuElement = [self createMenuElementForAction:thisAction atIndexPath:[NSArray arrayWithObject:@(idx)]];
|
|
[actions addObject:menuElement];
|
|
}];
|
|
|
|
return [UIMenu menuWithTitle:self.title children:actions];
|
|
}];
|
|
}
|
|
|
|
- (void)contextMenuInteraction:(UIContextMenuInteraction *)interaction willDisplayMenuForConfiguration:(UIContextMenuConfiguration *)configuration animator:(id<UIContextMenuInteractionAnimating>)animator API_AVAILABLE(ios(13.0)) {
|
|
_cancelled = true;
|
|
}
|
|
|
|
- (void)contextMenuInteraction:(UIContextMenuInteraction *)interaction willPerformPreviewActionForMenuWithConfiguration:(nonnull UIContextMenuConfiguration *)configuration animator:(nonnull id<UIContextMenuInteractionCommitAnimating>)animator API_AVAILABLE(ios(13.0)) {
|
|
if (self.onPreviewPress) {
|
|
self.onPreviewPress(@{});
|
|
}
|
|
}
|
|
|
|
- (void)contextMenuInteraction:(UIContextMenuInteraction *)interaction willEndForConfiguration:(UIContextMenuConfiguration *)configuration animator:(id<UIContextMenuInteractionAnimating>)animator API_AVAILABLE(ios(13.0)) {
|
|
if (_cancelled && self.onCancel) {
|
|
self.onCancel(@{});
|
|
}
|
|
}
|
|
|
|
- (UIPreviewParameters *)getPreviewParams {
|
|
UIPreviewParameters* previewParams = [[UIPreviewParameters alloc] init];
|
|
|
|
if (_previewBackgroundColor != nil) {
|
|
previewParams.backgroundColor = _previewBackgroundColor;
|
|
}
|
|
|
|
if (self.borderRadius > -1 ||
|
|
self.borderTopLeftRadius > -1 ||
|
|
self.borderTopRightRadius > -1 ||
|
|
self.borderBottomRightRadius > -1 ||
|
|
self.borderBottomLeftRadius > -1) {
|
|
|
|
CGFloat radius = self.borderRadius > -1 ? self.borderRadius : 0;
|
|
CGFloat topLeftRadius = self.borderTopLeftRadius > -1 ? self.borderTopLeftRadius : radius;
|
|
CGFloat topRightRadius = self.borderTopRightRadius > -1 ? self.borderTopRightRadius : radius;
|
|
CGFloat bottomRightRadius = self.borderBottomRightRadius > -1 ? self.borderBottomRightRadius : radius;
|
|
CGFloat bottomLeftRadius = self.borderBottomLeftRadius > -1 ? self.borderBottomLeftRadius : radius;
|
|
|
|
UIBezierPath *path = [UIBezierPath bezierPath];
|
|
CGRect bounds = self.bounds;
|
|
|
|
[path moveToPoint: CGPointMake(CGRectGetMinX(bounds) + topLeftRadius, CGRectGetMinY(bounds))];
|
|
|
|
[path addLineToPoint: CGPointMake(CGRectGetMaxX(bounds) - topRightRadius, CGRectGetMinY(bounds))];
|
|
[path addQuadCurveToPoint: CGPointMake(CGRectGetMaxX(bounds), CGRectGetMinY(bounds) + topRightRadius)
|
|
controlPoint: CGPointMake(CGRectGetMaxX(bounds), CGRectGetMinY(bounds))];
|
|
|
|
[path addLineToPoint: CGPointMake(CGRectGetMaxX(bounds), CGRectGetMaxY(bounds) - bottomRightRadius)];
|
|
[path addQuadCurveToPoint: CGPointMake(CGRectGetMaxX(bounds) - bottomRightRadius, CGRectGetMaxY(bounds))
|
|
controlPoint: CGPointMake(CGRectGetMaxX(bounds), CGRectGetMaxY(bounds))];
|
|
|
|
[path addLineToPoint: CGPointMake(CGRectGetMinX(bounds) + bottomLeftRadius, CGRectGetMaxY(bounds))];
|
|
[path addQuadCurveToPoint: CGPointMake(CGRectGetMinX(bounds), CGRectGetMaxY(bounds) - bottomLeftRadius)
|
|
controlPoint: CGPointMake(CGRectGetMinX(bounds), CGRectGetMaxY(bounds))];
|
|
|
|
[path addLineToPoint: CGPointMake(CGRectGetMinX(bounds), CGRectGetMinY(bounds) + topLeftRadius)];
|
|
[path addQuadCurveToPoint: CGPointMake(CGRectGetMinX(bounds) + topLeftRadius, CGRectGetMinY(bounds))
|
|
controlPoint: CGPointMake(CGRectGetMinX(bounds), CGRectGetMinY(bounds))];
|
|
|
|
[path closePath];
|
|
|
|
previewParams.visiblePath = path;
|
|
}
|
|
|
|
|
|
if (self.disableShadow) {
|
|
previewParams.shadowPath = [UIBezierPath bezierPath];
|
|
}
|
|
|
|
return previewParams;
|
|
}
|
|
|
|
- (UITargetedPreview *)contextMenuInteraction:(UIContextMenuInteraction *)interaction previewForHighlightingMenuWithConfiguration:(UIContextMenuConfiguration *)configuration API_AVAILABLE(ios(13.0)) {
|
|
UIPreviewTarget* previewTarget = [[UIPreviewTarget alloc] initWithContainer:self center:self.reactSubviews.firstObject.center];
|
|
|
|
|
|
return [[UITargetedPreview alloc] initWithView:self.reactSubviews.firstObject
|
|
parameters:[self getPreviewParams]
|
|
target:previewTarget];
|
|
}
|
|
|
|
- (UITargetedPreview *)contextMenuInteraction:(UIContextMenuInteraction *)interaction previewForDismissingMenuWithConfiguration:(UIContextMenuConfiguration *)configuration API_AVAILABLE(ios(13.0)) {
|
|
UIView *hostView = self.reactSubviews.firstObject;
|
|
|
|
if (!hostView.window) {
|
|
// The view is no longer available; it was unmounted.
|
|
return nil;
|
|
}
|
|
|
|
UIPreviewTarget* previewTarget = [[UIPreviewTarget alloc] initWithContainer:self center:hostView.center];
|
|
|
|
return [[UITargetedPreview alloc] initWithView:hostView
|
|
parameters:[self getPreviewParams]
|
|
target:previewTarget];
|
|
}
|
|
|
|
- (UIMenuElement*) createMenuElementForAction:(ContextMenuAction *)action atIndexPath:(NSArray<NSNumber *> *)indexPath {
|
|
UIMenuElement* menuElement = nil;
|
|
UIImage *iconImage = nil;
|
|
|
|
if (action.icon != nil) {
|
|
UIColor *iconColor;
|
|
|
|
if (action.iconColor != nil) {
|
|
iconColor = action.iconColor;
|
|
} else {
|
|
// Default color depends on dark mode
|
|
if (@available(iOS 13.0, *)) {
|
|
UIUserInterfaceStyle currentStyle = [UITraitCollection currentTraitCollection].userInterfaceStyle;
|
|
iconColor = (currentStyle == UIUserInterfaceStyleDark) ? [UIColor whiteColor] : [UIColor blackColor];
|
|
}
|
|
// Fallback for iOS < 13
|
|
else {
|
|
iconColor = [UIColor blackColor];
|
|
}
|
|
}
|
|
// Use custom icon from Assets.xcassets
|
|
iconImage = [[UIImage imageNamed:action.icon] imageWithTintColor:iconColor];
|
|
} else {
|
|
// Use system icon from SF Symbols
|
|
iconImage = [UIImage systemImageNamed:action.systemIcon];
|
|
}
|
|
|
|
UIColor *titleColor = nil;
|
|
if (action.titleColor != nil) {
|
|
titleColor = action.titleColor;
|
|
} else {
|
|
// Default title color depends on dark mode
|
|
if (@available(iOS 13.0, *)) {
|
|
UIUserInterfaceStyle currentStyle = [UITraitCollection currentTraitCollection].userInterfaceStyle;
|
|
titleColor = (currentStyle == UIUserInterfaceStyleDark) ? [UIColor whiteColor] : [UIColor blackColor];
|
|
} else {
|
|
titleColor = [UIColor blackColor];
|
|
}
|
|
}
|
|
|
|
if (action.actions != nil && action.actions.count > 0) {
|
|
NSMutableArray<UIMenuElement*> *children = [[NSMutableArray alloc] init];
|
|
[action.actions enumerateObjectsUsingBlock:^(ContextMenuAction * _Nonnull childAction, NSUInteger childIdx, BOOL * _Nonnull stop) {
|
|
id nextIndexPath = [indexPath arrayByAddingObject:@(childIdx)];
|
|
UIMenuElement *childElement = [self createMenuElementForAction:childAction atIndexPath:nextIndexPath];
|
|
if (childElement != nil) {
|
|
[children addObject:childElement];
|
|
}
|
|
}];
|
|
|
|
UIMenuOptions actionMenuOptions =
|
|
(action.inlineChildren ? UIMenuOptionsDisplayInline : 0) |
|
|
(action.destructive ? UIMenuOptionsDestructive : 0);
|
|
UIMenu *actionMenu = [UIMenu menuWithTitle:action.title
|
|
image:iconImage
|
|
identifier:nil
|
|
options:actionMenuOptions
|
|
children:children];
|
|
|
|
if (@available(iOS 15.0, *)) {
|
|
actionMenu.subtitle = action.subtitle;
|
|
}
|
|
|
|
menuElement = actionMenu;
|
|
} else {
|
|
UIAction* actionMenuItem =
|
|
[UIAction actionWithTitle:action.title image:iconImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
|
|
if (self.onPress != nil) {
|
|
self->_cancelled = false;
|
|
self.onPress(@{
|
|
@"index": [indexPath lastObject],
|
|
@"indexPath": indexPath,
|
|
@"name": action.title,
|
|
});
|
|
}
|
|
}];
|
|
|
|
if (@available(iOS 15.0, *)) {
|
|
actionMenuItem.subtitle = action.subtitle;
|
|
}
|
|
|
|
actionMenuItem.attributes =
|
|
(action.destructive ? UIMenuElementAttributesDestructive : 0) |
|
|
(action.disabled ? UIMenuElementAttributesDisabled : 0);
|
|
|
|
actionMenuItem.state =
|
|
action.selected ? UIMenuElementStateOn : UIMenuElementStateOff;
|
|
|
|
// Apply titleColor using attributedTitle (iOS 16+ only)
|
|
if (titleColor != nil && @available(iOS 16.0, *)) {
|
|
NSDictionary *attributes = @{NSForegroundColorAttributeName: titleColor};
|
|
NSAttributedString *attributedTitle = [[NSAttributedString alloc] initWithString:action.title attributes:attributes];
|
|
[actionMenuItem setValue:attributedTitle forKey:@"attributedTitle"];
|
|
}
|
|
|
|
menuElement = actionMenuItem;
|
|
}
|
|
|
|
return menuElement;
|
|
}
|
|
|
|
@end
|