@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.
@@ -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
+ }
@@ -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
+ }