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
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import TipKit
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
@objc public protocol TinyuiTipPopoverDelegate: AnyObject {
|
|
5
|
+
func onTipStatus(tipId: String, status: String, reason: String)
|
|
6
|
+
func onTipVisible(tipId: String, visible: Bool)
|
|
7
|
+
func onTipAction(tipId: String, id: String, title: String)
|
|
8
|
+
func onTipError(tipId: String, code: String, message: String)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@MainActor
|
|
12
|
+
@objc public final class TinyuiTipPopoverProvider: UIView, UIPopoverPresentationControllerDelegate {
|
|
13
|
+
private weak var delegate: TinyuiTipPopoverDelegate?
|
|
14
|
+
@objc public weak var parentViewController: UIViewController? {
|
|
15
|
+
didSet { TinyuiTipsRuntime.shared.requestUpdate() }
|
|
16
|
+
}
|
|
17
|
+
private var tip: TinyuiTip?
|
|
18
|
+
private var enabled = false
|
|
19
|
+
private var arrowEdge = "auto"
|
|
20
|
+
private var observation: Task<Void, Never>?
|
|
21
|
+
private var observationGeneration = 0
|
|
22
|
+
private var status: Tips.Status?
|
|
23
|
+
private var lastError: String?
|
|
24
|
+
private var suppressed = false
|
|
25
|
+
private var controller: TipUIPopoverViewController?
|
|
26
|
+
private var presentedTipId: String?
|
|
27
|
+
private var isPresenting = false
|
|
28
|
+
private var isDismissing = false
|
|
29
|
+
private var reportedVisible = false
|
|
30
|
+
|
|
31
|
+
@objc public convenience init(delegate: TinyuiTipPopoverDelegate) {
|
|
32
|
+
self.init(frame: .zero)
|
|
33
|
+
self.delegate = delegate
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@objc public func configure(_ configuration: [String: Any], enabled: Bool, arrowEdge: String) {
|
|
37
|
+
let nextTip = TinyuiTip(
|
|
38
|
+
id: configuration["tipId"] as? String ?? "",
|
|
39
|
+
titleString: configuration["title"] as? String ?? "",
|
|
40
|
+
messageString: configuration["message"] as? String ?? "",
|
|
41
|
+
symbolName: configuration["systemImage"] as? String ?? "",
|
|
42
|
+
actionContents: (configuration["actions"] as? [[String: String]] ?? []).map {
|
|
43
|
+
TinyuiTip.ActionContent(id: $0["id"] ?? "", title: $0["title"] ?? "")
|
|
44
|
+
},
|
|
45
|
+
maximumDisplayCount: configuration["maxDisplayCount"] as? Int ?? 0,
|
|
46
|
+
ignoresFrequency: configuration["ignoresDisplayFrequency"] as? Bool ?? false
|
|
47
|
+
)
|
|
48
|
+
if tip?.id != nextTip.id {
|
|
49
|
+
stopObservation()
|
|
50
|
+
status = nil
|
|
51
|
+
lastError = nil
|
|
52
|
+
suppressed = false
|
|
53
|
+
}
|
|
54
|
+
if !self.enabled && enabled { suppressed = false }
|
|
55
|
+
tip = nextTip
|
|
56
|
+
self.enabled = enabled
|
|
57
|
+
self.arrowEdge = arrowEdge
|
|
58
|
+
TinyuiTipsRuntime.shared.requestUpdate()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override public func didMoveToWindow() {
|
|
62
|
+
super.didMoveToWindow()
|
|
63
|
+
if window == nil {
|
|
64
|
+
stopObservation()
|
|
65
|
+
dismissTip()
|
|
66
|
+
TinyuiTipsRuntime.shared.unregister(self)
|
|
67
|
+
} else {
|
|
68
|
+
TinyuiTipsRuntime.shared.register(self)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override public func layoutSubviews() {
|
|
73
|
+
super.layoutSubviews()
|
|
74
|
+
configureArrow()
|
|
75
|
+
TinyuiTipsRuntime.shared.requestUpdate()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
var needsMonitoring: Bool {
|
|
79
|
+
window?.windowScene?.activationState == .foregroundActive &&
|
|
80
|
+
enabled && status == .available && !suppressed && lastError == nil
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func updatePresentation() {
|
|
84
|
+
let runtime = TinyuiTipsRuntime.shared
|
|
85
|
+
guard let tip else { dismissTip(); return }
|
|
86
|
+
guard window != nil, runtime.isConfigured else {
|
|
87
|
+
if window != nil && enabled && !runtime.isConfigured {
|
|
88
|
+
reportError("E_TIPS_NOT_CONFIGURED", "Call and await TipKit.configure() before displaying tips.")
|
|
89
|
+
}
|
|
90
|
+
dismissTip()
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
do {
|
|
94
|
+
try runtime.remember(tip)
|
|
95
|
+
lastError = nil
|
|
96
|
+
} catch {
|
|
97
|
+
let error = error as NSError
|
|
98
|
+
reportError(error.userInfo["code"] as? String ?? "E_TIP", error.localizedDescription)
|
|
99
|
+
dismissTip()
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
startObservation(tip)
|
|
103
|
+
|
|
104
|
+
if let controller, !isPresenting, !isDismissing, controller.presentingViewController == nil {
|
|
105
|
+
// Dismissal may originate inside TipKit rather than through the delegate.
|
|
106
|
+
suppressed = true
|
|
107
|
+
finishDismissal(controller)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
let presenter = eligiblePresenter()
|
|
111
|
+
guard enabled, status == .available, !suppressed, presenter != nil,
|
|
112
|
+
presentedTipId == nil || presentedTipId == tip.id else {
|
|
113
|
+
dismissTip()
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
guard controller == nil, runtime.activeProvider == nil, let presenter else { return }
|
|
117
|
+
runtime.activeProvider = self
|
|
118
|
+
let tipController = TipUIPopoverViewController(tip, sourceItem: self) { [weak self] action in
|
|
119
|
+
guard let self, self.tip?.id == tip.id,
|
|
120
|
+
let content = tip.actionContents.first(where: { $0.id == action.id }) else { return }
|
|
121
|
+
self.delegate?.onTipAction(tipId: tip.id, id: content.id, title: content.title)
|
|
122
|
+
}
|
|
123
|
+
tipController.presentationDelegate = self
|
|
124
|
+
controller = tipController
|
|
125
|
+
presentedTipId = tip.id
|
|
126
|
+
configureArrow()
|
|
127
|
+
isPresenting = true
|
|
128
|
+
presenter.present(tipController, animated: true) { [weak self, weak tipController] in
|
|
129
|
+
guard let self, let tipController, self.controller === tipController else { return }
|
|
130
|
+
self.isPresenting = false
|
|
131
|
+
if tipController.presentingViewController != nil {
|
|
132
|
+
self.reportedVisible = true
|
|
133
|
+
self.delegate?.onTipVisible(tipId: tip.id, visible: true)
|
|
134
|
+
}
|
|
135
|
+
TinyuiTipsRuntime.shared.requestUpdate()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private func startObservation(_ tip: TinyuiTip) {
|
|
140
|
+
guard observation == nil else { return }
|
|
141
|
+
observationGeneration += 1
|
|
142
|
+
let generation = observationGeneration
|
|
143
|
+
observation = Task { @MainActor [weak self] in
|
|
144
|
+
for await status in tip.statusUpdates {
|
|
145
|
+
guard !Task.isCancelled, let self, self.observationGeneration == generation else { break }
|
|
146
|
+
self.status = status
|
|
147
|
+
let state: String
|
|
148
|
+
let reason: String
|
|
149
|
+
switch status {
|
|
150
|
+
case .pending: state = "pending"; reason = ""
|
|
151
|
+
case .available: state = "available"; reason = ""
|
|
152
|
+
case .invalidated(let invalidation):
|
|
153
|
+
state = "invalidated"
|
|
154
|
+
switch invalidation {
|
|
155
|
+
case .actionPerformed: reason = "actionPerformed"
|
|
156
|
+
case .tipClosed: reason = "tipClosed"
|
|
157
|
+
case .displayCountExceeded: reason = "displayCountExceeded"
|
|
158
|
+
case .displayDurationExceeded: reason = "displayDurationExceeded"
|
|
159
|
+
@unknown default: reason = "unknown"
|
|
160
|
+
}
|
|
161
|
+
@unknown default: state = "pending"; reason = ""
|
|
162
|
+
}
|
|
163
|
+
self.delegate?.onTipStatus(tipId: tip.id, status: state, reason: reason)
|
|
164
|
+
TinyuiTipsRuntime.shared.requestUpdate()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private func stopObservation() {
|
|
170
|
+
observationGeneration += 1
|
|
171
|
+
observation?.cancel()
|
|
172
|
+
observation = nil
|
|
173
|
+
status = nil
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Reject obscured/detached anchors and non-current navigation/tab screens.
|
|
177
|
+
private func eligiblePresenter() -> UIViewController? {
|
|
178
|
+
guard let window, window.windowScene?.activationState == .foregroundActive,
|
|
179
|
+
!window.isHidden, let parent = parentViewController,
|
|
180
|
+
parent.viewIfLoaded?.window === window, !bounds.isEmpty else { return nil }
|
|
181
|
+
|
|
182
|
+
var visibleRect = convert(bounds, to: window)
|
|
183
|
+
var ancestor: UIView? = self
|
|
184
|
+
while let view = ancestor {
|
|
185
|
+
guard !view.isHidden, view.alpha > 0.01 else { return nil }
|
|
186
|
+
if view.clipsToBounds {
|
|
187
|
+
visibleRect = visibleRect.intersection(view.convert(view.bounds, to: window))
|
|
188
|
+
}
|
|
189
|
+
ancestor = view.superview
|
|
190
|
+
}
|
|
191
|
+
guard !visibleRect.intersection(window.bounds).isEmpty else { return nil }
|
|
192
|
+
|
|
193
|
+
var child = parent
|
|
194
|
+
var ancestorController: UIViewController? = parent
|
|
195
|
+
while let current = ancestorController {
|
|
196
|
+
guard !current.isBeingDismissed, !current.isBeingPresented else { return nil }
|
|
197
|
+
if let navigation = current as? UINavigationController,
|
|
198
|
+
navigation.topViewController !== child { return nil }
|
|
199
|
+
if let tabs = current as? UITabBarController,
|
|
200
|
+
tabs.selectedViewController !== child { return nil }
|
|
201
|
+
if let presented = current.presentedViewController, presented !== controller { return nil }
|
|
202
|
+
// Wait for navigation animations before presenting. Do not treat our own
|
|
203
|
+
// popover's presentation transition as the anchor leaving the screen.
|
|
204
|
+
if controller == nil && current.transitionCoordinator != nil { return nil }
|
|
205
|
+
child = current
|
|
206
|
+
ancestorController = current.parent
|
|
207
|
+
}
|
|
208
|
+
return parent
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private func configureArrow() {
|
|
212
|
+
guard let popover = controller?.popoverPresentationController else { return }
|
|
213
|
+
popover.sourceView = self
|
|
214
|
+
popover.sourceRect = bounds
|
|
215
|
+
let isRTL = effectiveUserInterfaceLayoutDirection == .rightToLeft
|
|
216
|
+
switch arrowEdge {
|
|
217
|
+
case "top": popover.permittedArrowDirections = .up
|
|
218
|
+
case "bottom": popover.permittedArrowDirections = .down
|
|
219
|
+
case "leading": popover.permittedArrowDirections = isRTL ? .right : .left
|
|
220
|
+
case "trailing": popover.permittedArrowDirections = isRTL ? .left : .right
|
|
221
|
+
default: popover.permittedArrowDirections = .any
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private func dismissTip() {
|
|
226
|
+
guard let controller, !isPresenting, !isDismissing else { return }
|
|
227
|
+
guard controller.presentingViewController != nil else { finishDismissal(controller); return }
|
|
228
|
+
isDismissing = true
|
|
229
|
+
controller.dismiss(animated: window != nil) { [weak self, weak controller] in
|
|
230
|
+
guard let self, let controller else { return }
|
|
231
|
+
self.finishDismissal(controller)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private func finishDismissal(_ dismissed: TipUIPopoverViewController) {
|
|
236
|
+
guard controller === dismissed else { return }
|
|
237
|
+
if reportedVisible, let presentedTipId {
|
|
238
|
+
delegate?.onTipVisible(tipId: presentedTipId, visible: false)
|
|
239
|
+
}
|
|
240
|
+
controller = nil
|
|
241
|
+
presentedTipId = nil
|
|
242
|
+
isPresenting = false
|
|
243
|
+
isDismissing = false
|
|
244
|
+
reportedVisible = false
|
|
245
|
+
TinyuiTipsRuntime.shared.release(self)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
|
249
|
+
guard let controller, presentationController.presentedViewController === controller else { return }
|
|
250
|
+
suppressed = true
|
|
251
|
+
finishDismissal(controller)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { .none }
|
|
255
|
+
|
|
256
|
+
public func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { .none }
|
|
257
|
+
|
|
258
|
+
private func reportError(_ code: String, _ message: String) {
|
|
259
|
+
guard lastError != code else { return }
|
|
260
|
+
lastError = code
|
|
261
|
+
delegate?.onTipError(tipId: tip?.id ?? "", code: code, message: message)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@objc public func reset() {
|
|
265
|
+
enabled = false
|
|
266
|
+
tip = nil
|
|
267
|
+
suppressed = false
|
|
268
|
+
lastError = nil
|
|
269
|
+
stopObservation()
|
|
270
|
+
dismissTip()
|
|
271
|
+
parentViewController = nil
|
|
272
|
+
TinyuiTipsRuntime.shared.unregister(self)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
deinit { observation?.cancel() }
|
|
276
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#if TINYUI_FEATURE_TIP_KIT
|
|
2
|
+
#import "TinyuiTipPopoverView.h"
|
|
3
|
+
#import <React/RCTConversions.h>
|
|
4
|
+
#import <React/UIView+React.h>
|
|
5
|
+
#import <react/renderer/components/TinyuiSpec/ComponentDescriptors.h>
|
|
6
|
+
#import <react/renderer/components/TinyuiSpec/EventEmitters.h>
|
|
7
|
+
#import <react/renderer/components/TinyuiSpec/Props.h>
|
|
8
|
+
#import <react/renderer/components/TinyuiSpec/RCTComponentViewHelpers.h>
|
|
9
|
+
#import "RCTFabricComponentsPlugins.h"
|
|
10
|
+
#import "Tinyui-Swift.h"
|
|
11
|
+
|
|
12
|
+
using namespace facebook::react;
|
|
13
|
+
|
|
14
|
+
@interface TinyuiTipPopoverView () <RCTTinyuiTipPopoverViewViewProtocol, TinyuiTipPopoverDelegate>
|
|
15
|
+
@end
|
|
16
|
+
|
|
17
|
+
@implementation TinyuiTipPopoverView {
|
|
18
|
+
TinyuiTipPopoverProvider *_provider;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
22
|
+
{
|
|
23
|
+
return concreteComponentDescriptorProvider<TinyuiTipPopoverViewComponentDescriptor>();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
27
|
+
{
|
|
28
|
+
if (self = [super initWithFrame:frame]) {
|
|
29
|
+
static const auto defaultProps = std::make_shared<const TinyuiTipPopoverViewProps>();
|
|
30
|
+
_props = defaultProps;
|
|
31
|
+
_provider = [[TinyuiTipPopoverProvider alloc] initWithDelegate:self];
|
|
32
|
+
self.contentView = _provider;
|
|
33
|
+
}
|
|
34
|
+
return self;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
- (void)didMoveToWindow
|
|
38
|
+
{
|
|
39
|
+
[super didMoveToWindow];
|
|
40
|
+
_provider.parentViewController = self.window != nil ? [self reactViewController] : nil;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
|
|
44
|
+
{
|
|
45
|
+
[_provider insertSubview:childComponentView atIndex:index];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
|
|
49
|
+
{
|
|
50
|
+
[childComponentView removeFromSuperview];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
54
|
+
{
|
|
55
|
+
const auto &next = *std::static_pointer_cast<const TinyuiTipPopoverViewProps>(props);
|
|
56
|
+
NSMutableArray *actions = [NSMutableArray new];
|
|
57
|
+
for (const auto &action : next.actions) {
|
|
58
|
+
[actions addObject:@{@"id": RCTNSStringFromString(action.id), @"title": RCTNSStringFromString(action.title)}];
|
|
59
|
+
}
|
|
60
|
+
[_provider configure:@{
|
|
61
|
+
@"tipId": RCTNSStringFromString(next.tipId),
|
|
62
|
+
@"title": RCTNSStringFromString(next.title),
|
|
63
|
+
@"message": RCTNSStringFromString(next.message),
|
|
64
|
+
@"systemImage": RCTNSStringFromString(next.systemImage),
|
|
65
|
+
@"actions": actions,
|
|
66
|
+
@"maxDisplayCount": @(next.maxDisplayCount),
|
|
67
|
+
@"ignoresDisplayFrequency": @(next.ignoresDisplayFrequency),
|
|
68
|
+
} enabled:next.enabled arrowEdge:RCTNSStringFromString(next.arrowEdge)];
|
|
69
|
+
[super updateProps:props oldProps:oldProps];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
- (void)onTipStatusWithTipId:(NSString *)tipId status:(NSString *)status reason:(NSString *)reason
|
|
73
|
+
{
|
|
74
|
+
if (_eventEmitter == nullptr) return;
|
|
75
|
+
std::static_pointer_cast<const TinyuiTipPopoverViewEventEmitter>(_eventEmitter)->onTipStatusChange({
|
|
76
|
+
.tipId = std::string(tipId.UTF8String ?: ""),
|
|
77
|
+
.status = std::string(status.UTF8String ?: ""),
|
|
78
|
+
.reason = std::string(reason.UTF8String ?: ""),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
- (void)onTipVisibleWithTipId:(NSString *)tipId visible:(BOOL)visible
|
|
83
|
+
{
|
|
84
|
+
if (_eventEmitter == nullptr) return;
|
|
85
|
+
std::static_pointer_cast<const TinyuiTipPopoverViewEventEmitter>(_eventEmitter)->onTipVisibleChange({
|
|
86
|
+
.tipId = std::string(tipId.UTF8String ?: ""), .visible = static_cast<bool>(visible),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
- (void)onTipActionWithTipId:(NSString *)tipId id:(NSString *)identifier title:(NSString *)title
|
|
91
|
+
{
|
|
92
|
+
if (_eventEmitter == nullptr) return;
|
|
93
|
+
std::static_pointer_cast<const TinyuiTipPopoverViewEventEmitter>(_eventEmitter)->onTipAction({
|
|
94
|
+
.tipId = std::string(tipId.UTF8String ?: ""),
|
|
95
|
+
.id = std::string(identifier.UTF8String ?: ""), .title = std::string(title.UTF8String ?: ""),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
- (void)onTipErrorWithTipId:(NSString *)tipId code:(NSString *)code message:(NSString *)message
|
|
100
|
+
{
|
|
101
|
+
if (_eventEmitter == nullptr) return;
|
|
102
|
+
std::static_pointer_cast<const TinyuiTipPopoverViewEventEmitter>(_eventEmitter)->onTipError({
|
|
103
|
+
.tipId = std::string(tipId.UTF8String ?: ""),
|
|
104
|
+
.code = std::string(code.UTF8String ?: ""), .message = std::string(message.UTF8String ?: ""),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
- (void)prepareForRecycle
|
|
109
|
+
{
|
|
110
|
+
[_provider reset];
|
|
111
|
+
[super prepareForRecycle];
|
|
112
|
+
}
|
|
113
|
+
@end
|
|
114
|
+
#endif
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import TipKit
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// Shared across RN surfaces and Fast Refresh. TipKit's datastore owns history.
|
|
5
|
+
@MainActor
|
|
6
|
+
@objc public final class TinyuiTipsRuntime: NSObject {
|
|
7
|
+
static let shared = TinyuiTipsRuntime()
|
|
8
|
+
|
|
9
|
+
private final class WeakProvider {
|
|
10
|
+
weak var value: TinyuiTipPopoverProvider?
|
|
11
|
+
init(_ value: TinyuiTipPopoverProvider) { self.value = value }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
private var frequency: String?
|
|
15
|
+
private var providers: [WeakProvider] = []
|
|
16
|
+
private var timer: Timer?
|
|
17
|
+
private var updateScheduled = false
|
|
18
|
+
private var knownTips: [String: TinyuiTip] = [:]
|
|
19
|
+
// Keep an in-flight presentation alive long enough to dismiss it even if
|
|
20
|
+
// Fabric unmounts/recycles its owning component during the animation.
|
|
21
|
+
var activeProvider: TinyuiTipPopoverProvider?
|
|
22
|
+
|
|
23
|
+
private override init() {
|
|
24
|
+
super.init()
|
|
25
|
+
NotificationCenter.default.addObserver(self, selector: #selector(applicationStateChanged), name: UIApplication.didBecomeActiveNotification, object: nil)
|
|
26
|
+
NotificationCenter.default.addObserver(self, selector: #selector(applicationStateChanged), name: UIApplication.willResignActiveNotification, object: nil)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@objc private func applicationStateChanged() { requestUpdate() }
|
|
30
|
+
|
|
31
|
+
var isConfigured: Bool { frequency != nil }
|
|
32
|
+
|
|
33
|
+
@objc public static func configure(_ frequency: String, completion: (NSError?) -> Void) {
|
|
34
|
+
do {
|
|
35
|
+
try shared.configure(frequency)
|
|
36
|
+
completion(nil)
|
|
37
|
+
} catch {
|
|
38
|
+
completion(error as NSError)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@objc public static func invalidate(_ tipId: String, reason: String, completion: (NSError?) -> Void) {
|
|
43
|
+
guard shared.isConfigured else {
|
|
44
|
+
completion(error("E_TIPS_NOT_CONFIGURED", "Call and await TipKit.configure() before invalidating a tip."))
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
guard !tipId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
|
48
|
+
reason == "actionPerformed" || reason == "tipClosed" else {
|
|
49
|
+
completion(error("E_INVALID_TIP", "A nonempty tipId and a supported invalidation reason are required."))
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
// Tip identity is independent of its view or Swift type. This also works
|
|
53
|
+
// before the tip mounts, when the feature is discovered without a tip.
|
|
54
|
+
let tip = shared.knownTips[tipId] ?? TinyuiTip(id: tipId)
|
|
55
|
+
tip.invalidate(reason: reason == "tipClosed" ? .tipClosed : .actionPerformed)
|
|
56
|
+
shared.requestUpdate()
|
|
57
|
+
completion(nil)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private func configure(_ frequency: String) throws {
|
|
61
|
+
if let configuredFrequency = self.frequency {
|
|
62
|
+
guard configuredFrequency == frequency else {
|
|
63
|
+
throw Self.error("E_TIPS_ALREADY_CONFIGURED", "TipKit is already configured with a different displayFrequency. Configure it once at app startup.")
|
|
64
|
+
}
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
let nativeFrequency: Tips.ConfigurationOption.DisplayFrequency
|
|
68
|
+
switch frequency {
|
|
69
|
+
case "immediate": nativeFrequency = .immediate
|
|
70
|
+
case "hourly": nativeFrequency = .hourly
|
|
71
|
+
case "daily": nativeFrequency = .daily
|
|
72
|
+
case "weekly": nativeFrequency = .weekly
|
|
73
|
+
case "monthly": nativeFrequency = .monthly
|
|
74
|
+
default: throw Self.error("E_INVALID_TIPS_CONFIGURATION", "Unsupported displayFrequency.")
|
|
75
|
+
}
|
|
76
|
+
try Tips.configure([.displayFrequency(nativeFrequency)])
|
|
77
|
+
self.frequency = frequency
|
|
78
|
+
requestUpdate()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static func error(_ code: String, _ message: String) -> NSError {
|
|
82
|
+
NSError(domain: "TinyuiTips", code: 1, userInfo: [
|
|
83
|
+
NSLocalizedDescriptionKey: message,
|
|
84
|
+
"code": code,
|
|
85
|
+
])
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func register(_ provider: TinyuiTipPopoverProvider) {
|
|
89
|
+
providers.removeAll { $0.value == nil }
|
|
90
|
+
if !providers.contains(where: { $0.value === provider }) {
|
|
91
|
+
providers.append(WeakProvider(provider))
|
|
92
|
+
}
|
|
93
|
+
requestUpdate()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func unregister(_ provider: TinyuiTipPopoverProvider) {
|
|
97
|
+
providers.removeAll { $0.value == nil || $0.value === provider }
|
|
98
|
+
requestUpdate()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func remember(_ tip: TinyuiTip) throws {
|
|
102
|
+
if let previous = knownTips[tip.id],
|
|
103
|
+
previous.maximumDisplayCount != tip.maximumDisplayCount || previous.ignoresFrequency != tip.ignoresFrequency {
|
|
104
|
+
throw Self.error("E_CONFLICTING_TIP_OPTIONS", "maxDisplayCount and ignoresDisplayFrequency must remain consistent for the same tipId.")
|
|
105
|
+
}
|
|
106
|
+
knownTips[tip.id] = tip
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func release(_ provider: TinyuiTipPopoverProvider) {
|
|
110
|
+
if activeProvider === provider { activeProvider = nil }
|
|
111
|
+
requestUpdate()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func requestUpdate() {
|
|
115
|
+
guard !updateScheduled else { return }
|
|
116
|
+
updateScheduled = true
|
|
117
|
+
DispatchQueue.main.async { [weak self] in
|
|
118
|
+
guard let self else { return }
|
|
119
|
+
self.updateScheduled = false
|
|
120
|
+
self.update()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private func update() {
|
|
125
|
+
providers.removeAll { $0.value == nil }
|
|
126
|
+
// Finish dismissing an old presentation before offering its slot to another.
|
|
127
|
+
activeProvider?.updatePresentation()
|
|
128
|
+
for provider in providers.compactMap(\.value) where provider !== activeProvider {
|
|
129
|
+
provider.updatePresentation()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// UIKit has no single notification for an anchor being scrolled offscreen,
|
|
133
|
+
// a navigation transition ending, or another modal being dismissed. Poll
|
|
134
|
+
// only while a tip is eligible/on screen; idle or disabled tips do no work.
|
|
135
|
+
let needsMonitoring = activeProvider != nil || providers.contains { $0.value?.needsMonitoring == true }
|
|
136
|
+
if needsMonitoring && timer == nil {
|
|
137
|
+
let timer = Timer(timeInterval: 0.25, repeats: true) { [weak self] _ in
|
|
138
|
+
Task { @MainActor [weak self] in self?.update() }
|
|
139
|
+
}
|
|
140
|
+
timer.tolerance = 0.05
|
|
141
|
+
RunLoop.main.add(timer, forMode: .common)
|
|
142
|
+
self.timer = timer
|
|
143
|
+
} else if !needsMonitoring {
|
|
144
|
+
timer?.invalidate()
|
|
145
|
+
timer = nil
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
codegenNativeComponent,
|
|
3
|
+
type CodegenTypes,
|
|
4
|
+
type ViewProps,
|
|
5
|
+
} from 'react-native';
|
|
6
|
+
|
|
7
|
+
export interface NativeConcentricViewProps extends ViewProps {
|
|
8
|
+
minimumRadius?: CodegenTypes.WithDefault<CodegenTypes.Double, 0>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default codegenNativeComponent<NativeConcentricViewProps>(
|
|
12
|
+
'TinyuiConcentricView',
|
|
13
|
+
{ excludedPlatforms: ['android'] }
|
|
14
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
import { StyleSheet } from 'react-native';
|
|
5
|
+
import { assertComponentEnabled } from "../utils/index.js";
|
|
6
|
+
import NativeConcentricView from './ConcentricViewNativeComponent';
|
|
7
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
|
+
export const ConcentricView = /*#__PURE__*/forwardRef(function ConcentricViewRoot({
|
|
9
|
+
minimumRadius = 0,
|
|
10
|
+
style,
|
|
11
|
+
...viewProps
|
|
12
|
+
}, ref) {
|
|
13
|
+
assertComponentEnabled('ConcentricView');
|
|
14
|
+
if (!Number.isFinite(minimumRadius) || minimumRadius < 0) {
|
|
15
|
+
throw new RangeError('[TinyUI] ConcentricView minimumRadius must be a finite, nonnegative number.');
|
|
16
|
+
}
|
|
17
|
+
return /*#__PURE__*/_jsx(NativeConcentricView, {
|
|
18
|
+
...viewProps,
|
|
19
|
+
ref: ref,
|
|
20
|
+
minimumRadius: minimumRadius,
|
|
21
|
+
style: [styles.container, style]
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
const styles = StyleSheet.create({
|
|
25
|
+
container: {
|
|
26
|
+
overflow: 'hidden'
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["forwardRef","StyleSheet","assertComponentEnabled","NativeConcentricView","jsx","_jsx","ConcentricView","ConcentricViewRoot","minimumRadius","style","viewProps","ref","Number","isFinite","RangeError","styles","container","create","overflow"],"sourceRoot":"../../../src","sources":["ConcentricView/index.tsx"],"mappings":";;AAAA,SAASA,UAAU,QAA2B,OAAO;AACrD,SAASC,UAAU,QAAmB,cAAc;AAEpD,SAASC,sBAAsB,QAAQ,mBAAU;AACjD,OAAOC,oBAAoB,MAAM,iCAAiC;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAGnE,OAAO,MAAMC,cAAc,gBAAGN,UAAU,CAGtC,SAASO,kBAAkBA,CAAC;EAAEC,aAAa,GAAG,CAAC;EAAEC,KAAK;EAAE,GAAGC;AAAU,CAAC,EAAEC,GAAG,EAAE;EAC7ET,sBAAsB,CAAC,gBAAgB,CAAC;EAExC,IAAI,CAACU,MAAM,CAACC,QAAQ,CAACL,aAAa,CAAC,IAAIA,aAAa,GAAG,CAAC,EAAE;IACxD,MAAM,IAAIM,UAAU,CAClB,6EACF,CAAC;EACH;EAEA,oBACET,IAAA,CAACF,oBAAoB;IAAA,GACfO,SAAS;IACbC,GAAG,EAAEA,GAAI;IACTH,aAAa,EAAEA,aAAc;IAC7BC,KAAK,EAAE,CAACM,MAAM,CAACC,SAAS,EAAEP,KAAK;EAAE,CAClC,CAAC;AAEN,CAAC,CAAC;AAEF,MAAMM,MAAM,GAAGd,UAAU,CAACgB,MAAM,CAAC;EAC/BD,SAAS,EAAE;IAAEE,QAAQ,EAAE;EAAS;AAClC,CAAC,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["ConcentricView/types.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
1
2
|
import {
|
|
2
3
|
codegenNativeComponent,
|
|
4
|
+
codegenNativeCommands,
|
|
3
5
|
type CodegenTypes,
|
|
6
|
+
type HostComponent,
|
|
4
7
|
type ViewProps,
|
|
5
8
|
} from 'react-native';
|
|
6
9
|
import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';
|
|
@@ -9,11 +12,17 @@ export interface NativeMenuProps extends ViewProps {
|
|
|
9
12
|
menuConfig: CodegenTypes.UnsafeMixed;
|
|
10
13
|
title?: string;
|
|
11
14
|
disabled?: boolean;
|
|
12
|
-
|
|
13
|
-
onItemPress?: DirectEventHandler<Readonly<{ id: string }>>;
|
|
14
|
-
onPrimaryAction?: DirectEventHandler<null>;
|
|
15
|
+
onActionPress?: DirectEventHandler<Readonly<{ id: string; title: string }>>;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
interface NativeCommands {
|
|
19
|
+
open: (viewRef: React.ComponentRef<HostComponent<NativeMenuProps>>) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const Commands = codegenNativeCommands<NativeCommands>({
|
|
23
|
+
supportedCommands: ['open'],
|
|
24
|
+
});
|
|
25
|
+
|
|
17
26
|
export default codegenNativeComponent<NativeMenuProps>('TinyuiMenuView', {
|
|
18
27
|
excludedPlatforms: ['android'],
|
|
19
28
|
});
|