react-native-tinyui 0.5.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +468 -28
- package/ios/ConcentricView/TinyuiConcentricView.h +12 -0
- package/ios/ConcentricView/TinyuiConcentricView.mm +60 -0
- package/ios/Core/Tinyui.mm +54 -0
- package/ios/Core/TinyuiComponentClasses.mm +44 -0
- package/ios/Menu/TinyuiMenuBuilder.swift +238 -0
- package/ios/Menu/TinyuiMenuProvider.swift +141 -75
- package/ios/Menu/TinyuiMenuView.mm +24 -35
- package/ios/Popover/TinyuiPopoverProvider.swift +198 -44
- package/ios/Popover/TinyuiPopoverView.mm +26 -1
- package/ios/SFSymbol/TinyuiSFSymbolEffects.h +9 -0
- package/ios/SFSymbol/TinyuiSFSymbolEffects.mm +180 -0
- package/ios/SFSymbol/TinyuiSFSymbolImage.h +8 -0
- package/ios/SFSymbol/TinyuiSFSymbolImage.mm +141 -0
- package/ios/SFSymbol/TinyuiSFSymbolShadowNode.h +31 -0
- package/ios/SFSymbol/TinyuiSFSymbolShadowNode.mm +30 -0
- package/ios/SFSymbol/TinyuiSFSymbolView.h +4 -0
- package/ios/SFSymbol/TinyuiSFSymbolView.mm +173 -0
- package/ios/Stepper/TinyuiStepperShadowNode.h +32 -0
- package/ios/Stepper/TinyuiStepperShadowNode.mm +28 -0
- package/ios/Stepper/TinyuiStepperView.h +12 -0
- package/ios/Stepper/TinyuiStepperView.mm +128 -0
- package/ios/TipKit/TinyuiTip.swift +31 -0
- package/ios/TipKit/TinyuiTipPopoverProvider.swift +276 -0
- package/ios/TipKit/TinyuiTipPopoverView.h +8 -0
- package/ios/TipKit/TinyuiTipPopoverView.mm +114 -0
- package/ios/TipKit/TinyuiTipsRuntime.swift +148 -0
- package/lib/module/ConcentricView/ConcentricViewNativeComponent.ts +14 -0
- package/lib/module/ConcentricView/index.js +29 -0
- package/lib/module/ConcentricView/index.js.map +1 -0
- package/lib/module/ConcentricView/types.js +4 -0
- package/lib/module/ConcentricView/types.js.map +1 -0
- package/lib/module/Menu/MenuNativeComponent.ts +12 -3
- package/lib/module/Menu/index.js +17 -12
- package/lib/module/Menu/index.js.map +1 -1
- package/lib/module/Menu/options.js +4 -12
- package/lib/module/Menu/options.js.map +1 -1
- package/lib/module/NativeTinyui.js.map +1 -1
- package/lib/module/Popover/index.js +4 -2
- package/lib/module/Popover/index.js.map +1 -1
- package/lib/module/SFSymbol/SFSymbolNativeComponent.ts +20 -0
- package/lib/module/SFSymbol/index.js +73 -0
- package/lib/module/SFSymbol/index.js.map +1 -0
- package/lib/module/SFSymbol/options.js +79 -0
- package/lib/module/SFSymbol/options.js.map +1 -0
- package/lib/module/SFSymbol/types.js +4 -0
- package/lib/module/SFSymbol/types.js.map +1 -0
- package/lib/module/Stepper/StepperNativeComponent.ts +29 -0
- package/lib/module/Stepper/index.js +75 -0
- package/lib/module/Stepper/index.js.map +1 -0
- package/lib/module/Stepper/types.js +4 -0
- package/lib/module/Stepper/types.js.map +1 -0
- package/lib/module/TipKit/Popover.js +89 -0
- package/lib/module/TipKit/Popover.js.map +1 -0
- package/lib/module/TipKit/TipPopoverNativeComponent.ts +37 -0
- package/lib/module/TipKit/index.js +28 -0
- package/lib/module/TipKit/index.js.map +1 -0
- package/lib/module/TipKit/types.js +4 -0
- package/lib/module/TipKit/types.js.map +1 -0
- package/lib/module/TipKit/validation.js +24 -0
- package/lib/module/TipKit/validation.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/getNativeTinyui.js.map +1 -1
- package/lib/typescript/src/ConcentricView/ConcentricViewNativeComponent.d.ts +7 -0
- package/lib/typescript/src/ConcentricView/ConcentricViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/ConcentricView/index.d.ts +4 -0
- package/lib/typescript/src/ConcentricView/index.d.ts.map +1 -0
- package/lib/typescript/src/ConcentricView/types.d.ts +6 -0
- package/lib/typescript/src/ConcentricView/types.d.ts.map +1 -0
- package/lib/typescript/src/Menu/MenuNativeComponent.d.ts +8 -4
- package/lib/typescript/src/Menu/MenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/Menu/index.d.ts +1 -1
- package/lib/typescript/src/Menu/index.d.ts.map +1 -1
- package/lib/typescript/src/Menu/options.d.ts +1 -5
- package/lib/typescript/src/Menu/options.d.ts.map +1 -1
- package/lib/typescript/src/Menu/types.d.ts +26 -7
- package/lib/typescript/src/Menu/types.d.ts.map +1 -1
- package/lib/typescript/src/NativeTinyui.d.ts +2 -0
- package/lib/typescript/src/NativeTinyui.d.ts.map +1 -1
- package/lib/typescript/src/Popover/index.d.ts.map +1 -1
- package/lib/typescript/src/Popover/types.d.ts +2 -0
- package/lib/typescript/src/Popover/types.d.ts.map +1 -1
- package/lib/typescript/src/SFSymbol/SFSymbolNativeComponent.d.ts +11 -0
- package/lib/typescript/src/SFSymbol/SFSymbolNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/SFSymbol/index.d.ts +4 -0
- package/lib/typescript/src/SFSymbol/index.d.ts.map +1 -0
- package/lib/typescript/src/SFSymbol/options.d.ts +86 -0
- package/lib/typescript/src/SFSymbol/options.d.ts.map +1 -0
- package/lib/typescript/src/SFSymbol/types.d.ts +113 -0
- package/lib/typescript/src/SFSymbol/types.d.ts.map +1 -0
- package/lib/typescript/src/Stepper/StepperNativeComponent.d.ts +21 -0
- package/lib/typescript/src/Stepper/StepperNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/Stepper/index.d.ts +4 -0
- package/lib/typescript/src/Stepper/index.d.ts.map +1 -0
- package/lib/typescript/src/Stepper/types.d.ts +24 -0
- package/lib/typescript/src/Stepper/types.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/Popover.d.ts +3 -0
- package/lib/typescript/src/TipKit/Popover.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts +38 -0
- package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/index.d.ts +10 -0
- package/lib/typescript/src/TipKit/index.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/types.d.ts +46 -0
- package/lib/typescript/src/TipKit/types.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/validation.d.ts +4 -0
- package/lib/typescript/src/TipKit/validation.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +9 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/getNativeTinyui.d.ts +1 -1
- package/lib/typescript/src/utils/getNativeTinyui.d.ts.map +1 -1
- package/package.json +37 -2
- package/{Tinyui.podspec → react-native-tinyui.podspec} +23 -2
- package/src/ConcentricView/ConcentricViewNativeComponent.ts +14 -0
- package/src/ConcentricView/index.tsx +34 -0
- package/src/ConcentricView/types.ts +6 -0
- package/src/Menu/MenuNativeComponent.ts +12 -3
- package/src/Menu/index.tsx +35 -12
- package/src/Menu/options.ts +2 -20
- package/src/Menu/types.ts +28 -7
- package/src/NativeTinyui.ts +2 -0
- package/src/Popover/index.tsx +8 -3
- package/src/Popover/types.ts +2 -0
- package/src/SFSymbol/SFSymbolNativeComponent.ts +20 -0
- package/src/SFSymbol/index.tsx +89 -0
- package/src/SFSymbol/options.ts +116 -0
- package/src/SFSymbol/types.ts +158 -0
- package/src/Stepper/StepperNativeComponent.ts +29 -0
- package/src/Stepper/index.tsx +91 -0
- package/src/Stepper/types.ts +24 -0
- package/src/TipKit/Popover.tsx +87 -0
- package/src/TipKit/TipPopoverNativeComponent.ts +37 -0
- package/src/TipKit/index.ts +47 -0
- package/src/TipKit/types.ts +57 -0
- package/src/TipKit/validation.ts +39 -0
- package/src/index.tsx +31 -0
- package/src/utils/getNativeTinyui.ts +8 -1
- package/ios/Core/TinyuiHostingHelpers.swift +0 -74
- package/ios/Menu/TinyuiMenuViewSwift.swift +0 -326
- package/ios/Popover/TinyuiPopoverViewSwift.swift +0 -88
package/ios/Core/Tinyui.mm
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#import "Tinyui.h"
|
|
2
|
+
#if TINYUI_FEATURE_TIP_KIT
|
|
3
|
+
#import "Tinyui-Swift.h"
|
|
4
|
+
#endif
|
|
2
5
|
|
|
3
6
|
@implementation Tinyui
|
|
4
7
|
|
|
@@ -16,10 +19,61 @@
|
|
|
16
19
|
#if TINYUI_FEATURE_LIQUID_GLASS_TEXT
|
|
17
20
|
[components addObject:@"LiquidGlassText"];
|
|
18
21
|
#endif
|
|
22
|
+
#if TINYUI_FEATURE_STEPPER
|
|
23
|
+
[components addObject:@"Stepper"];
|
|
24
|
+
#endif
|
|
25
|
+
#if TINYUI_FEATURE_CONCENTRIC_VIEW
|
|
26
|
+
[components addObject:@"ConcentricView"];
|
|
27
|
+
#endif
|
|
28
|
+
#if TINYUI_FEATURE_SF_SYMBOL
|
|
29
|
+
[components addObject:@"SFSymbol"];
|
|
30
|
+
#endif
|
|
31
|
+
#if TINYUI_FEATURE_TIP_KIT
|
|
32
|
+
[components addObject:@"TipKit"];
|
|
33
|
+
#endif
|
|
19
34
|
|
|
20
35
|
return components;
|
|
21
36
|
}
|
|
22
37
|
|
|
38
|
+
- (void)configureTips:(NSString *)displayFrequency
|
|
39
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
40
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
41
|
+
{
|
|
42
|
+
#if TINYUI_FEATURE_TIP_KIT
|
|
43
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
44
|
+
[TinyuiTipsRuntime configure:displayFrequency completion:^(NSError *error) {
|
|
45
|
+
if (error != nil) {
|
|
46
|
+
reject(error.userInfo[@"code"] ?: @"E_TIPS_CONFIGURATION", error.localizedDescription, error);
|
|
47
|
+
} else {
|
|
48
|
+
resolve(nil);
|
|
49
|
+
}
|
|
50
|
+
}];
|
|
51
|
+
});
|
|
52
|
+
#else
|
|
53
|
+
reject(@"E_TIP_NOT_INSTALLED", @"Enable TipKit in react-native-tinyui.components and run pod install.", nil);
|
|
54
|
+
#endif
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
- (void)invalidateTip:(NSString *)tipId
|
|
58
|
+
reason:(NSString *)reason
|
|
59
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
60
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
61
|
+
{
|
|
62
|
+
#if TINYUI_FEATURE_TIP_KIT
|
|
63
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
64
|
+
[TinyuiTipsRuntime invalidate:tipId reason:reason completion:^(NSError *error) {
|
|
65
|
+
if (error != nil) {
|
|
66
|
+
reject(error.userInfo[@"code"] ?: @"E_TIP_INVALIDATION", error.localizedDescription, error);
|
|
67
|
+
} else {
|
|
68
|
+
resolve(nil);
|
|
69
|
+
}
|
|
70
|
+
}];
|
|
71
|
+
});
|
|
72
|
+
#else
|
|
73
|
+
reject(@"E_TIP_NOT_INSTALLED", @"Enable TipKit in react-native-tinyui.components and run pod install.", nil);
|
|
74
|
+
#endif
|
|
75
|
+
}
|
|
76
|
+
|
|
23
77
|
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
24
78
|
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
25
79
|
{
|
|
@@ -35,6 +35,30 @@ TINYUI_DISABLED_COMPONENT(TinyuiPopoverView, TinyuiPopoverViewComponentDescripto
|
|
|
35
35
|
TINYUI_DISABLED_COMPONENT(TinyuiLiquidGlassTextView, TinyuiLiquidGlassTextViewComponentDescriptor)
|
|
36
36
|
#endif
|
|
37
37
|
|
|
38
|
+
#if TINYUI_FEATURE_STEPPER
|
|
39
|
+
#import "TinyuiStepperView.h"
|
|
40
|
+
#else
|
|
41
|
+
TINYUI_DISABLED_COMPONENT(TinyuiStepperView, TinyuiStepperViewComponentDescriptor)
|
|
42
|
+
#endif
|
|
43
|
+
|
|
44
|
+
#if TINYUI_FEATURE_CONCENTRIC_VIEW
|
|
45
|
+
#import "TinyuiConcentricView.h"
|
|
46
|
+
#else
|
|
47
|
+
TINYUI_DISABLED_COMPONENT(TinyuiConcentricView, TinyuiConcentricViewComponentDescriptor)
|
|
48
|
+
#endif
|
|
49
|
+
|
|
50
|
+
#if TINYUI_FEATURE_SF_SYMBOL
|
|
51
|
+
#import "TinyuiSFSymbolView.h"
|
|
52
|
+
#else
|
|
53
|
+
TINYUI_DISABLED_COMPONENT(TinyuiSFSymbolView, TinyuiSFSymbolViewComponentDescriptor)
|
|
54
|
+
#endif
|
|
55
|
+
|
|
56
|
+
#if TINYUI_FEATURE_TIP_KIT
|
|
57
|
+
#import "TinyuiTipPopoverView.h"
|
|
58
|
+
#else
|
|
59
|
+
TINYUI_DISABLED_COMPONENT(TinyuiTipPopoverView, TinyuiTipPopoverViewComponentDescriptor)
|
|
60
|
+
#endif
|
|
61
|
+
|
|
38
62
|
#undef TINYUI_DISABLED_COMPONENT
|
|
39
63
|
|
|
40
64
|
Class<RCTComponentViewProtocol> TinyuiMenuViewCls(void)
|
|
@@ -51,3 +75,23 @@ Class<RCTComponentViewProtocol> TinyuiLiquidGlassTextViewCls(void)
|
|
|
51
75
|
{
|
|
52
76
|
return TinyuiLiquidGlassTextView.class;
|
|
53
77
|
}
|
|
78
|
+
|
|
79
|
+
Class<RCTComponentViewProtocol> TinyuiStepperViewCls(void)
|
|
80
|
+
{
|
|
81
|
+
return TinyuiStepperView.class;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
Class<RCTComponentViewProtocol> TinyuiConcentricViewCls(void)
|
|
85
|
+
{
|
|
86
|
+
return TinyuiConcentricView.class;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Class<RCTComponentViewProtocol> TinyuiSFSymbolViewCls(void)
|
|
90
|
+
{
|
|
91
|
+
return TinyuiSFSymbolView.class;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Class<RCTComponentViewProtocol> TinyuiTipPopoverViewCls(void)
|
|
95
|
+
{
|
|
96
|
+
return TinyuiTipPopoverView.class;
|
|
97
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import React
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
@objc public protocol TinyuiMenuViewDelegate: AnyObject {
|
|
5
|
+
func onActionPress(id: String, title: String)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Builds UIKit menu elements from the configuration generated in JavaScript. */
|
|
9
|
+
final class TinyuiMenuBuilder {
|
|
10
|
+
private weak var delegate: TinyuiMenuViewDelegate?
|
|
11
|
+
|
|
12
|
+
init(delegate: TinyuiMenuViewDelegate?) {
|
|
13
|
+
self.delegate = delegate
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func buildMenu(title: String, config: [String: Any]) -> UIMenu {
|
|
17
|
+
print("[TinyUI][Menu] config:", config)
|
|
18
|
+
|
|
19
|
+
return UIMenu(
|
|
20
|
+
title: title,
|
|
21
|
+
children: buildChildren(from: dictionaries(in: config, key: "items"))
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private func buildChildren(
|
|
26
|
+
from dictionaries: [[String: Any]],
|
|
27
|
+
inheritedDestructive: Bool = false,
|
|
28
|
+
inheritedDisabled: Bool = false
|
|
29
|
+
) -> [UIMenuElement] {
|
|
30
|
+
// UIKit represents separators by placing adjacent groups in inline menus.
|
|
31
|
+
// Partitioning here also makes consecutive/leading/trailing dividers no-ops.
|
|
32
|
+
let groups = dictionaries.split {
|
|
33
|
+
($0["type"] as? String) == "separator"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if groups.count <= 1 {
|
|
37
|
+
return dictionaries.compactMap {
|
|
38
|
+
buildElement(
|
|
39
|
+
from: $0,
|
|
40
|
+
inheritedDestructive: inheritedDestructive,
|
|
41
|
+
inheritedDisabled: inheritedDisabled
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return groups.compactMap { group in
|
|
47
|
+
let children = group.compactMap {
|
|
48
|
+
buildElement(
|
|
49
|
+
from: $0,
|
|
50
|
+
inheritedDestructive: inheritedDestructive,
|
|
51
|
+
inheritedDisabled: inheritedDisabled
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
guard !children.isEmpty else {
|
|
55
|
+
return nil
|
|
56
|
+
}
|
|
57
|
+
return UIMenu(title: "", options: .displayInline, children: children)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private func buildElement(
|
|
62
|
+
from dictionary: [String: Any],
|
|
63
|
+
inheritedDestructive: Bool,
|
|
64
|
+
inheritedDisabled: Bool
|
|
65
|
+
) -> UIMenuElement? {
|
|
66
|
+
guard !(dictionary["hidden"] as? Bool ?? false) else {
|
|
67
|
+
return nil
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
switch dictionary["type"] as? String {
|
|
71
|
+
case "action":
|
|
72
|
+
return buildAction(
|
|
73
|
+
from: dictionary,
|
|
74
|
+
inheritedDestructive: inheritedDestructive,
|
|
75
|
+
inheritedDisabled: inheritedDisabled
|
|
76
|
+
)
|
|
77
|
+
case "submenu":
|
|
78
|
+
return buildSubmenu(
|
|
79
|
+
from: dictionary,
|
|
80
|
+
inheritedDestructive: inheritedDestructive,
|
|
81
|
+
inheritedDisabled: inheritedDisabled
|
|
82
|
+
)
|
|
83
|
+
case "section":
|
|
84
|
+
let children = buildChildren(
|
|
85
|
+
from: dictionaries(in: dictionary, key: "children"),
|
|
86
|
+
inheritedDestructive: inheritedDestructive,
|
|
87
|
+
inheritedDisabled: inheritedDisabled
|
|
88
|
+
)
|
|
89
|
+
guard !children.isEmpty else {
|
|
90
|
+
return nil
|
|
91
|
+
}
|
|
92
|
+
return UIMenu(
|
|
93
|
+
title: dictionary["title"] as? String ?? "",
|
|
94
|
+
options: .displayInline,
|
|
95
|
+
children: children
|
|
96
|
+
)
|
|
97
|
+
default:
|
|
98
|
+
return nil
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private func buildAction(
|
|
103
|
+
from dictionary: [String: Any],
|
|
104
|
+
inheritedDestructive: Bool,
|
|
105
|
+
inheritedDisabled: Bool
|
|
106
|
+
) -> UIAction {
|
|
107
|
+
let identifier = dictionary["id"] as? String ?? ""
|
|
108
|
+
let title = dictionary["title"] as? String ?? ""
|
|
109
|
+
var attributes: UIMenuElement.Attributes = []
|
|
110
|
+
|
|
111
|
+
if inheritedDisabled || (dictionary["disabled"] as? Bool ?? false) {
|
|
112
|
+
attributes.insert(.disabled)
|
|
113
|
+
}
|
|
114
|
+
if inheritedDestructive || (dictionary["destructive"] as? Bool ?? false) {
|
|
115
|
+
attributes.insert(.destructive)
|
|
116
|
+
}
|
|
117
|
+
if dictionary["hidden"] as? Bool ?? false {
|
|
118
|
+
attributes.insert(.hidden)
|
|
119
|
+
}
|
|
120
|
+
if dictionary["keepOpen"] as? Bool ?? false {
|
|
121
|
+
attributes.insert(.keepsMenuPresented)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return UIAction(
|
|
125
|
+
title: title,
|
|
126
|
+
subtitle: nonEmptyString(dictionary["subtitle"]),
|
|
127
|
+
image: image(from: dictionary),
|
|
128
|
+
identifier: nil,
|
|
129
|
+
discoverabilityTitle: nil,
|
|
130
|
+
attributes: attributes,
|
|
131
|
+
state: state(from: dictionary)
|
|
132
|
+
) { [weak delegate] _ in
|
|
133
|
+
delegate?.onActionPress(id: identifier, title: title)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private func buildSubmenu(
|
|
138
|
+
from dictionary: [String: Any],
|
|
139
|
+
inheritedDestructive: Bool,
|
|
140
|
+
inheritedDisabled: Bool
|
|
141
|
+
) -> UIMenuElement? {
|
|
142
|
+
let isDestructive =
|
|
143
|
+
inheritedDestructive || (dictionary["destructive"] as? Bool ?? false)
|
|
144
|
+
let isDisabled =
|
|
145
|
+
inheritedDisabled || (dictionary["disabled"] as? Bool ?? false)
|
|
146
|
+
let isInline = dictionary["displayInline"] as? Bool ?? false
|
|
147
|
+
|
|
148
|
+
// UIKit doesn't expose disabled/hidden attributes on UIMenu. A disabled
|
|
149
|
+
// hierarchical submenu is therefore represented by a disabled UIAction;
|
|
150
|
+
// inline submenu children can be disabled individually.
|
|
151
|
+
if isDisabled && !isInline {
|
|
152
|
+
var attributes: UIMenuElement.Attributes = [.disabled]
|
|
153
|
+
if isDestructive {
|
|
154
|
+
attributes.insert(.destructive)
|
|
155
|
+
}
|
|
156
|
+
return UIAction(
|
|
157
|
+
title: dictionary["title"] as? String ?? "",
|
|
158
|
+
subtitle: nonEmptyString(dictionary["subtitle"]),
|
|
159
|
+
image: image(from: dictionary),
|
|
160
|
+
identifier: nil,
|
|
161
|
+
discoverabilityTitle: nil,
|
|
162
|
+
attributes: attributes,
|
|
163
|
+
state: .off
|
|
164
|
+
) { _ in }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let children = buildChildren(
|
|
168
|
+
from: dictionaries(in: dictionary, key: "children"),
|
|
169
|
+
inheritedDestructive: isDestructive,
|
|
170
|
+
inheritedDisabled: isDisabled
|
|
171
|
+
)
|
|
172
|
+
guard !children.isEmpty else {
|
|
173
|
+
return nil
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
var options: UIMenu.Options = []
|
|
177
|
+
if isInline {
|
|
178
|
+
options.insert(.displayInline)
|
|
179
|
+
}
|
|
180
|
+
if isDestructive {
|
|
181
|
+
options.insert(.destructive)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return UIMenu(
|
|
185
|
+
title: dictionary["title"] as? String ?? "",
|
|
186
|
+
subtitle: nonEmptyString(dictionary["subtitle"]),
|
|
187
|
+
image: image(from: dictionary),
|
|
188
|
+
identifier: nil,
|
|
189
|
+
options: options,
|
|
190
|
+
children: children
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private func image(from dictionary: [String: Any]) -> UIImage? {
|
|
195
|
+
let image: UIImage?
|
|
196
|
+
if let assetName = nonEmptyString(dictionary["icon"]),
|
|
197
|
+
let assetImage = UIImage(named: assetName) {
|
|
198
|
+
image = assetImage
|
|
199
|
+
} else if let systemName = nonEmptyString(dictionary["systemImage"]) {
|
|
200
|
+
image = UIImage(systemName: systemName)
|
|
201
|
+
} else {
|
|
202
|
+
image = nil
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
guard let image else {
|
|
206
|
+
return nil
|
|
207
|
+
}
|
|
208
|
+
guard let tintColor = dictionary["iconColor"].flatMap(RCTConvert.uiColor) else {
|
|
209
|
+
return image.withRenderingMode(.alwaysTemplate)
|
|
210
|
+
}
|
|
211
|
+
return image.withTintColor(tintColor, renderingMode: .alwaysOriginal)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private func state(from dictionary: [String: Any]) -> UIMenuElement.State {
|
|
215
|
+
switch dictionary["state"] as? String {
|
|
216
|
+
case "on":
|
|
217
|
+
return .on
|
|
218
|
+
case "mixed":
|
|
219
|
+
return .mixed
|
|
220
|
+
default:
|
|
221
|
+
return .off
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private func dictionaries(
|
|
226
|
+
in dictionary: [String: Any],
|
|
227
|
+
key: String
|
|
228
|
+
) -> [[String: Any]] {
|
|
229
|
+
dictionary[key] as? [[String: Any]] ?? []
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private func nonEmptyString(_ value: Any?) -> String? {
|
|
233
|
+
guard let value = value as? String, !value.isEmpty else {
|
|
234
|
+
return nil
|
|
235
|
+
}
|
|
236
|
+
return value
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -1,116 +1,182 @@
|
|
|
1
|
-
import SwiftUI
|
|
2
1
|
import UIKit
|
|
2
|
+
import UIKit.UIGestureRecognizerSubclass
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* UIKit
|
|
6
|
-
*
|
|
7
|
-
* `UIHostingController`, mirroring react-native-pager-view's `PagerViewProvider`.
|
|
8
|
-
*
|
|
9
|
-
* The presenting view controller is supplied by the Objective-C++ component
|
|
10
|
-
* view so this file does not depend on React.
|
|
5
|
+
* UIButton-backed UIKit menu trigger used as the Fabric component's content
|
|
6
|
+
* view. UIButton owns the UIMenu presentation and its long-press interaction.
|
|
11
7
|
*/
|
|
12
|
-
@objc public class TinyuiMenuProvider:
|
|
8
|
+
@objc public final class TinyuiMenuProvider: UIButton {
|
|
13
9
|
private weak var delegate: TinyuiMenuViewDelegate?
|
|
14
|
-
private var
|
|
15
|
-
private var props = TinyuiMenuProps()
|
|
10
|
+
private weak var menuTouchGate: TinyuiMenuTouchGate?
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
/// React view controller hierarchy.
|
|
19
|
-
@objc public weak var parentViewController: UIViewController? {
|
|
12
|
+
@objc public var triggerView: UIView? {
|
|
20
13
|
didSet {
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
guard oldValue !== triggerView else {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
oldValue?.removeFromSuperview()
|
|
18
|
+
if let triggerView {
|
|
19
|
+
addSubview(triggerView)
|
|
23
20
|
}
|
|
21
|
+
setNeedsLayout()
|
|
24
22
|
}
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
props.triggerView = triggerView
|
|
31
|
-
}
|
|
25
|
+
@objc public convenience init(delegate: TinyuiMenuViewDelegate) {
|
|
26
|
+
self.init(frame: .zero)
|
|
27
|
+
self.delegate = delegate
|
|
32
28
|
}
|
|
33
29
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
props.menuConfig = menuConfig
|
|
38
|
-
}
|
|
30
|
+
override public init(frame: CGRect) {
|
|
31
|
+
super.init(frame: frame)
|
|
32
|
+
configure()
|
|
39
33
|
}
|
|
40
34
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
35
|
+
required init?(coder: NSCoder) {
|
|
36
|
+
super.init(coder: coder)
|
|
37
|
+
configure()
|
|
45
38
|
}
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
40
|
+
override public func didMoveToWindow() {
|
|
41
|
+
super.didMoveToWindow()
|
|
42
|
+
menuTouchGate?.presentedMenus.remove(self)
|
|
43
|
+
menuTouchGate = window.map { TinyuiMenuTouchGate.install(in: $0) }
|
|
51
44
|
}
|
|
52
45
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
override public func contextMenuInteraction(
|
|
47
|
+
_ interaction: UIContextMenuInteraction,
|
|
48
|
+
willDisplayMenuFor configuration: UIContextMenuConfiguration,
|
|
49
|
+
animator: UIContextMenuInteractionAnimating?
|
|
50
|
+
) {
|
|
51
|
+
super.contextMenuInteraction(interaction, willDisplayMenuFor: configuration, animator: animator)
|
|
52
|
+
menuTouchGate?.presentedMenus.add(self)
|
|
57
53
|
}
|
|
58
54
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
override public func contextMenuInteraction(
|
|
56
|
+
_ interaction: UIContextMenuInteraction,
|
|
57
|
+
willEndFor configuration: UIContextMenuConfiguration,
|
|
58
|
+
animator: UIContextMenuInteractionAnimating?
|
|
59
|
+
) {
|
|
60
|
+
super.contextMenuInteraction(interaction, willEndFor: configuration, animator: animator)
|
|
61
|
+
// The dismissal touch has already been filtered. New touches during the
|
|
62
|
+
// closing animation belong to the page again.
|
|
63
|
+
menuTouchGate?.presentedMenus.remove(self)
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
override public func
|
|
65
|
-
super.
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
override public func layoutSubviews() {
|
|
67
|
+
super.layoutSubviews()
|
|
68
|
+
triggerView?.frame = bounds
|
|
69
|
+
if let triggerView {
|
|
70
|
+
bringSubviewToFront(triggerView)
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
override public func
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
75
|
+
guard !isHidden, alpha > 0.01, isEnabled, isUserInteractionEnabled,
|
|
76
|
+
self.point(inside: point, with: event) else {
|
|
77
|
+
return nil
|
|
75
78
|
}
|
|
79
|
+
return self
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private func configure() {
|
|
83
|
+
backgroundColor = .clear
|
|
84
|
+
showsMenuAsPrimaryAction = true
|
|
76
85
|
}
|
|
77
86
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
@objc public func openMenu() {
|
|
88
|
+
guard isEnabled, isUserInteractionEnabled, !isHidden, alpha > 0.01,
|
|
89
|
+
window != nil, !bounds.isEmpty, menu != nil else {
|
|
81
90
|
return
|
|
82
91
|
}
|
|
92
|
+
if #available(iOS 17.4, *) {
|
|
93
|
+
performPrimaryAction()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
83
96
|
|
|
84
|
-
|
|
97
|
+
/** Applies one complete Fabric props snapshot before rebuilding the menu. */
|
|
98
|
+
@objc public func update(
|
|
99
|
+
menuConfig: [String: Any],
|
|
100
|
+
title: String,
|
|
101
|
+
disabled: Bool
|
|
102
|
+
) {
|
|
103
|
+
isEnabled = !disabled
|
|
104
|
+
isUserInteractionEnabled = !disabled
|
|
105
|
+
|
|
106
|
+
// UIButton enables its context menu interaction whenever a menu is assigned,
|
|
107
|
+
// independently of the control's enabled state. Remove it while disabled
|
|
108
|
+
// to block both primary-action and long-press presentation.
|
|
109
|
+
if disabled {
|
|
110
|
+
contextMenuInteraction?.dismissMenu()
|
|
111
|
+
menu = nil
|
|
85
112
|
return
|
|
86
113
|
}
|
|
87
114
|
|
|
88
|
-
|
|
89
|
-
|
|
115
|
+
menu = TinyuiMenuBuilder(delegate: delegate).buildMenu(
|
|
116
|
+
title: title,
|
|
117
|
+
config: menuConfig
|
|
90
118
|
)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Uses the early-event filtering approach from Expo's SystemMenuTouchGate:
|
|
124
|
+
* https://github.com/expo/expo/pull/49775
|
|
125
|
+
* TinyUI tracks its own menus through public UIButton lifecycle callbacks.
|
|
126
|
+
*/
|
|
127
|
+
final class TinyuiMenuTouchGate: UIGestureRecognizer, UIGestureRecognizerDelegate {
|
|
128
|
+
let presentedMenus = NSHashTable<TinyuiMenuProvider>.weakObjects()
|
|
129
|
+
private static let surfaceTouchHandlerClass = NSClassFromString("RCTSurfaceTouchHandler")
|
|
130
|
+
|
|
131
|
+
init() {
|
|
132
|
+
super.init(target: nil, action: nil)
|
|
133
|
+
cancelsTouchesInView = false
|
|
134
|
+
delaysTouchesBegan = false
|
|
135
|
+
delaysTouchesEnded = false
|
|
136
|
+
delegate = self
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
static func install(in window: UIWindow) -> TinyuiMenuTouchGate {
|
|
140
|
+
if let gate = window.gestureRecognizers?.first(where: { $0 is TinyuiMenuTouchGate })
|
|
141
|
+
as? TinyuiMenuTouchGate {
|
|
142
|
+
return gate
|
|
143
|
+
}
|
|
144
|
+
let gate = TinyuiMenuTouchGate()
|
|
145
|
+
window.addGestureRecognizer(gate)
|
|
146
|
+
return gate
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private func ignoreReactTouches(_ touches: Set<UITouch>, event: UIEvent) {
|
|
150
|
+
guard let window = view as? UIWindow,
|
|
151
|
+
presentedMenus.allObjects.contains(where: { $0.window === window }),
|
|
152
|
+
let handlerClass = Self.surfaceTouchHandlerClass else {
|
|
108
153
|
return
|
|
109
154
|
}
|
|
110
155
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
156
|
+
for touch in touches where touch.phase == .began {
|
|
157
|
+
var ancestor = touch.view
|
|
158
|
+
while let current = ancestor {
|
|
159
|
+
for handler in current.gestureRecognizers ?? [] where handler.isKind(of: handlerClass) {
|
|
160
|
+
handler.ignore(touch, for: event)
|
|
161
|
+
}
|
|
162
|
+
ancestor = current.superview
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive event: UIEvent) -> Bool {
|
|
168
|
+
// Filtering in touchesBegan alone is too late: RN may already have sent
|
|
169
|
+
// touchStart/onPressIn to JS. UIKit asks this delegate before delivery.
|
|
170
|
+
if let touches = event.allTouches {
|
|
171
|
+
ignoreReactTouches(touches, event: event)
|
|
172
|
+
}
|
|
173
|
+
return true
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
177
|
+
super.touchesBegan(touches, with: event)
|
|
178
|
+
// Fallback for delivery orders where RN sees the touch first.
|
|
179
|
+
ignoreReactTouches(touches, event: event)
|
|
180
|
+
state = .failed
|
|
115
181
|
}
|
|
116
182
|
}
|