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
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Observable props shared between the Objective-C++ component view and the
|
|
6
|
+
* SwiftUI `Menu` implementation. Mirror of `TinyuiMenuProps` in codegen.
|
|
7
|
+
*/
|
|
8
|
+
class TinyuiMenuProps: ObservableObject {
|
|
9
|
+
@Published var menuConfig: String = "{}"
|
|
10
|
+
@Published var disabled: Bool = false
|
|
11
|
+
@Published var hasPrimaryAction: Bool = false
|
|
12
|
+
@Published var triggerView: UIView?
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* SwiftUI `Menu` backed by UIKit's UIMenu, matching expo-ui's MenuView.
|
|
17
|
+
*
|
|
18
|
+
* The React Native trigger (a UIView) is embedded as the menu label through
|
|
19
|
+
* `RepresentableView`, so the tap target remains the native RN subtree while
|
|
20
|
+
* the menu content is built natively from the JSON config.
|
|
21
|
+
*
|
|
22
|
+
* Named distinctly from the Fabric component view `TinyuiMenuView`.
|
|
23
|
+
*/
|
|
24
|
+
struct TinyuiMenuSwiftUIView: View {
|
|
25
|
+
@ObservedObject var props: TinyuiMenuProps
|
|
26
|
+
weak var delegate: TinyuiMenuViewDelegate?
|
|
27
|
+
|
|
28
|
+
var body: some View {
|
|
29
|
+
Group {
|
|
30
|
+
if let triggerView = props.triggerView {
|
|
31
|
+
if props.hasPrimaryAction {
|
|
32
|
+
Menu {
|
|
33
|
+
menuContent
|
|
34
|
+
} label: {
|
|
35
|
+
RepresentableView(view: triggerView)
|
|
36
|
+
} primaryAction: {
|
|
37
|
+
delegate?.onPrimaryAction()
|
|
38
|
+
}
|
|
39
|
+
.disabled(props.disabled)
|
|
40
|
+
} else {
|
|
41
|
+
Menu {
|
|
42
|
+
menuContent
|
|
43
|
+
} label: {
|
|
44
|
+
RepresentableView(view: triggerView)
|
|
45
|
+
}
|
|
46
|
+
.disabled(props.disabled)
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
// No trigger mounted yet; render an empty view until the RN child
|
|
50
|
+
// is attached to the provider.
|
|
51
|
+
Color.clear
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@ViewBuilder
|
|
57
|
+
private var menuContent: some View {
|
|
58
|
+
ForEach(buildElements(from: props.menuConfig)) { element in
|
|
59
|
+
element.buildView(delegate: delegate)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Parses the JSON menu config into a list of SwiftUI element builders.
|
|
64
|
+
private func buildElements(from json: String) -> [TinyuiMenuElement] {
|
|
65
|
+
guard
|
|
66
|
+
let data = json.data(using: .utf8),
|
|
67
|
+
let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
68
|
+
let items = root["items"] as? [[String: Any]]
|
|
69
|
+
else {
|
|
70
|
+
return []
|
|
71
|
+
}
|
|
72
|
+
return items.map { TinyuiMenuElement(dictionary: $0) }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Lightweight representation of a single UIMenu element parsed from the JSON
|
|
78
|
+
* config. Used to build SwiftUI menu content without resolving all elements
|
|
79
|
+
* eagerly. Returns `AnyView` to avoid recursive opaque return type errors.
|
|
80
|
+
*/
|
|
81
|
+
struct TinyuiMenuElement: Identifiable {
|
|
82
|
+
let id = UUID()
|
|
83
|
+
private let dictionary: [String: Any]
|
|
84
|
+
|
|
85
|
+
init(dictionary: [String: Any]) {
|
|
86
|
+
self.dictionary = dictionary
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func buildView(
|
|
90
|
+
delegate: TinyuiMenuViewDelegate?,
|
|
91
|
+
inheritedDestructive: Bool = false
|
|
92
|
+
) -> AnyView {
|
|
93
|
+
if dictionary["hidden"] as? Bool ?? false {
|
|
94
|
+
return AnyView(EmptyView())
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
switch dictionary["type"] as? String {
|
|
98
|
+
case "action":
|
|
99
|
+
let destructive = inheritedDestructive || (dictionary["destructive"] as? Bool ?? false)
|
|
100
|
+
let keepOpen = dictionary["keepOpen"] as? Bool ?? false
|
|
101
|
+
return AnyView(
|
|
102
|
+
Button(role: destructive ? .destructive : nil) {
|
|
103
|
+
if let id = dictionary["id"] as? String {
|
|
104
|
+
delegate?.onItemPress(id: id)
|
|
105
|
+
}
|
|
106
|
+
} label: {
|
|
107
|
+
TinyuiMenuLabel(
|
|
108
|
+
dictionary: dictionary,
|
|
109
|
+
destructive: destructive,
|
|
110
|
+
showsState: true
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
.disabled(dictionary["disabled"] as? Bool ?? false)
|
|
114
|
+
.menuActionDismissBehavior(keepOpen ? .disabled : .automatic)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
case "submenu":
|
|
118
|
+
let children = (dictionary["children"] as? [[String: Any]] ?? [])
|
|
119
|
+
.map { TinyuiMenuElement(dictionary: $0) }
|
|
120
|
+
let destructive = inheritedDestructive || (dictionary["destructive"] as? Bool ?? false)
|
|
121
|
+
let disabled = dictionary["disabled"] as? Bool ?? false
|
|
122
|
+
|
|
123
|
+
if dictionary["displayInline"] as? Bool ?? false {
|
|
124
|
+
return AnyView(
|
|
125
|
+
Section {
|
|
126
|
+
ForEach(children) { child in
|
|
127
|
+
child.buildView(
|
|
128
|
+
delegate: delegate,
|
|
129
|
+
inheritedDestructive: destructive
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
} header: {
|
|
133
|
+
TinyuiMenuLabel(
|
|
134
|
+
dictionary: dictionary,
|
|
135
|
+
destructive: destructive,
|
|
136
|
+
showsState: false
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
.disabled(disabled)
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return AnyView(
|
|
144
|
+
Menu {
|
|
145
|
+
ForEach(children) { child in
|
|
146
|
+
child.buildView(
|
|
147
|
+
delegate: delegate,
|
|
148
|
+
inheritedDestructive: destructive
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
} label: {
|
|
152
|
+
TinyuiMenuLabel(
|
|
153
|
+
dictionary: dictionary,
|
|
154
|
+
destructive: destructive,
|
|
155
|
+
showsState: false
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
.disabled(disabled)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
case "section":
|
|
162
|
+
let children = (dictionary["children"] as? [[String: Any]] ?? [])
|
|
163
|
+
.map { TinyuiMenuElement(dictionary: $0) }
|
|
164
|
+
return AnyView(
|
|
165
|
+
Section {
|
|
166
|
+
ForEach(children) { child in
|
|
167
|
+
child.buildView(
|
|
168
|
+
delegate: delegate,
|
|
169
|
+
inheritedDestructive: inheritedDestructive
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
} header: {
|
|
173
|
+
if let title = dictionary["title"] as? String, !title.isEmpty {
|
|
174
|
+
Text(title)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
case "separator":
|
|
180
|
+
return AnyView(Divider())
|
|
181
|
+
|
|
182
|
+
default:
|
|
183
|
+
return AnyView(EmptyView())
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Label content shared by actions and submenus. */
|
|
189
|
+
private struct TinyuiMenuLabel: View {
|
|
190
|
+
let dictionary: [String: Any]
|
|
191
|
+
let destructive: Bool
|
|
192
|
+
let showsState: Bool
|
|
193
|
+
|
|
194
|
+
var body: some View {
|
|
195
|
+
HStack {
|
|
196
|
+
menuIcon
|
|
197
|
+
|
|
198
|
+
VStack(alignment: .leading, spacing: 1) {
|
|
199
|
+
title
|
|
200
|
+
|
|
201
|
+
if let subtitle = dictionary["subtitle"] as? String,
|
|
202
|
+
!subtitle.isEmpty {
|
|
203
|
+
Text(subtitle)
|
|
204
|
+
.font(.caption)
|
|
205
|
+
.foregroundStyle(.secondary)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if showsState {
|
|
210
|
+
switch dictionary["state"] as? String {
|
|
211
|
+
case "on":
|
|
212
|
+
Image(systemName: "checkmark")
|
|
213
|
+
case "mixed":
|
|
214
|
+
Image(systemName: "minus")
|
|
215
|
+
default:
|
|
216
|
+
EmptyView()
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@ViewBuilder
|
|
223
|
+
private var title: some View {
|
|
224
|
+
let text = Text(dictionary["title"] as? String ?? "")
|
|
225
|
+
if let color = TinyuiMenuColor.uiColor(from: dictionary["titleColor"]) {
|
|
226
|
+
text.foregroundStyle(Color(uiColor: color))
|
|
227
|
+
} else if destructive {
|
|
228
|
+
text.foregroundStyle(Color.red)
|
|
229
|
+
} else {
|
|
230
|
+
text
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@ViewBuilder
|
|
235
|
+
private var menuIcon: some View {
|
|
236
|
+
let iconColor = TinyuiMenuColor.uiColor(from: dictionary["iconColor"])
|
|
237
|
+
|
|
238
|
+
if let icon = dictionary["icon"] as? String, !icon.isEmpty {
|
|
239
|
+
if let image = UIImage(named: icon) {
|
|
240
|
+
if let iconColor {
|
|
241
|
+
Image(
|
|
242
|
+
uiImage: image.withTintColor(
|
|
243
|
+
iconColor,
|
|
244
|
+
renderingMode: .alwaysOriginal
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
} else {
|
|
248
|
+
Image(uiImage: image.withRenderingMode(.alwaysTemplate))
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} else if let systemImage = dictionary["systemImage"] as? String,
|
|
252
|
+
!systemImage.isEmpty {
|
|
253
|
+
if let iconColor {
|
|
254
|
+
Image(systemName: systemImage)
|
|
255
|
+
.foregroundStyle(Color(uiColor: iconColor))
|
|
256
|
+
} else {
|
|
257
|
+
Image(systemName: systemImage)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Converts React Native's processed ColorValue JSON into a dynamic UIColor. */
|
|
264
|
+
private enum TinyuiMenuColor {
|
|
265
|
+
static func uiColor(from value: Any?) -> UIColor? {
|
|
266
|
+
if let number = value as? NSNumber {
|
|
267
|
+
let argb = number.uint32Value
|
|
268
|
+
return UIColor(
|
|
269
|
+
red: CGFloat((argb >> 16) & 0xff) / 255,
|
|
270
|
+
green: CGFloat((argb >> 8) & 0xff) / 255,
|
|
271
|
+
blue: CGFloat(argb & 0xff) / 255,
|
|
272
|
+
alpha: CGFloat((argb >> 24) & 0xff) / 255
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
guard let object = value as? [String: Any] else {
|
|
277
|
+
return nil
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if let semanticNames = object["semantic"] as? [String] {
|
|
281
|
+
for name in semanticNames {
|
|
282
|
+
if let color = semanticColor(named: name) {
|
|
283
|
+
return color
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if let dynamic = object["dynamic"] as? [String: Any] {
|
|
289
|
+
return UIColor { traits in
|
|
290
|
+
let highContrast = traits.accessibilityContrast == .high
|
|
291
|
+
let dark = traits.userInterfaceStyle == .dark
|
|
292
|
+
let preferredKey: String
|
|
293
|
+
|
|
294
|
+
switch (dark, highContrast) {
|
|
295
|
+
case (true, true):
|
|
296
|
+
preferredKey = "highContrastDark"
|
|
297
|
+
case (false, true):
|
|
298
|
+
preferredKey = "highContrastLight"
|
|
299
|
+
case (true, false):
|
|
300
|
+
preferredKey = "dark"
|
|
301
|
+
case (false, false):
|
|
302
|
+
preferredKey = "light"
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return uiColor(from: dynamic[preferredKey])
|
|
306
|
+
?? uiColor(from: dynamic[dark ? "dark" : "light"])
|
|
307
|
+
?? UIColor.clear
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return nil
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private static func semanticColor(named name: String) -> UIColor? {
|
|
315
|
+
if let color = UIColor(named: name) {
|
|
316
|
+
return color
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let normalizedName = name.hasSuffix("Color")
|
|
320
|
+
? String(name.dropLast("Color".count))
|
|
321
|
+
: name
|
|
322
|
+
|
|
323
|
+
switch normalizedName {
|
|
324
|
+
case "label": return .label
|
|
325
|
+
case "secondaryLabel": return .secondaryLabel
|
|
326
|
+
case "tertiaryLabel": return .tertiaryLabel
|
|
327
|
+
case "quaternaryLabel": return .quaternaryLabel
|
|
328
|
+
case "placeholderText": return .placeholderText
|
|
329
|
+
case "separator": return .separator
|
|
330
|
+
case "opaqueSeparator": return .opaqueSeparator
|
|
331
|
+
case "link": return .link
|
|
332
|
+
case "systemBackground": return .systemBackground
|
|
333
|
+
case "secondarySystemBackground": return .secondarySystemBackground
|
|
334
|
+
case "tertiarySystemBackground": return .tertiarySystemBackground
|
|
335
|
+
case "systemGroupedBackground": return .systemGroupedBackground
|
|
336
|
+
case "secondarySystemGroupedBackground": return .secondarySystemGroupedBackground
|
|
337
|
+
case "tertiarySystemGroupedBackground": return .tertiarySystemGroupedBackground
|
|
338
|
+
case "systemFill": return .systemFill
|
|
339
|
+
case "secondarySystemFill": return .secondarySystemFill
|
|
340
|
+
case "tertiarySystemFill": return .tertiarySystemFill
|
|
341
|
+
case "quaternarySystemFill": return .quaternarySystemFill
|
|
342
|
+
case "systemRed": return .systemRed
|
|
343
|
+
case "systemOrange": return .systemOrange
|
|
344
|
+
case "systemYellow": return .systemYellow
|
|
345
|
+
case "systemGreen": return .systemGreen
|
|
346
|
+
case "systemMint": return .systemMint
|
|
347
|
+
case "systemTeal": return .systemTeal
|
|
348
|
+
case "systemCyan": return .systemCyan
|
|
349
|
+
case "systemBlue": return .systemBlue
|
|
350
|
+
case "systemIndigo": return .systemIndigo
|
|
351
|
+
case "systemPurple": return .systemPurple
|
|
352
|
+
case "systemPink": return .systemPink
|
|
353
|
+
case "systemBrown": return .systemBrown
|
|
354
|
+
case "systemGray": return .systemGray
|
|
355
|
+
case "systemGray2": return .systemGray2
|
|
356
|
+
case "systemGray3": return .systemGray3
|
|
357
|
+
case "systemGray4": return .systemGray4
|
|
358
|
+
case "systemGray5": return .systemGray5
|
|
359
|
+
case "systemGray6": return .systemGray6
|
|
360
|
+
default: return nil
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@objc public protocol TinyuiMenuViewDelegate {
|
|
366
|
+
func onItemPress(id: String)
|
|
367
|
+
func onPrimaryAction()
|
|
368
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* UIKit bridge between the Fabric `TinyuiPopoverView` component and the
|
|
6
|
+
* SwiftUI `TinyuiPopoverSwiftUIView`. Hosts the SwiftUI view through a
|
|
7
|
+
* `UIHostingController`, mirroring react-native-pager-view's bridge.
|
|
8
|
+
*/
|
|
9
|
+
@objc public class TinyuiPopoverProvider: UIView {
|
|
10
|
+
private weak var delegate: TinyuiPopoverViewDelegate?
|
|
11
|
+
private var hostingController: UIHostingController<TinyuiPopoverSwiftUIView>?
|
|
12
|
+
private var props = TinyuiPopoverProps()
|
|
13
|
+
|
|
14
|
+
/// Presenting view controller, resolved by the component view from its
|
|
15
|
+
/// React view controller hierarchy.
|
|
16
|
+
@objc public weak var parentViewController: UIViewController? {
|
|
17
|
+
didSet {
|
|
18
|
+
if window != nil {
|
|
19
|
+
setupView()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@objc public var triggerView: UIView? {
|
|
25
|
+
didSet {
|
|
26
|
+
props.triggerView = triggerView
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@objc public var contentView: UIView? {
|
|
31
|
+
didSet {
|
|
32
|
+
props.contentView = contentView
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@objc public var isPresented: Bool = false {
|
|
37
|
+
didSet {
|
|
38
|
+
props.isPresented = isPresented
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@objc public var attachmentAnchor: String = "center" {
|
|
43
|
+
didSet {
|
|
44
|
+
props.attachmentAnchor = attachmentAnchor
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@objc public var arrowEdge: String = "none" {
|
|
49
|
+
didSet {
|
|
50
|
+
props.arrowEdge = arrowEdge
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@objc public convenience init(delegate: TinyuiPopoverViewDelegate) {
|
|
55
|
+
self.init()
|
|
56
|
+
self.delegate = delegate
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override public func didMoveToWindow() {
|
|
60
|
+
super.didMoveToWindow()
|
|
61
|
+
if window != nil {
|
|
62
|
+
setupView()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override public func layoutSubviews() {
|
|
67
|
+
super.layoutSubviews()
|
|
68
|
+
if window != nil {
|
|
69
|
+
setupView()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private func setupView() {
|
|
74
|
+
if hostingController != nil {
|
|
75
|
+
syncParentViewController()
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
guard let parentViewController else {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let hostingController = UIHostingController(
|
|
84
|
+
rootView: TinyuiPopoverSwiftUIView(props: props, delegate: delegate)
|
|
85
|
+
)
|
|
86
|
+
self.hostingController = hostingController
|
|
87
|
+
|
|
88
|
+
parentViewController.addChild(hostingController)
|
|
89
|
+
hostingController.view.backgroundColor = .clear
|
|
90
|
+
addSubview(hostingController.view)
|
|
91
|
+
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
92
|
+
hostingController.view.pinEdges(to: self)
|
|
93
|
+
hostingController.didMove(toParent: parentViewController)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Keeps the hosting controller attached to the current view controller.
|
|
97
|
+
private func syncParentViewController(to parentViewController: UIViewController? = nil) {
|
|
98
|
+
guard
|
|
99
|
+
let hostingController,
|
|
100
|
+
let parentViewController = parentViewController ?? self.parentViewController,
|
|
101
|
+
hostingController.parent !== parentViewController
|
|
102
|
+
else {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
hostingController.willMove(toParent: nil)
|
|
107
|
+
hostingController.removeFromParent()
|
|
108
|
+
parentViewController.addChild(hostingController)
|
|
109
|
+
hostingController.didMove(toParent: parentViewController)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#if TINYUI_FEATURE_POPOVER
|
|
2
|
+
|
|
3
|
+
#import <React/RCTViewComponentView.h>
|
|
4
|
+
#import <UIKit/UIKit.h>
|
|
5
|
+
|
|
6
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
7
|
+
|
|
8
|
+
@interface TinyuiPopoverView : RCTViewComponentView
|
|
9
|
+
@end
|
|
10
|
+
|
|
11
|
+
NS_ASSUME_NONNULL_END
|
|
12
|
+
|
|
13
|
+
#endif // TINYUI_FEATURE_POPOVER
|
|
14
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#if TINYUI_FEATURE_POPOVER
|
|
2
|
+
|
|
3
|
+
#import "TinyuiPopoverView.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 TinyuiPopoverView () <RCTTinyuiPopoverViewViewProtocol, TinyuiPopoverViewDelegate>
|
|
22
|
+
@end
|
|
23
|
+
|
|
24
|
+
@implementation TinyuiPopoverView {
|
|
25
|
+
TinyuiPopoverProvider *_popoverProvider;
|
|
26
|
+
UIView *_triggerView;
|
|
27
|
+
UIView<RCTComponentViewProtocol> *_contentComponentView;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
31
|
+
{
|
|
32
|
+
return concreteComponentDescriptorProvider<TinyuiPopoverViewComponentDescriptor>();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
36
|
+
{
|
|
37
|
+
if (self = [super initWithFrame:frame]) {
|
|
38
|
+
static const auto defaultProps = std::make_shared<const TinyuiPopoverViewProps>();
|
|
39
|
+
_props = defaultProps;
|
|
40
|
+
|
|
41
|
+
_popoverProvider = [[TinyuiPopoverProvider alloc] initWithDelegate:self];
|
|
42
|
+
self.contentView = _popoverProvider;
|
|
43
|
+
}
|
|
44
|
+
return self;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
- (void)didMoveToWindow
|
|
48
|
+
{
|
|
49
|
+
[super didMoveToWindow];
|
|
50
|
+
if (self.window != nil) {
|
|
51
|
+
_popoverProvider.parentViewController = [self reactViewController];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#pragma mark - React lifecycle
|
|
56
|
+
|
|
57
|
+
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
|
|
58
|
+
index:(NSInteger)index
|
|
59
|
+
{
|
|
60
|
+
if (_triggerView == nil) {
|
|
61
|
+
// First child is the trigger, used as the popover's anchor.
|
|
62
|
+
_triggerView = childComponentView;
|
|
63
|
+
_popoverProvider.triggerView = childComponentView;
|
|
64
|
+
[_popoverProvider addSubview:childComponentView];
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Second child is the popover content, presented inside the popover.
|
|
69
|
+
NSAssert(_contentComponentView == nil, @"TinyuiPopoverView accepts one content child.");
|
|
70
|
+
_contentComponentView = childComponentView;
|
|
71
|
+
_popoverProvider.contentView = childComponentView;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
|
|
75
|
+
index:(NSInteger)index
|
|
76
|
+
{
|
|
77
|
+
[childComponentView removeFromSuperview];
|
|
78
|
+
if (_triggerView == childComponentView) {
|
|
79
|
+
_triggerView = nil;
|
|
80
|
+
_popoverProvider.triggerView = nil;
|
|
81
|
+
} else if (_contentComponentView == childComponentView) {
|
|
82
|
+
_contentComponentView = nil;
|
|
83
|
+
_popoverProvider.contentView = nil;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#pragma mark - Props
|
|
88
|
+
|
|
89
|
+
- (void)updateProps:(Props::Shared const &)props
|
|
90
|
+
oldProps:(Props::Shared const &)oldProps
|
|
91
|
+
{
|
|
92
|
+
const auto &newPopoverProps = *std::static_pointer_cast<TinyuiPopoverViewProps const>(props);
|
|
93
|
+
|
|
94
|
+
_popoverProvider.isPresented = newPopoverProps.isPresented;
|
|
95
|
+
_popoverProvider.attachmentAnchor = RCTNSStringFromString(newPopoverProps.attachmentAnchor);
|
|
96
|
+
_popoverProvider.arrowEdge = RCTNSStringFromString(newPopoverProps.arrowEdge);
|
|
97
|
+
|
|
98
|
+
[super updateProps:props oldProps:oldProps];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#pragma mark - TinyuiPopoverViewDelegate
|
|
102
|
+
|
|
103
|
+
- (void)onIsPresentedChangeWithIsPresented:(BOOL)isPresented
|
|
104
|
+
{
|
|
105
|
+
if (_eventEmitter == nullptr) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
std::static_pointer_cast<const TinyuiPopoverViewEventEmitter>(_eventEmitter)
|
|
109
|
+
->onIsPresentedChange(TinyuiPopoverViewEventEmitter::OnIsPresentedChange{
|
|
110
|
+
.isPresented = static_cast<bool>(isPresented)});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
- (void)prepareForRecycle
|
|
114
|
+
{
|
|
115
|
+
_triggerView = nil;
|
|
116
|
+
_contentComponentView = nil;
|
|
117
|
+
_popoverProvider.triggerView = nil;
|
|
118
|
+
_popoverProvider.contentView = nil;
|
|
119
|
+
[super prepareForRecycle];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@end
|
|
123
|
+
|
|
124
|
+
#endif // TINYUI_FEATURE_POPOVER
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Observable props shared between the Objective-C++ component view and the
|
|
6
|
+
* SwiftUI `Popover` implementation. Mirror of `TinyuiPopoverProps` in codegen.
|
|
7
|
+
*/
|
|
8
|
+
class TinyuiPopoverProps: ObservableObject {
|
|
9
|
+
@Published var isPresented: Bool = false
|
|
10
|
+
@Published var attachmentAnchor: String = "center"
|
|
11
|
+
@Published var arrowEdge: String = "none"
|
|
12
|
+
@Published var triggerView: UIView?
|
|
13
|
+
@Published var contentView: UIView?
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* SwiftUI `popover` backed by UIPopoverPresentationController, matching
|
|
18
|
+
* expo-ui's PopoverView. The RN trigger is embedded as the anchor view and the
|
|
19
|
+
* RN content view is presented inside the popover.
|
|
20
|
+
*
|
|
21
|
+
* Named distinctly from the Fabric component view `TinyuiPopoverView`.
|
|
22
|
+
*/
|
|
23
|
+
struct TinyuiPopoverSwiftUIView: View {
|
|
24
|
+
@ObservedObject var props: TinyuiPopoverProps
|
|
25
|
+
weak var delegate: TinyuiPopoverViewDelegate?
|
|
26
|
+
|
|
27
|
+
@State private var isPresented: Bool = false
|
|
28
|
+
|
|
29
|
+
var body: some View {
|
|
30
|
+
if let triggerView = props.triggerView {
|
|
31
|
+
RepresentableView(view: triggerView)
|
|
32
|
+
.popover(
|
|
33
|
+
isPresented: $isPresented,
|
|
34
|
+
attachmentAnchor: attachmentAnchorValue,
|
|
35
|
+
arrowEdge: arrowEdgeValue
|
|
36
|
+
) {
|
|
37
|
+
if #available(iOS 16.4, *) {
|
|
38
|
+
popoverContent
|
|
39
|
+
.presentationCompactAdaptation(.popover)
|
|
40
|
+
} else {
|
|
41
|
+
popoverContent
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
.onChange(of: isPresented) { newValue in
|
|
45
|
+
delegate?.onIsPresentedChange(isPresented: newValue)
|
|
46
|
+
}
|
|
47
|
+
.onChange(of: props.isPresented) { newValue in
|
|
48
|
+
isPresented = newValue
|
|
49
|
+
}
|
|
50
|
+
.onAppear {
|
|
51
|
+
isPresented = props.isPresented
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
Color.clear
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private var attachmentAnchorValue: PopoverAttachmentAnchor {
|
|
59
|
+
switch props.attachmentAnchor {
|
|
60
|
+
case "top": return .point(.top)
|
|
61
|
+
case "bottom": return .point(.bottom)
|
|
62
|
+
case "leading": return .point(.leading)
|
|
63
|
+
case "trailing": return .point(.trailing)
|
|
64
|
+
default: return .point(.center)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private var arrowEdgeValue: Edge? {
|
|
69
|
+
switch props.arrowEdge {
|
|
70
|
+
case "top": return .top
|
|
71
|
+
case "bottom": return .bottom
|
|
72
|
+
case "leading": return .leading
|
|
73
|
+
case "trailing": return .trailing
|
|
74
|
+
default: return nil
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@ViewBuilder
|
|
79
|
+
private var popoverContent: some View {
|
|
80
|
+
if let contentView = props.contentView {
|
|
81
|
+
RepresentableView(view: contentView)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@objc public protocol TinyuiPopoverViewDelegate {
|
|
87
|
+
func onIsPresentedChange(isPresented: Bool)
|
|
88
|
+
}
|