react-native-nano-icons 0.1.4 → 0.1.6

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.
@@ -7,30 +7,41 @@
7
7
 
8
8
  using namespace facebook::react;
9
9
 
10
- // Drawing canvas that can be shifted outside the Yoga frame.
11
- @interface NanoIconDrawingView : UIView
12
- @property (nonatomic, copy) void (^drawBlock)(CGContextRef, CGRect);
10
+ // Forward-declare so the layer subclass can call the drawing method.
11
+ @interface NanoIconView ()
12
+ - (void)_drawIconInContext:(CGContextRef)context bounds:(CGRect)bounds;
13
13
  @end
14
14
 
15
- @implementation NanoIconDrawingView
16
- - (instancetype)initWithFrame:(CGRect)frame {
17
- if (self = [super initWithFrame:frame]) {
18
- self.opaque = NO;
19
- self.backgroundColor = [UIColor clearColor];
20
- self.userInteractionEnabled = NO;
21
- }
22
- return self;
23
- }
24
- - (void)drawRect:(CGRect)rect {
25
- if (self.drawBlock) {
26
- CGContextRef ctx = UIGraphicsGetCurrentContext();
27
- if (ctx) self.drawBlock(ctx, self.bounds);
28
- }
15
+ // Lightweight sublayer for inline-in-Text icons. Provides a shifted pixel
16
+ // buffer so the icon can overflow the Yoga frame without a full UIView.
17
+ @interface NanoIconDrawingLayer : CALayer
18
+ @property (nonatomic, weak) NanoIconView *owner;
19
+ @end
20
+
21
+ @implementation NanoIconDrawingLayer
22
+ - (void)drawInContext:(CGContextRef)ctx {
23
+ [self.owner _drawIconInContext:ctx bounds:self.bounds];
29
24
  }
30
25
  @end
31
26
 
27
+ // Process-wide CTFontRef cache keyed by (fontFamily, fontSize).
28
+ // Avoids 1000× CTFontCreateWithName for identical (family, size) combos.
29
+ static CTFontRef NanoIconGetCachedFont(NSString *family, CGFloat size) {
30
+ static NSMutableDictionary *cache;
31
+ static dispatch_once_t onceToken;
32
+ dispatch_once(&onceToken, ^{ cache = [NSMutableDictionary new]; });
33
+
34
+ NSString *key = [NSString stringWithFormat:@"%@:%.1f", family, size];
35
+ id existing = cache[key];
36
+ if (existing) return (__bridge CTFontRef)existing;
37
+
38
+ CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)family, size, NULL);
39
+ if (font) cache[key] = (__bridge id)font;
40
+ return font;
41
+ }
42
+
32
43
  @implementation NanoIconView {
33
- CTFontRef _font;
44
+ CTFontRef _font; // borrowed from static cache — do NOT CFRelease
34
45
  NSString *_fontFamily;
35
46
  CGFloat _fontSize;
36
47
  std::vector<CGGlyph> _glyphs;
@@ -39,7 +50,19 @@ using namespace facebook::react;
39
50
  CGFloat _fitScale;
40
51
  CGPoint _baselinePosition;
41
52
  BOOL _metricsValid;
42
- NanoIconDrawingView *_drawingView;
53
+
54
+ // Inline-in-Text detection — resolved once on first layout, cached until reparenting.
55
+ // Standalone icons (vast majority) skip the superview walk entirely after detection.
56
+ BOOL _inlineDetected;
57
+ BOOL _isInlineInText;
58
+ UIView * __weak _paragraphView;
59
+ CGFloat _cachedBaselineOffset;
60
+ BOOL _baselineOffsetValid;
61
+
62
+ // Drawing sublayer for inline icons — provides a shifted pixel buffer so the
63
+ // icon can overflow the Yoga frame. Lighter than a UIView (no responder chain,
64
+ // hit testing, or accessibility). Standalone icons draw directly via drawRect:.
65
+ CALayer *_drawingLayer;
43
66
  }
44
67
 
45
68
  - (instancetype)initWithFrame:(CGRect)frame {
@@ -49,16 +72,12 @@ using namespace facebook::react;
49
72
  self.opaque = NO;
50
73
  self.backgroundColor = [UIColor clearColor];
51
74
  self.clipsToBounds = NO;
75
+ self.contentMode = UIViewContentModeRedraw;
52
76
 
53
77
  _fitScale = 1.0;
54
78
  _baselinePosition = CGPointZero;
55
79
 
56
- _drawingView = [[NanoIconDrawingView alloc] initWithFrame:self.bounds];
57
- __weak __typeof(self) weakSelf = self;
58
- _drawingView.drawBlock = ^(CGContextRef context, CGRect bounds) {
59
- [weakSelf _drawIconInContext:context bounds:bounds];
60
- };
61
- [self addSubview:_drawingView];
80
+ // _drawingLayer is created lazily only when inline in Text
62
81
  }
63
82
  return self;
64
83
  }
@@ -87,23 +106,47 @@ using namespace facebook::react;
87
106
  _metricsValid = YES;
88
107
  }
89
108
 
90
- // Distance from this view's bottom edge to the parent text baseline.
91
- // Returns 0 when standalone or when the icon is taller than the text line.
92
- - (CGFloat)_inlineBaselineOffset {
109
+ // Detect whether this icon is inline inside a <Text> component and, if so,
110
+ // compute the baseline offset in a single pass. Deferred to first layout
111
+ // (not didMoveToSuperview) because Fabric may assemble the hierarchy bottom-up.
112
+ // For standalone icons this completes in 1-3 class name checks with no
113
+ // ObjC runtime method resolution or attributed string reads.
114
+ - (void)_detectAndCacheInlineState {
115
+ _inlineDetected = YES;
116
+ _isInlineInText = NO;
117
+ _paragraphView = nil;
118
+ _cachedBaselineOffset = 0;
119
+ _baselineOffsetValid = YES;
120
+
121
+ // RCTParagraphComponentView is the immediate or near-immediate parent
122
+ // when the icon is inside <Text>. Three levels covers all known layouts.
93
123
  UIView *current = self.superview;
94
- NSAttributedString *attrStr = nil;
95
- while (current) {
96
- if ([current respondsToSelector:@selector(attributedText)]) {
97
- attrStr = [current performSelector:@selector(attributedText)];
98
- if (attrStr.length > 0) break;
124
+ UIView *target = nil;
125
+ for (int i = 0; i < 3 && current; i++) {
126
+ if ([NSStringFromClass([current class]) isEqualToString:@"RCTParagraphComponentView"]) {
127
+ target = current;
128
+ break;
99
129
  }
100
130
  current = current.superview;
101
131
  }
102
- if (!attrStr) return 0;
132
+
133
+ if (!target) return;
134
+
135
+ NSAttributedString *attrStr = nil;
136
+ if ([target respondsToSelector:@selector(attributedText)]) {
137
+ attrStr = [target performSelector:@selector(attributedText)];
138
+ }
139
+ if (!attrStr || attrStr.length == 0) return;
103
140
 
104
141
  UIFont *f = [attrStr attribute:NSFontAttributeName atIndex:0 effectiveRange:nil];
105
- if (!f) return 0;
142
+ if (!f) return;
143
+
144
+ _isInlineInText = YES;
145
+ _paragraphView = target;
106
146
 
147
+ // Derive the distance from this view's bottom edge to the text baseline.
148
+ // Respects custom RN lineHeight (mapped to NSParagraphStyle.maximumLineHeight)
149
+ // and any baseline offset applied by Fabric's text layout.
107
150
  NSParagraphStyle *style = [attrStr attribute:NSParagraphStyleAttributeName
108
151
  atIndex:0 effectiveRange:nil];
109
152
  CGFloat lineHeight = (style && style.maximumLineHeight > 0)
@@ -117,22 +160,79 @@ using namespace facebook::react;
117
160
  CGFloat posInLine = fmod(frameBottom, lineHeight);
118
161
  if (posInLine < 0.01) posInLine = lineHeight;
119
162
 
120
- return MAX(0, posInLine - baselineFromLineTop);
163
+ _cachedBaselineOffset = MAX(0, posInLine - baselineFromLineTop);
164
+ }
165
+
166
+ // Lazily create the drawing sublayer for inline-in-Text icons.
167
+ // The sublayer's frame is shifted upward so the icon overflows the Yoga box.
168
+ - (void)_ensureDrawingLayer {
169
+ if (_drawingLayer) return;
170
+ NanoIconDrawingLayer *layer = [NanoIconDrawingLayer layer];
171
+ layer.owner = self;
172
+ layer.opaque = NO;
173
+ layer.contentsScale = [UIScreen mainScreen].scale;
174
+ [self.layer addSublayer:layer];
175
+ _drawingLayer = layer;
176
+ }
177
+
178
+ // Re-detect inline state when the view moves to a new parent.
179
+ - (void)didMoveToSuperview {
180
+ [super didMoveToSuperview];
181
+ _inlineDetected = NO;
182
+ _baselineOffsetValid = NO;
183
+ }
184
+
185
+ // Invalidate cached offset when size changes (text relayout).
186
+ - (void)setBounds:(CGRect)bounds {
187
+ if (!CGSizeEqualToSize(self.bounds.size, bounds.size)) {
188
+ _baselineOffsetValid = NO;
189
+ }
190
+ [super setBounds:bounds];
121
191
  }
122
192
 
123
193
  #pragma mark - Layout
124
194
 
195
+ // Standalone: no work beyond metrics validation (drawing via drawRect: on self).
196
+ // Inline: position the drawing sublayer with the cached baseline offset,
197
+ // recomputing only when bounds change or the view is reparented.
125
198
  - (void)layoutSubviews {
126
199
  [super layoutSubviews];
127
200
  if (!_metricsValid) [self _updateMetrics];
128
201
 
129
- CGFloat offset = [self _inlineBaselineOffset];
130
- _drawingView.frame = CGRectMake(0, -offset,
131
- self.bounds.size.width, self.bounds.size.height);
202
+ if (!_inlineDetected) {
203
+ [self _detectAndCacheInlineState];
204
+ }
205
+
206
+ if (_isInlineInText) {
207
+ if (!_baselineOffsetValid) {
208
+ [self _detectAndCacheInlineState];
209
+ }
210
+ BOOL created = !_drawingLayer;
211
+ [self _ensureDrawingLayer];
212
+ CGRect newFrame = CGRectMake(0, -_cachedBaselineOffset,
213
+ self.bounds.size.width, self.bounds.size.height);
214
+ if (!CGRectEqualToRect(_drawingLayer.frame, newFrame)) {
215
+ [CATransaction begin];
216
+ [CATransaction setDisableActions:YES];
217
+ _drawingLayer.frame = newFrame;
218
+ [CATransaction commit];
219
+ }
220
+ // First layout after inline detection: the layer missed the initial
221
+ // setNeedsDisplay from updateProps (which targeted self before detection).
222
+ if (created) [_drawingLayer setNeedsDisplay];
223
+ }
224
+ // standalone: draws directly via drawRect: on self
132
225
  }
133
226
 
134
227
  #pragma mark - Drawing
135
228
 
229
+ // Standalone icons draw directly in this view's drawRect:.
230
+ - (void)drawRect:(CGRect)rect {
231
+ if (_isInlineInText) return; // inline icons draw via _drawingLayer
232
+ CGContextRef ctx = UIGraphicsGetCurrentContext();
233
+ if (ctx) [self _drawIconInContext:ctx bounds:self.bounds];
234
+ }
235
+
136
236
  // Render multi-color icons by drawing each color layer glyph at the same
137
237
  // position. Layers stack via painter's order to compose the final icon.
138
238
  - (void)_drawIconInContext:(CGContextRef)context bounds:(CGRect)bounds {
@@ -229,10 +329,9 @@ using namespace facebook::react;
229
329
 
230
330
  if (oldViewProps.fontFamily != newViewProps.fontFamily ||
231
331
  oldViewProps.fontSize != newViewProps.fontSize) {
232
- if (_font) { CFRelease(_font); _font = NULL; }
233
332
  _fontFamily = [NSString stringWithUTF8String:newViewProps.fontFamily.c_str()];
234
333
  _fontSize = newViewProps.fontSize;
235
- _font = CTFontCreateWithName((__bridge CFStringRef)_fontFamily, _fontSize, NULL);
334
+ _font = NanoIconGetCachedFont(_fontFamily, _fontSize);
236
335
  _metricsValid = NO;
237
336
  fontChanged = YES;
238
337
  needsRedraw = YES;
@@ -271,11 +370,17 @@ using namespace facebook::react;
271
370
  }
272
371
 
273
372
  [super updateProps:props oldProps:oldProps];
274
- if (needsRedraw) [_drawingView setNeedsDisplay];
373
+ if (needsRedraw) {
374
+ if (_isInlineInText && _drawingLayer) {
375
+ [_drawingLayer setNeedsDisplay];
376
+ } else {
377
+ [self setNeedsDisplay];
378
+ }
379
+ }
275
380
  }
276
381
 
277
382
  - (void)dealloc {
278
- if (_font) CFRelease(_font);
383
+ // _font is borrowed from static cache — do not release
279
384
  [self _releaseCachedColors];
280
385
  }
281
386
 
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nano-icons",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Use any svg as font. High-performance, build-time icon font generation and rendering for React Native & Expo.",
5
5
  "react-native": "src/index.ts",
6
6
  "source": "src/index.ts",