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.
Files changed (74) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +172 -0
  3. package/Tinyui.podspec +85 -0
  4. package/ios/Core/Tinyui.h +8 -0
  5. package/ios/Core/Tinyui.mm +30 -0
  6. package/ios/Core/TinyuiComponentClasses.mm +27 -0
  7. package/ios/Core/TinyuiHostingHelpers.swift +74 -0
  8. package/ios/Menu/TinyuiMenuProvider.swift +110 -0
  9. package/ios/Menu/TinyuiMenuView.h +14 -0
  10. package/ios/Menu/TinyuiMenuView.mm +120 -0
  11. package/ios/Menu/TinyuiMenuViewSwift.swift +368 -0
  12. package/ios/Popover/TinyuiPopoverProvider.swift +111 -0
  13. package/ios/Popover/TinyuiPopoverView.h +14 -0
  14. package/ios/Popover/TinyuiPopoverView.mm +124 -0
  15. package/ios/Popover/TinyuiPopoverViewSwift.swift +88 -0
  16. package/lib/module/Menu.js +43 -0
  17. package/lib/module/Menu.js.map +1 -0
  18. package/lib/module/Menu.types.js +4 -0
  19. package/lib/module/Menu.types.js.map +1 -0
  20. package/lib/module/MenuNativeComponent.ts +14 -0
  21. package/lib/module/MenuOptions.js +73 -0
  22. package/lib/module/MenuOptions.js.map +1 -0
  23. package/lib/module/NativeTinyui.js +5 -0
  24. package/lib/module/NativeTinyui.js.map +1 -0
  25. package/lib/module/Popover.js +71 -0
  26. package/lib/module/Popover.js.map +1 -0
  27. package/lib/module/Popover.types.js +4 -0
  28. package/lib/module/Popover.types.js.map +1 -0
  29. package/lib/module/PopoverNativeComponent.ts +13 -0
  30. package/lib/module/PopoverPrimitives.js +33 -0
  31. package/lib/module/PopoverPrimitives.js.map +1 -0
  32. package/lib/module/getNativeTinyui.js +30 -0
  33. package/lib/module/getNativeTinyui.js.map +1 -0
  34. package/lib/module/index.js +6 -0
  35. package/lib/module/index.js.map +1 -0
  36. package/lib/module/package.json +1 -0
  37. package/lib/module/react-native.d.js +2 -0
  38. package/lib/module/react-native.d.js.map +1 -0
  39. package/lib/typescript/package.json +1 -0
  40. package/lib/typescript/src/Menu.d.ts +4 -0
  41. package/lib/typescript/src/Menu.d.ts.map +1 -0
  42. package/lib/typescript/src/Menu.types.d.ts +63 -0
  43. package/lib/typescript/src/Menu.types.d.ts.map +1 -0
  44. package/lib/typescript/src/MenuNativeComponent.d.ts +14 -0
  45. package/lib/typescript/src/MenuNativeComponent.d.ts.map +1 -0
  46. package/lib/typescript/src/MenuOptions.d.ts +7 -0
  47. package/lib/typescript/src/MenuOptions.d.ts.map +1 -0
  48. package/lib/typescript/src/NativeTinyui.d.ts +11 -0
  49. package/lib/typescript/src/NativeTinyui.d.ts.map +1 -0
  50. package/lib/typescript/src/Popover.d.ts +7 -0
  51. package/lib/typescript/src/Popover.d.ts.map +1 -0
  52. package/lib/typescript/src/Popover.types.d.ts +35 -0
  53. package/lib/typescript/src/Popover.types.d.ts.map +1 -0
  54. package/lib/typescript/src/PopoverNativeComponent.d.ts +13 -0
  55. package/lib/typescript/src/PopoverNativeComponent.d.ts.map +1 -0
  56. package/lib/typescript/src/PopoverPrimitives.d.ts +21 -0
  57. package/lib/typescript/src/PopoverPrimitives.d.ts.map +1 -0
  58. package/lib/typescript/src/getNativeTinyui.d.ts +13 -0
  59. package/lib/typescript/src/getNativeTinyui.d.ts.map +1 -0
  60. package/lib/typescript/src/index.d.ts +7 -0
  61. package/lib/typescript/src/index.d.ts.map +1 -0
  62. package/package.json +175 -0
  63. package/src/Menu.tsx +52 -0
  64. package/src/Menu.types.ts +77 -0
  65. package/src/MenuNativeComponent.ts +14 -0
  66. package/src/MenuOptions.ts +146 -0
  67. package/src/NativeTinyui.ts +12 -0
  68. package/src/Popover.tsx +92 -0
  69. package/src/Popover.types.ts +49 -0
  70. package/src/PopoverNativeComponent.ts +13 -0
  71. package/src/PopoverPrimitives.tsx +36 -0
  72. package/src/getNativeTinyui.ts +38 -0
  73. package/src/index.tsx +23 -0
  74. 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,8 @@
1
+ #import <TinyuiSpec/TinyuiSpec.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ @interface Tinyui : NativeTinyuiSpecBase <NativeTinyuiSpec>
6
+ @end
7
+
8
+ NS_ASSUME_NONNULL_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,14 @@
1
+ #if TINYUI_FEATURE_MENU
2
+
3
+ #import <React/RCTViewComponentView.h>
4
+ #import <UIKit/UIKit.h>
5
+
6
+ NS_ASSUME_NONNULL_BEGIN
7
+
8
+ @interface TinyuiMenuView : RCTViewComponentView
9
+ @end
10
+
11
+ NS_ASSUME_NONNULL_END
12
+
13
+ #endif // TINYUI_FEATURE_MENU
14
+
@@ -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