@react-native-ama/core 2.0.0-beta.1 → 2.0.0-beta.2
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/ama.config.json +17 -0
- package/android/build.gradle +43 -0
- package/android/src/debug/AndroidManifest.xml +2 -0
- package/android/src/debug/java/expo/modules/ama/ReactNativeAmaModule.kt +544 -0
- package/android/src/debug/java/expo/modules/ama/ReactNativeAmaView.kt +30 -0
- package/android/src/debug/java/expo/modules/ama/highlight.kt +189 -0
- package/android/src/debug/java/expo/modules/ama/logger.kt +25 -0
- package/android/src/debug/java/expo/modules/ama/nodesGrabber.kt +664 -0
- package/expo-module.config.json +9 -0
- package/ios/Highlight.swift +264 -0
- package/ios/Logger.swift +31 -0
- package/ios/NodesGrabber.swift +733 -0
- package/ios/ReactNativeAma.podspec +29 -0
- package/ios/ReactNativeAmaModule.swift +568 -0
- package/ios/ReactNativeAmaView.swift +38 -0
- package/ios/TapGesture.swift +107 -0
- package/package.json +8 -2
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Helper class to observe when a view is deallocated and trigger cleanup
|
|
4
|
+
private class ViewLifecycleObserver {
|
|
5
|
+
weak var view: UIView?
|
|
6
|
+
let viewTag: Int
|
|
7
|
+
weak var highlight: Highlight?
|
|
8
|
+
|
|
9
|
+
init(view: UIView, highlight: Highlight) {
|
|
10
|
+
self.view = view
|
|
11
|
+
self.viewTag = view.tag
|
|
12
|
+
self.highlight = highlight
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
deinit {
|
|
16
|
+
// View is being deallocated, clean up the highlight
|
|
17
|
+
highlight?.clearHighlight(viewId: viewTag)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Simple wrapper that holds a weak reference to a view
|
|
22
|
+
private class WeakViewBox {
|
|
23
|
+
weak var view: UIView?
|
|
24
|
+
|
|
25
|
+
init(_ view: UIView) {
|
|
26
|
+
self.view = view
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public class Highlight {
|
|
31
|
+
private let stripeOverlayTag = 0xA11
|
|
32
|
+
private let borderLayerName = "ama_border"
|
|
33
|
+
private var stripeOverlays = [Int: UIView]()
|
|
34
|
+
private var borderLayers = [Int: CAShapeLayer]()
|
|
35
|
+
|
|
36
|
+
/// Maps view tag to a weak reference of the actual view for orphan detection
|
|
37
|
+
private var trackedViews = [Int: WeakViewBox]()
|
|
38
|
+
|
|
39
|
+
public init() {}
|
|
40
|
+
|
|
41
|
+
public func highlight(view: UIView, mode: String, hexColor: String, gap: CGFloat = 0, lineWidth: CGFloat = 3, issueCount: Int = 1) {
|
|
42
|
+
let color = UIColor(hex: hexColor) ?? .red
|
|
43
|
+
|
|
44
|
+
DispatchQueue.main.async {
|
|
45
|
+
// Clean up any orphaned highlights before adding new ones
|
|
46
|
+
self.cleanupOrphanedHighlights()
|
|
47
|
+
|
|
48
|
+
// Track this view
|
|
49
|
+
self.trackedViews[view.tag] = WeakViewBox(view)
|
|
50
|
+
|
|
51
|
+
switch mode {
|
|
52
|
+
case "background":
|
|
53
|
+
self.applyStripyBackground(to: view, color: color)
|
|
54
|
+
case "border":
|
|
55
|
+
self.applyBorderOverlay(to: view, color: color, gap: gap, lineWidth: lineWidth, issueCount: issueCount)
|
|
56
|
+
default:
|
|
57
|
+
self.applyBorderOverlay(to: view, color: color, gap: gap, lineWidth: lineWidth, issueCount: issueCount)
|
|
58
|
+
self.applyStripyBackground(to: view, color: color)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public func clearHighlight(viewId: Int) {
|
|
64
|
+
DispatchQueue.main.async {
|
|
65
|
+
self.trackedViews.removeValue(forKey: viewId)
|
|
66
|
+
self.clearStripeOverlay(viewId: viewId)
|
|
67
|
+
self.clearBorderOverlay(viewId: viewId)
|
|
68
|
+
self.cleanupOrphanedHighlights()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Cleans up highlights for views that are no longer in the view hierarchy
|
|
73
|
+
/// This handles the case where a component/screen is unmounted
|
|
74
|
+
public func cleanupOrphanedHighlights() {
|
|
75
|
+
// Find all viewIds where the tracked view is nil (deallocated) or has no window (removed from hierarchy)
|
|
76
|
+
let orphanedViewIds = trackedViews.compactMap { (viewId, box) -> Int? in
|
|
77
|
+
// View is nil (deallocated) or not in any window (removed from hierarchy)
|
|
78
|
+
if box.view == nil || box.view?.window == nil {
|
|
79
|
+
return viewId
|
|
80
|
+
}
|
|
81
|
+
return nil
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Clear highlights for orphaned views
|
|
85
|
+
for viewId in orphanedViewIds {
|
|
86
|
+
trackedViews.removeValue(forKey: viewId)
|
|
87
|
+
clearStripeOverlay(viewId: viewId)
|
|
88
|
+
clearBorderOverlay(viewId: viewId)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private func clearStripeOverlay(viewId: Int) {
|
|
93
|
+
if let overlay = stripeOverlays.removeValue(forKey: viewId) {
|
|
94
|
+
overlay.removeFromSuperview()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private func clearBorderOverlay(viewId: Int) {
|
|
99
|
+
if let border = borderLayers.removeValue(forKey: viewId) {
|
|
100
|
+
border.removeFromSuperlayer()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Call this when you know the screen is going away
|
|
105
|
+
public func clearAll() {
|
|
106
|
+
stripeOverlays.values.forEach { $0.removeFromSuperview() }
|
|
107
|
+
borderLayers.values.forEach { $0.removeFromSuperlayer() }
|
|
108
|
+
stripeOverlays.removeAll()
|
|
109
|
+
borderLayers.removeAll()
|
|
110
|
+
trackedViews.removeAll()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public func clearHighlight2(viewId: Int) {
|
|
114
|
+
guard let root = UIApplication.shared.keyWindow,
|
|
115
|
+
let target = root.viewWithTag(viewId)
|
|
116
|
+
else { return }
|
|
117
|
+
|
|
118
|
+
target.viewWithTag(stripeOverlayTag)?.removeFromSuperview()
|
|
119
|
+
|
|
120
|
+
target.layer.sublayers?
|
|
121
|
+
.filter { $0.name == borderLayerName }
|
|
122
|
+
.forEach { $0.removeFromSuperlayer() }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private func applyStripyBackground(to view: UIView, color: UIColor) {
|
|
126
|
+
view.viewWithTag(stripeOverlayTag)?.removeFromSuperview()
|
|
127
|
+
|
|
128
|
+
let stripeImage = makeStripyPatternImage(color: color)
|
|
129
|
+
let overlay = UIView(frame: view.bounds)
|
|
130
|
+
overlay.tag = stripeOverlayTag
|
|
131
|
+
overlay.isUserInteractionEnabled = false
|
|
132
|
+
overlay.backgroundColor = UIColor(patternImage: stripeImage)
|
|
133
|
+
overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
134
|
+
view.addSubview(overlay)
|
|
135
|
+
|
|
136
|
+
stripeOverlays[view.tag] = overlay
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private func applyBorderOverlay(to view: UIView, color: UIColor, gap: CGFloat, lineWidth: CGFloat = 3, issueCount: Int = 1) {
|
|
140
|
+
view.layer.sublayers?
|
|
141
|
+
.filter { $0.name == borderLayerName }
|
|
142
|
+
.forEach { $0.removeFromSuperlayer() }
|
|
143
|
+
|
|
144
|
+
let stroke: CGFloat = lineWidth
|
|
145
|
+
let border = CAShapeLayer()
|
|
146
|
+
border.name = borderLayerName
|
|
147
|
+
border.frame = view.bounds
|
|
148
|
+
border.lineWidth = stroke
|
|
149
|
+
border.strokeColor = color.cgColor
|
|
150
|
+
border.fillColor = UIColor.clear.cgColor
|
|
151
|
+
|
|
152
|
+
// Make the border dotted
|
|
153
|
+
let dashLength: NSNumber = NSNumber(value: Double(stroke * 2))
|
|
154
|
+
let gapLength: NSNumber = NSNumber(value: Double(stroke * 2))
|
|
155
|
+
border.lineDashPattern = [dashLength, gapLength]
|
|
156
|
+
|
|
157
|
+
let inset = (stroke / 2) + gap
|
|
158
|
+
let rect = view.bounds.insetBy(dx: -inset, dy: -inset)
|
|
159
|
+
border.path = UIBezierPath(rect: rect).cgPath
|
|
160
|
+
|
|
161
|
+
// Create circle badge with issue count
|
|
162
|
+
let badgeSize: CGFloat = 24
|
|
163
|
+
let badgeLayer = CAShapeLayer()
|
|
164
|
+
badgeLayer.frame = CGRect(
|
|
165
|
+
x: rect.maxX - badgeSize / 2,
|
|
166
|
+
y: rect.minY - badgeSize / 2,
|
|
167
|
+
width: badgeSize,
|
|
168
|
+
height: badgeSize
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
// Draw circle
|
|
172
|
+
let circlePath = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: badgeSize, height: badgeSize))
|
|
173
|
+
badgeLayer.path = circlePath.cgPath
|
|
174
|
+
badgeLayer.fillColor = color.cgColor
|
|
175
|
+
|
|
176
|
+
// Add issue count text
|
|
177
|
+
let textLayer = CATextLayer()
|
|
178
|
+
textLayer.frame = badgeLayer.bounds
|
|
179
|
+
textLayer.string = "\(issueCount)"
|
|
180
|
+
textLayer.fontSize = badgeSize * 0.55
|
|
181
|
+
textLayer.foregroundColor = UIColor.white.cgColor
|
|
182
|
+
textLayer.alignmentMode = .center
|
|
183
|
+
textLayer.contentsScale = UIScreen.main.scale
|
|
184
|
+
|
|
185
|
+
// Center text vertically
|
|
186
|
+
let font = UIFont.boldSystemFont(ofSize: badgeSize * 0.55)
|
|
187
|
+
let textHeight = font.lineHeight
|
|
188
|
+
textLayer.frame = CGRect(
|
|
189
|
+
x: 0,
|
|
190
|
+
y: (badgeSize - textHeight) / 2,
|
|
191
|
+
width: badgeSize,
|
|
192
|
+
height: textHeight
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
badgeLayer.addSublayer(textLayer)
|
|
196
|
+
border.addSublayer(badgeLayer)
|
|
197
|
+
view.layer.addSublayer(border)
|
|
198
|
+
|
|
199
|
+
borderLayers[view.tag] = border
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
func makeStripyPatternImage(
|
|
203
|
+
stripeWidth: CGFloat = 2,
|
|
204
|
+
gapWidth: CGFloat = 10,
|
|
205
|
+
tileSize: CGFloat = 150,
|
|
206
|
+
color: UIColor = .red
|
|
207
|
+
) -> UIImage {
|
|
208
|
+
let size = CGSize(width: tileSize, height: tileSize)
|
|
209
|
+
let extended = tileSize * 1.5
|
|
210
|
+
|
|
211
|
+
let base = UIGraphicsImageRenderer(size: size).image { ctx in
|
|
212
|
+
let c = ctx.cgContext
|
|
213
|
+
c.setFillColor(color.cgColor)
|
|
214
|
+
|
|
215
|
+
c.translateBy(x: size.width / 2, y: size.height / 2)
|
|
216
|
+
c.rotate(by: -.pi / 4)
|
|
217
|
+
c.translateBy(x: -size.width / 2, y: -size.height / 2)
|
|
218
|
+
|
|
219
|
+
var y: CGFloat = -extended
|
|
220
|
+
while y < extended {
|
|
221
|
+
c.fill(
|
|
222
|
+
CGRect(
|
|
223
|
+
x: -extended,
|
|
224
|
+
y: y,
|
|
225
|
+
width: extended * 2,
|
|
226
|
+
height: stripeWidth))
|
|
227
|
+
y += stripeWidth + gapWidth
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return base.resizableImage(withCapInsets: .zero, resizingMode: .tile)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
extension UIColor {
|
|
236
|
+
convenience init?(hex: String) {
|
|
237
|
+
var s = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
238
|
+
.replacingOccurrences(of: "0x", with: "")
|
|
239
|
+
.replacingOccurrences(of: "#", with: "")
|
|
240
|
+
.uppercased()
|
|
241
|
+
|
|
242
|
+
if s.count == 3 || s.count == 4 {
|
|
243
|
+
s = s.map { "\($0)\($0)" }.joined()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
guard s.count == 6 || s.count == 8,
|
|
247
|
+
let value = UInt64(s, radix: 16) else { return nil }
|
|
248
|
+
|
|
249
|
+
let r, g, b, a: CGFloat
|
|
250
|
+
if s.count == 6 {
|
|
251
|
+
r = CGFloat((value & 0xFF0000) >> 16) / 255.0
|
|
252
|
+
g = CGFloat((value & 0x00FF00) >> 8) / 255.0
|
|
253
|
+
b = CGFloat( value & 0x0000FF) / 255.0
|
|
254
|
+
a = 1.0
|
|
255
|
+
} else {
|
|
256
|
+
r = CGFloat((value & 0xFF000000) >> 24) / 255.0
|
|
257
|
+
g = CGFloat((value & 0x00FF0000) >> 16) / 255.0
|
|
258
|
+
b = CGFloat((value & 0x0000FF00) >> 8) / 255.0
|
|
259
|
+
a = CGFloat( value & 0x000000FF) / 255.0
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
self.init(red: r, green: g, blue: b, alpha: a)
|
|
263
|
+
}
|
|
264
|
+
}
|
package/ios/Logger.swift
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
public struct Logger {
|
|
4
|
+
// Replace with your own bundle identifier or subsystem
|
|
5
|
+
private static let subsystem = Bundle.main.bundleIdentifier ?? "com.your.app"
|
|
6
|
+
private static let log = os.Logger(subsystem: subsystem, category: "ReactNative AMA")
|
|
7
|
+
|
|
8
|
+
public static func info(_ fn: String, _ message: String, extra: String? = nil) {
|
|
9
|
+
if let extra = extra, !extra.isEmpty {
|
|
10
|
+
log.info("[INFO]: \(fn) \(message) >>> \(extra, privacy: .public)")
|
|
11
|
+
} else {
|
|
12
|
+
log.info("[INFO]: \(fn) \(message, privacy: .public)")
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public static func info2(_ fn: String, _ message: String) {
|
|
17
|
+
info(" \(fn)", message)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public static func debug(_ fn: String, _ message: String, extra: String? = nil) {
|
|
21
|
+
if let extra = extra, !extra.isEmpty {
|
|
22
|
+
log.info("[DEBUG]: \(fn) \(message) >>> \(extra, privacy: .public)")
|
|
23
|
+
} else {
|
|
24
|
+
log.info("[DEBUG]: \(fn) \(message, privacy: .public)")
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public static func error(_ fn: String, _ message: String) {
|
|
29
|
+
log.info("[ERROR]: \(fn) \(message, privacy: .public)")
|
|
30
|
+
}
|
|
31
|
+
}
|