react-native-tinyui 0.1.1
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/LICENSE +20 -0
- package/README.md +172 -0
- package/Tinyui.podspec +85 -0
- package/ios/Core/Tinyui.h +8 -0
- package/ios/Core/Tinyui.mm +30 -0
- package/ios/Core/TinyuiComponentClasses.mm +27 -0
- package/ios/Core/TinyuiHostingHelpers.swift +74 -0
- package/ios/Menu/TinyuiMenuProvider.swift +110 -0
- package/ios/Menu/TinyuiMenuView.h +14 -0
- package/ios/Menu/TinyuiMenuView.mm +120 -0
- package/ios/Menu/TinyuiMenuViewSwift.swift +368 -0
- package/ios/Popover/TinyuiPopoverProvider.swift +111 -0
- package/ios/Popover/TinyuiPopoverView.h +14 -0
- package/ios/Popover/TinyuiPopoverView.mm +124 -0
- package/ios/Popover/TinyuiPopoverViewSwift.swift +88 -0
- package/lib/module/Menu.js +43 -0
- package/lib/module/Menu.js.map +1 -0
- package/lib/module/Menu.types.js +4 -0
- package/lib/module/Menu.types.js.map +1 -0
- package/lib/module/MenuNativeComponent.ts +14 -0
- package/lib/module/MenuOptions.js +73 -0
- package/lib/module/MenuOptions.js.map +1 -0
- package/lib/module/NativeTinyui.js +5 -0
- package/lib/module/NativeTinyui.js.map +1 -0
- package/lib/module/Popover.js +71 -0
- package/lib/module/Popover.js.map +1 -0
- package/lib/module/Popover.types.js +4 -0
- package/lib/module/Popover.types.js.map +1 -0
- package/lib/module/PopoverNativeComponent.ts +13 -0
- package/lib/module/PopoverPrimitives.js +33 -0
- package/lib/module/PopoverPrimitives.js.map +1 -0
- package/lib/module/getNativeTinyui.js +30 -0
- package/lib/module/getNativeTinyui.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/react-native.d.js +2 -0
- package/lib/module/react-native.d.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/Menu.d.ts +4 -0
- package/lib/typescript/src/Menu.d.ts.map +1 -0
- package/lib/typescript/src/Menu.types.d.ts +63 -0
- package/lib/typescript/src/Menu.types.d.ts.map +1 -0
- package/lib/typescript/src/MenuNativeComponent.d.ts +14 -0
- package/lib/typescript/src/MenuNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/MenuOptions.d.ts +7 -0
- package/lib/typescript/src/MenuOptions.d.ts.map +1 -0
- package/lib/typescript/src/NativeTinyui.d.ts +11 -0
- package/lib/typescript/src/NativeTinyui.d.ts.map +1 -0
- package/lib/typescript/src/Popover.d.ts +7 -0
- package/lib/typescript/src/Popover.d.ts.map +1 -0
- package/lib/typescript/src/Popover.types.d.ts +35 -0
- package/lib/typescript/src/Popover.types.d.ts.map +1 -0
- package/lib/typescript/src/PopoverNativeComponent.d.ts +13 -0
- package/lib/typescript/src/PopoverNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/PopoverPrimitives.d.ts +21 -0
- package/lib/typescript/src/PopoverPrimitives.d.ts.map +1 -0
- package/lib/typescript/src/getNativeTinyui.d.ts +13 -0
- package/lib/typescript/src/getNativeTinyui.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +7 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +175 -0
- package/src/Menu.tsx +52 -0
- package/src/Menu.types.ts +77 -0
- package/src/MenuNativeComponent.ts +14 -0
- package/src/MenuOptions.ts +146 -0
- package/src/NativeTinyui.ts +12 -0
- package/src/Popover.tsx +92 -0
- package/src/Popover.types.ts +49 -0
- package/src/PopoverNativeComponent.ts +13 -0
- package/src/PopoverPrimitives.tsx +36 -0
- package/src/getNativeTinyui.ts +38 -0
- package/src/index.tsx +23 -0
- package/src/react-native.d.ts +5 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Darkce
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# react-native-tinyui
|
|
2
|
+
|
|
3
|
+
Dependency-free, native iOS `Menu` and `Popover` components for React Native's
|
|
4
|
+
New Architecture. The public API follows normal React composition patterns;
|
|
5
|
+
the native UI is implemented with SwiftUI behind Fabric components.
|
|
6
|
+
|
|
7
|
+
The initial component behavior is inspired by
|
|
8
|
+
[`@expo/ui`](https://github.com/expo/expo/tree/main/packages/expo-ui), but this
|
|
9
|
+
package does not require Expo Modules or any other runtime package. The
|
|
10
|
+
React Native ↔ SwiftUI bridge follows the approach used by
|
|
11
|
+
[`react-native-pager-view`](https://github.com/callstack/react-native-pager-view):
|
|
12
|
+
a Fabric component view hosts the SwiftUI view through a `UIHostingController`.
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- React Native with the New Architecture enabled
|
|
17
|
+
- iOS 17 or newer
|
|
18
|
+
- `react` and `react-native` are the only peer dependencies
|
|
19
|
+
|
|
20
|
+
This package intentionally ships only iOS native code. Android and web are not
|
|
21
|
+
supported targets.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm install react-native-tinyui
|
|
27
|
+
cd ios && pod install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Optional component selection
|
|
31
|
+
|
|
32
|
+
To reduce the native code compiled into your app, list only the components you
|
|
33
|
+
use in your app's `package.json`:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"react-native-tinyui": {
|
|
38
|
+
"components": ["Menu"]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Available components:
|
|
44
|
+
|
|
45
|
+
- `Menu`
|
|
46
|
+
- `Popover`
|
|
47
|
+
|
|
48
|
+
Both components are enabled by default. Use an empty `components` array to
|
|
49
|
+
compile only the TurboModule core. Run `pod install` again whenever this list
|
|
50
|
+
changes. Rendering a component whose native part was not selected throws an
|
|
51
|
+
error that names the missing component.
|
|
52
|
+
|
|
53
|
+
Component-specific JavaScript entry points are also available:
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { Menu } from 'react-native-tinyui/menu';
|
|
57
|
+
import { Popover } from 'react-native-tinyui/popover';
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The root `react-native-tinyui` imports remain supported for backwards
|
|
61
|
+
compatibility. JavaScript entry points provide cleaner dependency boundaries;
|
|
62
|
+
`react-native-tinyui.components` controls which native source files are compiled.
|
|
63
|
+
|
|
64
|
+
## Menu
|
|
65
|
+
|
|
66
|
+
`Menu` uses `children` as its trigger and receives its native menu entries
|
|
67
|
+
through the `options` prop. Without `onPrimaryAction`, a tap opens the menu.
|
|
68
|
+
When `onPrimaryAction` is present, a tap invokes it and a long press opens the
|
|
69
|
+
menu.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { Menu } from 'react-native-tinyui';
|
|
73
|
+
|
|
74
|
+
<Menu
|
|
75
|
+
accessibilityLabel="Document actions"
|
|
76
|
+
onPrimaryAction={() => openDocument()}
|
|
77
|
+
options={[
|
|
78
|
+
{
|
|
79
|
+
title: 'Rename',
|
|
80
|
+
systemImage: 'square.and.pencil',
|
|
81
|
+
onSelect: rename,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
title: 'Pinned',
|
|
85
|
+
state: 'on',
|
|
86
|
+
systemImage: 'pin',
|
|
87
|
+
onSelect: togglePinned,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: 'submenu',
|
|
91
|
+
title: 'Share',
|
|
92
|
+
systemImage: 'square.and.arrow.up',
|
|
93
|
+
options: [
|
|
94
|
+
{ title: 'Copy link', onSelect: copyLink },
|
|
95
|
+
{ title: 'Invite people', onSelect: invitePeople },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
{ type: 'divider' },
|
|
99
|
+
{
|
|
100
|
+
title: 'Delete',
|
|
101
|
+
destructive: true,
|
|
102
|
+
systemImage: 'trash',
|
|
103
|
+
onSelect: remove,
|
|
104
|
+
},
|
|
105
|
+
]}
|
|
106
|
+
>
|
|
107
|
+
<View style={styles.button}>
|
|
108
|
+
<Text>Actions</Text>
|
|
109
|
+
</View>
|
|
110
|
+
</Menu>;
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Actions are the default option type. Use `type: 'submenu'`, `type: 'section'`,
|
|
114
|
+
or `type: 'divider'` for structural entries; nested entries use their own
|
|
115
|
+
`options` array. SF Symbols are passed with `systemImage`; missing symbols
|
|
116
|
+
simply render no image.
|
|
117
|
+
|
|
118
|
+
Action options support `subtitle`, `state`, `destructive`, `disabled`, `hidden`,
|
|
119
|
+
and `keepOpen`. Use `titleColor` to customize the title, or `icon` to load an
|
|
120
|
+
image from the containing app's asset catalog; `iconColor` tints either an asset
|
|
121
|
+
image or an SF Symbol. These colors accept React Native `ColorValue`s, including
|
|
122
|
+
dynamic and semantic iOS colors. Submenus support the same label fields plus
|
|
123
|
+
`destructive`, `disabled`, `hidden`, and `displayInline`.
|
|
124
|
+
|
|
125
|
+
## Popover
|
|
126
|
+
|
|
127
|
+
`Popover` supports standard controlled and uncontrolled React state. Its content
|
|
128
|
+
remains a normal interactive React Native view tree hosted by a real
|
|
129
|
+
`UIPopoverPresentationController`.
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import { Popover, PopoverClose } from 'react-native-tinyui';
|
|
133
|
+
|
|
134
|
+
<Popover
|
|
135
|
+
defaultOpen={false}
|
|
136
|
+
attachmentAnchor="bottom"
|
|
137
|
+
arrowEdge="top"
|
|
138
|
+
contentSize={{ width: 320, height: 220 }}
|
|
139
|
+
onOpenChange={(open) => console.log({ open })}
|
|
140
|
+
content={({ close }) => (
|
|
141
|
+
<View style={styles.content}>
|
|
142
|
+
<Text>Any React Native content can be rendered here.</Text>
|
|
143
|
+
<PopoverClose close={close} style={styles.doneButton}>
|
|
144
|
+
<Text>Done</Text>
|
|
145
|
+
</PopoverClose>
|
|
146
|
+
</View>
|
|
147
|
+
)}
|
|
148
|
+
>
|
|
149
|
+
<View style={styles.button}>
|
|
150
|
+
<Text>Show details</Text>
|
|
151
|
+
</View>
|
|
152
|
+
</Popover>;
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The trigger is `children` — tapping it opens the popover. The popover body is
|
|
156
|
+
the `content` prop, which can be a node or a render function receiving
|
|
157
|
+
`{ close }` to dismiss the popover from within. `PopoverClose` (also available
|
|
158
|
+
as `Popover.Close`) renders a `Pressable` that calls `close` when pressed.
|
|
159
|
+
|
|
160
|
+
Use `open` with `onOpenChange` for a controlled popover, or `defaultOpen` for an
|
|
161
|
+
uncontrolled one. `contentSize` defaults to 320 × 240 points and can be updated
|
|
162
|
+
while the popover is visible.
|
|
163
|
+
|
|
164
|
+
## Contributing
|
|
165
|
+
|
|
166
|
+
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
|
167
|
+
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
|
|
168
|
+
- [Code of conduct](CODE_OF_CONDUCT.md)
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
package/Tinyui.podspec
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "pathname"
|
|
3
|
+
|
|
4
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
5
|
+
|
|
6
|
+
components = {
|
|
7
|
+
"Menu" => {
|
|
8
|
+
:source_files => "ios/Menu/**/*.{h,m,mm,swift,cpp}",
|
|
9
|
+
:definition => "TINYUI_FEATURE_MENU=1",
|
|
10
|
+
:frameworks => ["UIKit"],
|
|
11
|
+
},
|
|
12
|
+
"Popover" => {
|
|
13
|
+
:source_files => "ios/Popover/**/*.{h,m,mm,swift,cpp}",
|
|
14
|
+
:definition => "TINYUI_FEATURE_POPOVER=1",
|
|
15
|
+
:frameworks => ["UIKit"],
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
configured_components = nil
|
|
20
|
+
app_path = ENV["APP_PATH"]
|
|
21
|
+
|
|
22
|
+
unless app_path.nil? || app_path.empty?
|
|
23
|
+
app_root = Pathname.new(app_path)
|
|
24
|
+
unless app_root.absolute?
|
|
25
|
+
app_root = Pod::Config.instance.installation_root.join(app_root)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
app_package_path = app_root.join("package.json").cleanpath
|
|
29
|
+
if app_package_path.file?
|
|
30
|
+
app_package = JSON.parse(File.read(app_package_path))
|
|
31
|
+
tinyui_config = app_package["react-native-tinyui"]
|
|
32
|
+
|
|
33
|
+
unless tinyui_config.nil?
|
|
34
|
+
unless tinyui_config.is_a?(Hash)
|
|
35
|
+
raise "react-native-tinyui in #{app_package_path} must be an object"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
configured_components = tinyui_config["components"]
|
|
39
|
+
unless configured_components.is_a?(Array) && configured_components.all? { |component| component.is_a?(String) }
|
|
40
|
+
raise "react-native-tinyui.components in #{app_package_path} must be an array of strings"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
selected_component_names = configured_components.nil? ? components.keys : configured_components.uniq
|
|
47
|
+
|
|
48
|
+
unknown_components = selected_component_names - components.keys
|
|
49
|
+
unless unknown_components.empty?
|
|
50
|
+
raise "Unknown react-native-tinyui components: #{unknown_components.join(', ')}. " \
|
|
51
|
+
"Available components: #{components.keys.join(', ')}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if defined?(Pod::UI)
|
|
55
|
+
selection = selected_component_names.empty? ? "Core only" : selected_component_names.join(", ")
|
|
56
|
+
source = configured_components.nil? ? "defaults" : "package.json"
|
|
57
|
+
Pod::UI.puts "[TinyUI] Enabled components from #{source}: #{selection}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
selected_components = selected_component_names.map { |name| components.fetch(name) }
|
|
61
|
+
source_files = ["ios/Core/**/*.{h,m,mm,swift,cpp}"] + selected_components.map { |component| component[:source_files] }
|
|
62
|
+
definitions = selected_components.map { |component| component[:definition] }
|
|
63
|
+
frameworks = selected_components.flat_map { |component| component.fetch(:frameworks, []) }.uniq
|
|
64
|
+
|
|
65
|
+
Pod::Spec.new do |s|
|
|
66
|
+
s.name = "Tinyui"
|
|
67
|
+
s.version = package["version"]
|
|
68
|
+
s.summary = package["description"]
|
|
69
|
+
s.homepage = package["homepage"]
|
|
70
|
+
s.license = package["license"]
|
|
71
|
+
s.authors = package["author"]
|
|
72
|
+
|
|
73
|
+
s.platforms = { :ios => "17.0" }
|
|
74
|
+
s.source = { :git => "https://github.com/luoxuhai/react-native-tinyui.git", :tag => "#{s.version}" }
|
|
75
|
+
|
|
76
|
+
s.source_files = source_files
|
|
77
|
+
s.private_header_files = ["ios/Core/**/*.h", "ios/Menu/**/*.h", "ios/Popover/**/*.h"]
|
|
78
|
+
s.swift_version = "5.0"
|
|
79
|
+
s.frameworks = frameworks unless frameworks.empty?
|
|
80
|
+
s.pod_target_xcconfig = {
|
|
81
|
+
"GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) #{definitions.join(' ')}",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
install_modules_dependencies(s)
|
|
85
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#import "Tinyui.h"
|
|
2
|
+
|
|
3
|
+
@implementation Tinyui
|
|
4
|
+
|
|
5
|
+
- (NSArray<NSString *> *)getEnabledComponents
|
|
6
|
+
{
|
|
7
|
+
NSMutableArray<NSString *> *components = [NSMutableArray array];
|
|
8
|
+
|
|
9
|
+
#if TINYUI_FEATURE_MENU
|
|
10
|
+
[components addObject:@"Menu"];
|
|
11
|
+
#endif
|
|
12
|
+
#if TINYUI_FEATURE_POPOVER
|
|
13
|
+
[components addObject:@"Popover"];
|
|
14
|
+
#endif
|
|
15
|
+
|
|
16
|
+
return components;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
20
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
21
|
+
{
|
|
22
|
+
return std::make_shared<facebook::react::NativeTinyuiSpecJSI>(params);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
+ (NSString *)moduleName
|
|
26
|
+
{
|
|
27
|
+
return @"Tinyui";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#import <React/RCTComponentViewProtocol.h>
|
|
2
|
+
|
|
3
|
+
#if TINYUI_FEATURE_MENU
|
|
4
|
+
#import "TinyuiMenuView.h"
|
|
5
|
+
#endif
|
|
6
|
+
|
|
7
|
+
#if TINYUI_FEATURE_POPOVER
|
|
8
|
+
#import "TinyuiPopoverView.h"
|
|
9
|
+
#endif
|
|
10
|
+
|
|
11
|
+
Class<RCTComponentViewProtocol> TinyuiMenuViewCls(void)
|
|
12
|
+
{
|
|
13
|
+
#if TINYUI_FEATURE_MENU
|
|
14
|
+
return TinyuiMenuView.class;
|
|
15
|
+
#else
|
|
16
|
+
return nil;
|
|
17
|
+
#endif
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Class<RCTComponentViewProtocol> TinyuiPopoverViewCls(void)
|
|
21
|
+
{
|
|
22
|
+
#if TINYUI_FEATURE_POPOVER
|
|
23
|
+
return TinyuiPopoverView.class;
|
|
24
|
+
#else
|
|
25
|
+
return nil;
|
|
26
|
+
#endif
|
|
27
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Identifiable wrapper used to render React Native views (UIView) inside
|
|
6
|
+
* SwiftUI. Modeled after react-native-pager-view's bridge.
|
|
7
|
+
*/
|
|
8
|
+
struct IdentifiablePlatformView: Identifiable, Equatable {
|
|
9
|
+
let id = UUID()
|
|
10
|
+
let view: UIView
|
|
11
|
+
|
|
12
|
+
init(_ view: UIView) {
|
|
13
|
+
self.view = view
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Renders a UIView inside SwiftUI. Uses UIViewControllerRepresentable so the
|
|
19
|
+
* hosted React view keeps a valid view-controller hierarchy.
|
|
20
|
+
*/
|
|
21
|
+
struct RepresentableView: UIViewControllerRepresentable {
|
|
22
|
+
var view: UIView
|
|
23
|
+
|
|
24
|
+
func makeUIViewController(context: Context) -> PlatformChildViewController {
|
|
25
|
+
let viewController = PlatformChildViewController()
|
|
26
|
+
viewController.wrappedView = view
|
|
27
|
+
return viewController
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func updateUIViewController(
|
|
31
|
+
_ uiViewController: PlatformChildViewController,
|
|
32
|
+
context: Context
|
|
33
|
+
) {
|
|
34
|
+
if uiViewController.wrappedView !== view {
|
|
35
|
+
uiViewController.wrappedView = view
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class PlatformChildViewController: UIViewController {
|
|
41
|
+
var wrappedView: UIView? {
|
|
42
|
+
didSet {
|
|
43
|
+
guard oldValue !== wrappedView else { return }
|
|
44
|
+
oldValue?.removeFromSuperview()
|
|
45
|
+
if isViewLoaded {
|
|
46
|
+
mountWrappedView()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override func viewDidLoad() {
|
|
52
|
+
super.viewDidLoad()
|
|
53
|
+
view.backgroundColor = .clear
|
|
54
|
+
mountWrappedView()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private func mountWrappedView() {
|
|
58
|
+
guard let wrappedView else { return }
|
|
59
|
+
view.addSubview(wrappedView)
|
|
60
|
+
wrappedView.translatesAutoresizingMaskIntoConstraints = false
|
|
61
|
+
wrappedView.pinEdges(to: view)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
extension UIView {
|
|
66
|
+
func pinEdges(to other: UIView) {
|
|
67
|
+
NSLayoutConstraint.activate([
|
|
68
|
+
leadingAnchor.constraint(equalTo: other.leadingAnchor),
|
|
69
|
+
trailingAnchor.constraint(equalTo: other.trailingAnchor),
|
|
70
|
+
topAnchor.constraint(equalTo: other.topAnchor),
|
|
71
|
+
bottomAnchor.constraint(equalTo: other.bottomAnchor),
|
|
72
|
+
])
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* UIKit bridge between the Fabric `TinyuiMenuView` component and the SwiftUI
|
|
6
|
+
* `TinyuiMenuSwiftUIView`. Hosts the SwiftUI view through a
|
|
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.
|
|
11
|
+
*/
|
|
12
|
+
@objc public class TinyuiMenuProvider: UIView {
|
|
13
|
+
private weak var delegate: TinyuiMenuViewDelegate?
|
|
14
|
+
private var hostingController: UIHostingController<TinyuiMenuSwiftUIView>?
|
|
15
|
+
private var props = TinyuiMenuProps()
|
|
16
|
+
|
|
17
|
+
/// Presenting view controller, resolved by the component view from its
|
|
18
|
+
/// React view controller hierarchy.
|
|
19
|
+
@objc public weak var parentViewController: UIViewController? {
|
|
20
|
+
didSet {
|
|
21
|
+
if window != nil {
|
|
22
|
+
setupView()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// The React Native trigger view rendered as the menu's label.
|
|
28
|
+
@objc public var triggerView: UIView? {
|
|
29
|
+
didSet {
|
|
30
|
+
props.triggerView = triggerView
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// JSON menu configuration passed to the SwiftUI view.
|
|
35
|
+
@objc public var menuConfig: String = "{}" {
|
|
36
|
+
didSet {
|
|
37
|
+
props.menuConfig = menuConfig
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@objc public var disabled: Bool = false {
|
|
42
|
+
didSet {
|
|
43
|
+
props.disabled = disabled
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@objc public var hasPrimaryAction: Bool = false {
|
|
48
|
+
didSet {
|
|
49
|
+
props.hasPrimaryAction = hasPrimaryAction
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@objc public convenience init(delegate: TinyuiMenuViewDelegate) {
|
|
54
|
+
self.init()
|
|
55
|
+
self.delegate = delegate
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override public func didMoveToWindow() {
|
|
59
|
+
super.didMoveToWindow()
|
|
60
|
+
if window != nil {
|
|
61
|
+
setupView()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override public func layoutSubviews() {
|
|
66
|
+
super.layoutSubviews()
|
|
67
|
+
if window != nil {
|
|
68
|
+
setupView()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private func setupView() {
|
|
73
|
+
if hostingController != nil {
|
|
74
|
+
syncParentViewController()
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
guard let parentViewController else {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let hostingController = UIHostingController(
|
|
83
|
+
rootView: TinyuiMenuSwiftUIView(props: props, delegate: delegate)
|
|
84
|
+
)
|
|
85
|
+
self.hostingController = hostingController
|
|
86
|
+
|
|
87
|
+
parentViewController.addChild(hostingController)
|
|
88
|
+
hostingController.view.backgroundColor = .clear
|
|
89
|
+
addSubview(hostingController.view)
|
|
90
|
+
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
91
|
+
hostingController.view.pinEdges(to: self)
|
|
92
|
+
hostingController.didMove(toParent: parentViewController)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Keeps the hosting controller attached to the current view controller.
|
|
96
|
+
private func syncParentViewController(to parentViewController: UIViewController? = nil) {
|
|
97
|
+
guard
|
|
98
|
+
let hostingController,
|
|
99
|
+
let parentViewController = parentViewController ?? self.parentViewController,
|
|
100
|
+
hostingController.parent !== parentViewController
|
|
101
|
+
else {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
hostingController.willMove(toParent: nil)
|
|
106
|
+
hostingController.removeFromParent()
|
|
107
|
+
parentViewController.addChild(hostingController)
|
|
108
|
+
hostingController.didMove(toParent: parentViewController)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#if TINYUI_FEATURE_MENU
|
|
2
|
+
|
|
3
|
+
#import "TinyuiMenuView.h"
|
|
4
|
+
|
|
5
|
+
#import <React/RCTConversions.h>
|
|
6
|
+
#import <React/UIView+React.h>
|
|
7
|
+
|
|
8
|
+
#import <react/renderer/components/TinyuiSpec/ComponentDescriptors.h>
|
|
9
|
+
#import <react/renderer/components/TinyuiSpec/EventEmitters.h>
|
|
10
|
+
#import <react/renderer/components/TinyuiSpec/Props.h>
|
|
11
|
+
#import <react/renderer/components/TinyuiSpec/RCTComponentViewHelpers.h>
|
|
12
|
+
|
|
13
|
+
#import "RCTFabricComponentsPlugins.h"
|
|
14
|
+
|
|
15
|
+
#if __has_include("Tinyui-Swift.h")
|
|
16
|
+
#import "Tinyui-Swift.h"
|
|
17
|
+
#endif
|
|
18
|
+
|
|
19
|
+
using namespace facebook::react;
|
|
20
|
+
|
|
21
|
+
@interface TinyuiMenuView () <RCTTinyuiMenuViewViewProtocol, TinyuiMenuViewDelegate>
|
|
22
|
+
@end
|
|
23
|
+
|
|
24
|
+
@implementation TinyuiMenuView {
|
|
25
|
+
TinyuiMenuProvider *_menuProvider;
|
|
26
|
+
UIView *_triggerView;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
30
|
+
{
|
|
31
|
+
return concreteComponentDescriptorProvider<TinyuiMenuViewComponentDescriptor>();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
35
|
+
{
|
|
36
|
+
if (self = [super initWithFrame:frame]) {
|
|
37
|
+
static const auto defaultProps = std::make_shared<const TinyuiMenuViewProps>();
|
|
38
|
+
_props = defaultProps;
|
|
39
|
+
|
|
40
|
+
_menuProvider = [[TinyuiMenuProvider alloc] initWithDelegate:self];
|
|
41
|
+
self.contentView = _menuProvider;
|
|
42
|
+
}
|
|
43
|
+
return self;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
- (void)didMoveToWindow
|
|
47
|
+
{
|
|
48
|
+
[super didMoveToWindow];
|
|
49
|
+
if (self.window != nil) {
|
|
50
|
+
_menuProvider.parentViewController = [self reactViewController];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#pragma mark - React lifecycle
|
|
55
|
+
|
|
56
|
+
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
|
|
57
|
+
index:(NSInteger)index
|
|
58
|
+
{
|
|
59
|
+
// TinyuiMenuView renders its trigger as a child; we hand it to the
|
|
60
|
+
// provider, which uses it as the SwiftUI `Menu` label.
|
|
61
|
+
_triggerView = childComponentView;
|
|
62
|
+
_menuProvider.triggerView = childComponentView;
|
|
63
|
+
[_menuProvider addSubview:childComponentView];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
|
|
67
|
+
index:(NSInteger)index
|
|
68
|
+
{
|
|
69
|
+
[childComponentView removeFromSuperview];
|
|
70
|
+
if (_triggerView == childComponentView) {
|
|
71
|
+
_triggerView = nil;
|
|
72
|
+
_menuProvider.triggerView = nil;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#pragma mark - Props
|
|
77
|
+
|
|
78
|
+
- (void)updateProps:(Props::Shared const &)props
|
|
79
|
+
oldProps:(Props::Shared const &)oldProps
|
|
80
|
+
{
|
|
81
|
+
const auto &oldMenuProps = *std::static_pointer_cast<TinyuiMenuViewProps const>(_props);
|
|
82
|
+
const auto &newMenuProps = *std::static_pointer_cast<TinyuiMenuViewProps const>(props);
|
|
83
|
+
|
|
84
|
+
if (oldMenuProps.menuConfig != newMenuProps.menuConfig) {
|
|
85
|
+
_menuProvider.menuConfig = RCTNSStringFromString(newMenuProps.menuConfig);
|
|
86
|
+
}
|
|
87
|
+
if (oldMenuProps.hasPrimaryAction != newMenuProps.hasPrimaryAction) {
|
|
88
|
+
_menuProvider.hasPrimaryAction = newMenuProps.hasPrimaryAction;
|
|
89
|
+
}
|
|
90
|
+
if (oldMenuProps.disabled != newMenuProps.disabled) {
|
|
91
|
+
_menuProvider.disabled = newMenuProps.disabled;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
[super updateProps:props oldProps:oldProps];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#pragma mark - TinyuiMenuViewDelegate
|
|
98
|
+
|
|
99
|
+
- (void)onItemPressWithId:(NSString *)identifier
|
|
100
|
+
{
|
|
101
|
+
if (_eventEmitter == nullptr) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
std::static_pointer_cast<const TinyuiMenuViewEventEmitter>(_eventEmitter)
|
|
105
|
+
->onItemPress(TinyuiMenuViewEventEmitter::OnItemPress{
|
|
106
|
+
.id = std::string(identifier.UTF8String ?: "")});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
- (void)onPrimaryAction
|
|
110
|
+
{
|
|
111
|
+
if (_eventEmitter == nullptr) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
std::static_pointer_cast<const TinyuiMenuViewEventEmitter>(_eventEmitter)
|
|
115
|
+
->onPrimaryAction(TinyuiMenuViewEventEmitter::OnPrimaryAction{});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@end
|
|
119
|
+
|
|
120
|
+
#endif // TINYUI_FEATURE_MENU
|