@onekeyfe/react-native-bottom-tabs 6.7.0-alpha.409 → 6.7.0-alpha.412

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.
@@ -107,7 +107,7 @@ dependencies {
107
107
 
108
108
  implementation("io.coil-kt.coil3:coil:${COIL_VERSION}")
109
109
  implementation("io.coil-kt.coil3:coil-network-okhttp:${COIL_VERSION}")
110
- implementation("io.coil-kt.coil3:coil-svg:${COIL_VERSION}")
110
+ implementation project(":onekeyfe_react-native-image")
111
111
  }
112
112
 
113
113
  if (isNewArchitectureEnabled()) {
@@ -9,6 +9,7 @@ import androidx.appcompat.view.ContextThemeWrapper
9
9
  import android.graphics.Color
10
10
  import android.graphics.Canvas
11
11
  import android.graphics.Outline
12
+ import android.graphics.drawable.BitmapDrawable
12
13
  import android.graphics.drawable.ColorDrawable
13
14
  import android.graphics.drawable.Drawable
14
15
  import android.graphics.drawable.GradientDrawable
@@ -39,7 +40,6 @@ import androidx.core.view.forEachIndexed
39
40
  import coil3.ImageLoader
40
41
  import coil3.asDrawable
41
42
  import coil3.request.ImageRequest
42
- import coil3.svg.SvgDecoder
43
43
  import coil3.size.Precision
44
44
  import coil3.size.Size as CoilSize
45
45
  import coil3.size.Scale
@@ -55,9 +55,12 @@ import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY
55
55
  import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_LABELED
56
56
  import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_UNLABELED
57
57
  import com.google.android.material.transition.platform.MaterialFadeThrough
58
+ import com.margelo.nitro.onekeyimage.OneKeyNamedSVG
58
59
  import kotlin.math.abs
59
60
  import kotlin.math.roundToInt
60
61
 
62
+ private const val ONEKEY_ICON_URI_PREFIX = "onekey-icon://"
63
+
61
64
  private fun getMaterialContext(context: Context): Context {
62
65
  return ContextThemeWrapper(context, com.google.android.material.R.style.Theme_MaterialComponents_DayNight)
63
66
  }
@@ -193,6 +196,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
193
196
  var items: MutableList<TabInfo> = mutableListOf()
194
197
  private val iconSources: MutableMap<Int, ImageSource> = mutableMapOf()
195
198
  private val drawableCache: MutableMap<ImageSource, Drawable> = mutableMapOf()
199
+ private val iconRequestTokens: MutableMap<Int, Any> = mutableMapOf()
196
200
 
197
201
  private var isLayoutEnqueued = false
198
202
  private var selectedItem: String? = null
@@ -260,7 +264,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
260
264
 
261
265
  private val imageLoader = ImageLoader.Builder(context)
262
266
  .components {
263
- add(SvgDecoder.Factory())
267
+ add(SharedSvgDecoder.Factory())
264
268
  }
265
269
  .build()
266
270
 
@@ -466,7 +470,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
466
470
  drawableCache.clear()
467
471
  iconSources.forEach { (index, source) ->
468
472
  bottomNavigation.menu.findItem(index)?.let { menuItem ->
469
- getDrawable(source) { menuItem.icon = it }
473
+ getDrawable(index, source) { menuItem.icon = it }
470
474
  }
471
475
  }
472
476
  }
@@ -1253,7 +1257,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1253
1257
 
1254
1258
  menuItem.isVisible = !item.hidden
1255
1259
  if (iconSources.containsKey(index)) {
1256
- getDrawable(iconSources[index]!!) {
1260
+ getDrawable(index, iconSources[index]!!) {
1257
1261
  menuItem.icon = it
1258
1262
  }
1259
1263
  }
@@ -1323,7 +1327,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1323
1327
 
1324
1328
  // Update existing item if exists.
1325
1329
  bottomNavigation.menu.findItem(idx)?.let { menuItem ->
1326
- getDrawable(imageSource) {
1330
+ getDrawable(idx, imageSource) {
1327
1331
  menuItem.icon = it
1328
1332
  }
1329
1333
  }
@@ -1355,11 +1359,50 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1355
1359
  }
1356
1360
 
1357
1361
  @SuppressLint("CheckResult")
1358
- private fun getDrawable(imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) {
1362
+ private fun getDrawable(index: Int, imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) {
1363
+ iconRequestTokens.remove(index)?.let { OneKeyNamedSVG.cancel(it) }
1359
1364
  drawableCache[imageSource]?.let {
1360
1365
  onDrawableReady(it)
1361
1366
  return
1362
1367
  }
1368
+ val rawSource = imageSource.uri.orEmpty()
1369
+ if (rawSource.startsWith(ONEKEY_ICON_URI_PREFIX)) {
1370
+ val iconName = rawSource.removePrefix(ONEKEY_ICON_URI_PREFIX).trim()
1371
+ if (iconName.isEmpty()) {
1372
+ onDrawableReady(null)
1373
+ return
1374
+ }
1375
+ val token = Any()
1376
+ iconRequestTokens[index] = token
1377
+ val iconSizePx = bottomNavigation.itemIconSize.coerceAtLeast(1)
1378
+ val density = resources.displayMetrics.density
1379
+ OneKeyNamedSVG.request(
1380
+ owner = token,
1381
+ assets = context.applicationContext.assets,
1382
+ name = iconName,
1383
+ color = "rgba(0,0,0,1)",
1384
+ width = iconSizePx,
1385
+ height = iconSizePx,
1386
+ scale = density,
1387
+ ) { bitmap ->
1388
+ if (iconRequestTokens[index] !== token) {
1389
+ return@request
1390
+ }
1391
+ iconRequestTokens.remove(index)
1392
+ if (bitmap == null) {
1393
+ onDrawableReady(null)
1394
+ return@request
1395
+ }
1396
+ val stateDrawable = BitmapDrawable(context.resources, bitmap)
1397
+ drawableCache[imageSource] = stateDrawable
1398
+ if (iconSources[index] == imageSource) {
1399
+ onDrawableReady(stateDrawable)
1400
+ }
1401
+ }
1402
+ return
1403
+ }
1404
+ val token = Any()
1405
+ iconRequestTokens[index] = token
1363
1406
  val iconSizePx = bottomNavigation.itemIconSize
1364
1407
  val request = ImageRequest.Builder(context)
1365
1408
  .data(imageSource.getUri(context))
@@ -1368,9 +1411,15 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1368
1411
  .precision(Precision.EXACT)
1369
1412
  .target { drawable ->
1370
1413
  post {
1414
+ if (iconRequestTokens[index] !== token) {
1415
+ return@post
1416
+ }
1417
+ iconRequestTokens.remove(index)
1371
1418
  val stateDrawable = drawable.asDrawable(context.resources)
1372
1419
  drawableCache[imageSource] = stateDrawable
1373
- onDrawableReady(stateDrawable)
1420
+ if (iconSources[index] == imageSource) {
1421
+ onDrawableReady(stateDrawable)
1422
+ }
1374
1423
  }
1375
1424
  }
1376
1425
  .listener(
@@ -1440,6 +1489,8 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
1440
1489
  }
1441
1490
 
1442
1491
  fun onDropViewInstance() {
1492
+ iconRequestTokens.values.forEach { OneKeyNamedSVG.cancel(it) }
1493
+ iconRequestTokens.clear()
1443
1494
  androidBottomBlurEnabled = false
1444
1495
  updateBlurAppearance()
1445
1496
  releaseBottomBlur()
@@ -0,0 +1,51 @@
1
+ package com.onekey.bottomtabs
2
+
3
+ import coil3.ImageLoader
4
+ import coil3.asImage
5
+ import coil3.decode.DecodeResult
6
+ import coil3.decode.DecodeUtils
7
+ import coil3.decode.Decoder
8
+ import coil3.fetch.SourceFetchResult
9
+ import coil3.request.Options
10
+ import coil3.request.maxBitmapSize
11
+ import com.margelo.nitro.onekeyimage.OneKeySVG
12
+ import kotlinx.coroutines.runInterruptible
13
+ import okio.ByteString.Companion.encodeUtf8
14
+ import kotlin.math.roundToInt
15
+
16
+ /** Coil retains fetching, cancellation and sizing; image owns SVG parsing/rendering. */
17
+ internal class SharedSvgDecoder(private val result: SourceFetchResult, private val options: Options) : Decoder {
18
+ override suspend fun decode(): DecodeResult = runInterruptible {
19
+ val document = result.source.source().inputStream().use { OneKeySVG.decode(it) }
20
+ val width = document.viewBoxWidth ?: document.intrinsicWidth
21
+ val height = document.viewBoxHeight ?: document.intrinsicHeight
22
+ val destination = DecodeUtils.computeDstSize(
23
+ if (width > 0) width.roundToInt() else 512,
24
+ if (height > 0) height.roundToInt() else 512,
25
+ options.size, options.scale, options.maxBitmapSize)
26
+ val dstWidth = destination.first
27
+ val dstHeight = destination.second
28
+ val (outputWidth, outputHeight) = if (width > 0 && height > 0) {
29
+ val multiplier = DecodeUtils.computeSizeMultiplier(width, height, dstWidth.toFloat(), dstHeight.toFloat(), options.scale)
30
+ (width * multiplier).toInt() to (height * multiplier).toInt()
31
+ } else dstWidth to dstHeight
32
+ DecodeResult(document.render(outputWidth, outputHeight).asImage(), isSampled = true)
33
+ }
34
+
35
+ class Factory : Decoder.Factory {
36
+ override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
37
+ if (result.mimeType != "image/svg+xml") {
38
+ val source = result.source.source()
39
+ if (!source.rangeEquals(0, "<".encodeUtf8())) return null
40
+ val tag = "<svg".encodeUtf8()
41
+ var found = false
42
+ for (index in 0L until (1024L - tag.size)) {
43
+ if (!source.request(index + tag.size)) break
44
+ if (source.rangeEquals(index, tag)) { found = true; break }
45
+ }
46
+ if (!found) return null
47
+ }
48
+ return SharedSvgDecoder(result, options)
49
+ }
50
+ }
51
+ }
package/docs/SPEC.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Native Tab View behavioral contract
2
2
 
3
+ ## Shared image SVG decoder — Implemented
4
+
5
+ BottomTabs depends on the matching image SDK peer/native module and delegates
6
+ SVG byte decoding to its native facade. No new third-party dependency or SVG
7
+ renderer is introduced. JavaScript props, source resolution and existing tab
8
+ behavior remain unchanged. There is no independent Web SVG reader to migrate.
9
+
10
+ iOS keeps the existing SvgDecoder RCTImageDataDecoder registration (which is
11
+ available to the React image loader beyond tabs), source detection, failure
12
+ delivery and its no-op cancellation block; only data-to-image implementation
13
+ moves out of the local CoreSVG fork. Existing URI loaders, selected/unselected
14
+ and side-accessory paths, SF Symbols, resize/tint and source ordering remain.
15
+ Raw decoding may retain intrinsic/vector dimensions where the old decoder
16
+ ignored target dimensions; it does not use named-icon workers or wait for a
17
+ main-thread callback.
18
+
19
+ Android retains Coil fetching, cache and ordinary image decoders. Its SVG factory
20
+ uses image's parse/render facility with the existing requested dimensions,
21
+ Scale.FILL and Precision.EXACT semantics. The independent coil-svg dependency is
22
+ removed once that route is replaced; no URI loading is moved to Glide. Decoder
23
+ execution remains in Coil's existing worker/cancellation/error lifecycle, with
24
+ no additional pool, cache or main-queue hop. Mutable drawable bounds are owned
25
+ per request, while shared raster data remains immutable.
26
+
27
+ Focused SDK fixtures cover raw SVG and ordinary images through actual RCTImageLoader
28
+ and Coil/BottomTabs consumers, source sizing/non-square art and iOS SF Symbols.
29
+ The current iOS 18.6 fixture does not accept iOS 27.1 side placement; complete
30
+ App styling, platform navigation and signed release acceptance remain separate. Existing source callback races are
31
+ not silently repaired as part of decoding unification. Builds and runtime
32
+ evidence are reported separately. This implementation does not change global image
33
+ loader registration or default-device configuration.
34
+
3
35
  ## Native left drawer
4
36
 
5
37
  Status: implemented in module source. Package TypeScript, iOS/Android Fabric
@@ -1,6 +1,8 @@
1
1
  import Foundation
2
2
  import UIKit
3
+ #if canImport(React)
3
4
  import React
5
+ #endif
4
6
 
5
7
  extension Notification.Name {
6
8
  static let bottomAccessoryPlacementChanged = Notification.Name("bottomAccessoryPlacementChanged")
@@ -1,6 +1,71 @@
1
1
  import Foundation
2
2
  import UIKit
3
+ #if canImport(React)
3
4
  import React
5
+ #endif
6
+
7
+ @objc private protocol OneKeyNamedSVGBridgeProtocol: AnyObject {
8
+ @objc(cancelWithOwner:)
9
+ func cancel(owner: AnyObject)
10
+
11
+ @objc(requestWithOwner:name:color:width:height:scale:complete:)
12
+ func request(
13
+ owner: AnyObject,
14
+ name: String,
15
+ color: String,
16
+ width: Int,
17
+ height: Int,
18
+ scale: Double,
19
+ complete: @escaping (UIImage?) -> Void
20
+ )
21
+ }
22
+
23
+ /// Bridges to OneKeyNamedSVG.shared in OneKeyImage without importing the OneKeyImage
24
+ /// Swift module (which transitively imports NitroModules C++ headers).
25
+ private enum OneKeyNamedSVGLoader {
26
+ private static var bridge: OneKeyNamedSVGBridgeProtocol? {
27
+ guard let cls: AnyClass = NSClassFromString("OneKeyNamedSVGBridge")
28
+ ?? NSClassFromString("OneKeyImage.OneKeyNamedSVGBridge")
29
+ else {
30
+ return nil
31
+ }
32
+ let meta = cls as AnyObject
33
+ guard meta.responds(to: #selector(OneKeyNamedSVGBridgeProtocol.cancel(owner:))),
34
+ meta.responds(to: #selector(OneKeyNamedSVGBridgeProtocol.request(owner:name:color:width:height:scale:complete:)))
35
+ else {
36
+ return nil
37
+ }
38
+ return unsafeBitCast(meta, to: OneKeyNamedSVGBridgeProtocol.self)
39
+ }
40
+
41
+ static func cancel(owner: AnyObject) {
42
+ bridge?.cancel(owner: owner)
43
+ }
44
+
45
+ static func request(
46
+ owner: AnyObject,
47
+ name: String,
48
+ color: String,
49
+ width: Int,
50
+ height: Int,
51
+ scale: CGFloat,
52
+ complete: @escaping (UIImage?) -> Void
53
+ ) {
54
+ guard let bridge else {
55
+ complete(nil)
56
+ return
57
+ }
58
+ bridge.request(
59
+ owner: owner,
60
+ name: name,
61
+ color: color,
62
+ width: width,
63
+ height: height,
64
+ scale: Double(scale),
65
+ complete: complete
66
+ )
67
+ }
68
+ }
4
69
 
5
70
  // MARK: - Data types for props from JS
6
71
 
@@ -77,6 +142,10 @@ class RCTBottomTabsContainerView: UIView {
77
142
  private var loadedSideAccessoryIcon: UIImage?
78
143
  // Increments per icon load; a completion applies only to the latest one.
79
144
  private var sideAccessoryIconRequest = 0
145
+ private static let oneKeyIconSchemePrefix = "onekey-icon://"
146
+ private var iconRequestTokens: [Int: NSObject] = [:]
147
+ private var selectedIconRequestTokens: [Int: NSObject] = [:]
148
+ private var sideAccessoryRequestToken = NSObject()
80
149
  private var isSideAccessoryLayoutScheduled = false
81
150
  private static let sideAccessorySize: CGFloat = 48
82
151
  private static let sideAccessorySpacing: CGFloat = 12
@@ -188,6 +257,22 @@ class RCTBottomTabsContainerView: UIView {
188
257
  fatalError("init(coder:) has not been implemented")
189
258
  }
190
259
 
260
+ deinit {
261
+ for token in iconRequestTokens.values {
262
+ OneKeyNamedSVGLoader.cancel(owner: token)
263
+ }
264
+ for token in selectedIconRequestTokens.values {
265
+ OneKeyNamedSVGLoader.cancel(owner: token)
266
+ }
267
+ OneKeyNamedSVGLoader.cancel(owner: sideAccessoryRequestToken)
268
+ }
269
+
270
+ private static func oneKeyIconName(from uri: String) -> String? {
271
+ guard uri.hasPrefix(oneKeyIconSchemePrefix) else { return nil }
272
+ let name = String(uri.dropFirst(oneKeyIconSchemePrefix.count)).trimmingCharacters(in: .whitespacesAndNewlines)
273
+ return name.isEmpty ? nil : name
274
+ }
275
+
191
276
  override func didAddSubview(_ subview: UIView) {
192
277
  super.didAddSubview(subview)
193
278
  if let tbcView = tabBarController?.view, subview !== tbcView {
@@ -217,7 +302,7 @@ class RCTBottomTabsContainerView: UIView {
217
302
  /// Where the system draws the tab bar: "bottom", or "leading"/"trailing" for
218
303
  /// the vertical bar some devices (iPhone Duo) place in a side strip.
219
304
  private func currentTabBarPlacement() -> String {
220
- #if compiler(>=6.4)
305
+ #if compiler(>=6.4) && !targetEnvironment(macCatalyst)
221
306
  if #available(iOS 27.1, *), let tbc = tabBarController {
222
307
  switch tbc.traitCollection.verticalBarEdge {
223
308
  case .leading: return "leading"
@@ -230,7 +315,7 @@ class RCTBottomTabsContainerView: UIView {
230
315
  }
231
316
 
232
317
  private func registerVerticalBarTraitChanges(_ tbc: UITabBarController) {
233
- #if compiler(>=6.4)
318
+ #if compiler(>=6.4) && !targetEnvironment(macCatalyst)
234
319
  if #available(iOS 27.1, *) {
235
320
  tbc.registerForTraitChanges(
236
321
  UITraitCollection.systemTraitsAffectingVerticalBarEdge
@@ -340,6 +425,7 @@ class RCTBottomTabsContainerView: UIView {
340
425
  private func loadSideAccessoryIcon() {
341
426
  sideAccessoryIconRequest += 1
342
427
  let request = sideAccessoryIconRequest
428
+ OneKeyNamedSVGLoader.cancel(owner: sideAccessoryRequestToken)
343
429
  // Drop the previous icon so a slower or failed load never leaves it shown
344
430
  // for the new source.
345
431
  if loadedSideAccessoryIcon != nil {
@@ -347,7 +433,37 @@ class RCTBottomTabsContainerView: UIView {
347
433
  scheduleSideAccessoryLayout()
348
434
  }
349
435
  let source = sideAccessoryIcon.map { IconSource(dict: $0) }
350
- guard let source, let url = URL(string: source.uri) else { return }
436
+ guard let source, !source.uri.isEmpty else { return }
437
+ if let iconName = Self.oneKeyIconName(from: source.uri) {
438
+ let token = NSObject()
439
+ sideAccessoryRequestToken = token
440
+ let scale = max(1.0, window?.screen.scale ?? UIScreen.main.scale)
441
+ let pxW = max(1, Int(ceil(iconSize.width * scale)))
442
+ let pxH = max(1, Int(ceil(iconSize.height * scale)))
443
+ OneKeyNamedSVGLoader.request(
444
+ owner: token,
445
+ name: iconName,
446
+ color: "rgba(0,0,0,1)",
447
+ width: pxW,
448
+ height: pxH,
449
+ scale: scale
450
+ ) { [weak self] image in
451
+ guard let self,
452
+ request == self.sideAccessoryIconRequest,
453
+ self.sideAccessoryRequestToken === token,
454
+ let image else { return }
455
+ let scaledImage: UIImage
456
+ if let cgImage = image.cgImage {
457
+ scaledImage = UIImage(cgImage: cgImage, scale: scale, orientation: .up)
458
+ } else {
459
+ scaledImage = image
460
+ }
461
+ self.loadedSideAccessoryIcon = scaledImage.withRenderingMode(.alwaysTemplate)
462
+ self.scheduleSideAccessoryLayout()
463
+ }
464
+ return
465
+ }
466
+ guard let url = URL(string: source.uri) else { return }
351
467
  // The image loader arrives with the first state update; setImageLoader
352
468
  // calls this again.
353
469
  guard let imageLoader else { return }
@@ -755,21 +871,69 @@ class RCTBottomTabsContainerView: UIView {
755
871
 
756
872
  @objc func setImageLoader(_ loader: RCTImageLoaderProtocol) {
757
873
  self.imageLoader = loader
758
- loadIconsFromSources()
759
- loadSelectedIconsFromSources()
760
- loadSideAccessoryIcon()
874
+ let iconSources = (icons ?? []).map { IconSource(dict: $0) }
875
+ if iconSources.contains(where: { !$0.uri.isEmpty && Self.oneKeyIconName(from: $0.uri) == nil }) {
876
+ loadIconsFromSources()
877
+ }
878
+ let selectedIconSources = (selectedIcons ?? []).map { IconSource(dict: $0) }
879
+ if selectedIconSources.contains(where: { !$0.uri.isEmpty && Self.oneKeyIconName(from: $0.uri) == nil }) {
880
+ loadSelectedIconsFromSources()
881
+ }
882
+ if let sideSource = sideAccessoryIcon.map({ IconSource(dict: $0) }),
883
+ !sideSource.uri.isEmpty,
884
+ Self.oneKeyIconName(from: sideSource.uri) == nil {
885
+ loadSideAccessoryIcon()
886
+ }
761
887
  }
762
888
 
763
889
  private func loadIconsFromSources() {
764
- guard let imageLoader, let iconDicts = icons else { return }
890
+ guard let iconDicts = icons else { return }
765
891
 
766
892
  let iconSources = iconDicts.map { IconSource(dict: $0) }
893
+ let imageLoader = self.imageLoader
894
+
895
+ for staleIndex in iconRequestTokens.keys where staleIndex >= iconSources.count {
896
+ if let previous = iconRequestTokens.removeValue(forKey: staleIndex) {
897
+ OneKeyNamedSVGLoader.cancel(owner: previous)
898
+ }
899
+ }
767
900
 
768
901
  for (index, source) in iconSources.enumerated() {
902
+ if let previous = iconRequestTokens.removeValue(forKey: index) {
903
+ OneKeyNamedSVGLoader.cancel(owner: previous)
904
+ }
769
905
  guard !source.uri.isEmpty else { continue }
906
+ let token = NSObject()
907
+ iconRequestTokens[index] = token
908
+
909
+ if let iconName = Self.oneKeyIconName(from: source.uri) {
910
+ let scale = max(1.0, window?.screen.scale ?? UIScreen.main.scale)
911
+ let pxW = max(1, Int(ceil(iconSize.width * scale)))
912
+ let pxH = max(1, Int(ceil(iconSize.height * scale)))
913
+ OneKeyNamedSVGLoader.request(
914
+ owner: token,
915
+ name: iconName,
916
+ color: "rgba(0,0,0,1)",
917
+ width: pxW,
918
+ height: pxH,
919
+ scale: scale
920
+ ) { [weak self] image in
921
+ guard let self,
922
+ self.iconRequestTokens[index] === token,
923
+ let image else { return }
924
+ let scaledImage: UIImage
925
+ if let cgImage = image.cgImage {
926
+ scaledImage = UIImage(cgImage: cgImage, scale: scale, orientation: .up)
927
+ } else {
928
+ scaledImage = image
929
+ }
930
+ self.loadedIcons[index] = scaledImage
931
+ self.rebuildViewControllers()
932
+ }
933
+ continue
934
+ }
770
935
 
771
- let url = URL(string: source.uri)
772
- guard let url else { continue }
936
+ guard let imageLoader, let url = URL(string: source.uri) else { continue }
773
937
 
774
938
  let request = URLRequest(url: url)
775
939
  let size = CGSize(width: source.width, height: source.height)
@@ -787,6 +951,7 @@ class RCTBottomTabsContainerView: UIView {
787
951
  if error != nil { return }
788
952
  guard let image, let self else { return }
789
953
  DispatchQueue.main.async {
954
+ guard self.iconRequestTokens[index] === token else { return }
790
955
  self.loadedIcons[index] = image.resizeImageTo(size: self.iconSize)
791
956
  self.rebuildViewControllers()
792
957
  }
@@ -795,15 +960,53 @@ class RCTBottomTabsContainerView: UIView {
795
960
  }
796
961
 
797
962
  private func loadSelectedIconsFromSources() {
798
- guard let imageLoader, let iconDicts = selectedIcons else { return }
963
+ guard let iconDicts = selectedIcons else { return }
799
964
 
800
965
  let iconSources = iconDicts.map { IconSource(dict: $0) }
966
+ let imageLoader = self.imageLoader
967
+
968
+ for staleIndex in selectedIconRequestTokens.keys where staleIndex >= iconSources.count {
969
+ if let previous = selectedIconRequestTokens.removeValue(forKey: staleIndex) {
970
+ OneKeyNamedSVGLoader.cancel(owner: previous)
971
+ }
972
+ }
801
973
 
802
974
  for (index, source) in iconSources.enumerated() {
975
+ if let previous = selectedIconRequestTokens.removeValue(forKey: index) {
976
+ OneKeyNamedSVGLoader.cancel(owner: previous)
977
+ }
803
978
  guard !source.uri.isEmpty else { continue }
979
+ let token = NSObject()
980
+ selectedIconRequestTokens[index] = token
981
+
982
+ if let iconName = Self.oneKeyIconName(from: source.uri) {
983
+ let scale = max(1.0, window?.screen.scale ?? UIScreen.main.scale)
984
+ let pxW = max(1, Int(ceil(iconSize.width * scale)))
985
+ let pxH = max(1, Int(ceil(iconSize.height * scale)))
986
+ OneKeyNamedSVGLoader.request(
987
+ owner: token,
988
+ name: iconName,
989
+ color: "rgba(0,0,0,1)",
990
+ width: pxW,
991
+ height: pxH,
992
+ scale: scale
993
+ ) { [weak self] image in
994
+ guard let self,
995
+ self.selectedIconRequestTokens[index] === token,
996
+ let image else { return }
997
+ let scaledImage: UIImage
998
+ if let cgImage = image.cgImage {
999
+ scaledImage = UIImage(cgImage: cgImage, scale: scale, orientation: .up)
1000
+ } else {
1001
+ scaledImage = image
1002
+ }
1003
+ self.loadedSelectedIcons[index] = scaledImage
1004
+ self.rebuildViewControllers()
1005
+ }
1006
+ continue
1007
+ }
804
1008
 
805
- let url = URL(string: source.uri)
806
- guard let url else { continue }
1009
+ guard let imageLoader, let url = URL(string: source.uri) else { continue }
807
1010
 
808
1011
  let request = URLRequest(url: url)
809
1012
  let size = CGSize(width: source.width, height: source.height)
@@ -821,6 +1024,7 @@ class RCTBottomTabsContainerView: UIView {
821
1024
  if error != nil { return }
822
1025
  guard let image, let self else { return }
823
1026
  DispatchQueue.main.async {
1027
+ guard self.selectedIconRequestTokens[index] === token else { return }
824
1028
  self.loadedSelectedIcons[index] = image.resizeImageTo(size: self.iconSize)
825
1029
  self.rebuildViewControllers()
826
1030
  }
@@ -1,5 +1,5 @@
1
1
  #import "SvgDecoder.h"
2
- #import "CoreSVG.h"
2
+ #import <OneKeyImage/OneKeyImageCoderBridge.h>
3
3
 
4
4
  @implementation SvgDecoder
5
5
 
@@ -7,7 +7,11 @@ RCT_EXPORT_MODULE()
7
7
 
8
8
  - (BOOL)canDecodeImageData:(NSData *)imageData
9
9
  {
10
- return [CoreSVGWrapper isSVGData:imageData];
10
+ if (!imageData) return NO;
11
+ // Preserve the existing global React image-decoder selection boundary.
12
+ return [imageData rangeOfData:[@"</svg>" dataUsingEncoding:NSUTF8StringEncoding]
13
+ options:NSDataSearchBackwards
14
+ range:NSMakeRange(imageData.length - MIN(100, imageData.length), MIN(100, imageData.length))].location != NSNotFound;
11
15
  }
12
16
 
13
17
  - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData
@@ -16,7 +20,7 @@ RCT_EXPORT_MODULE()
16
20
  resizeMode:(RCTResizeMode)resizeMode
17
21
  completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
18
22
  {
19
- UIImage *image = [CoreSVGWrapper.shared imageFromSVGData:imageData];
23
+ UIImage *image = [OneKeyImageCoderBridge decodeSVGData:imageData pixelSize:CGSizeZero];
20
24
 
21
25
  if (image) {
22
26
  completionHandler(nil, image);
@@ -1,4 +1,6 @@
1
+ #if canImport(React)
1
2
  import React
3
+ #endif
2
4
  import UIKit
3
5
 
4
6
  enum TabBarFontSize {
@@ -3,16 +3,74 @@
3
3
  import React, { useLayoutEffect, useRef } from "react";
4
4
  import { Image, Platform, StyleSheet, View, processColor } from "react-native";
5
5
  import { BottomTabBarHeightContext } from "./utils/BottomTabBarHeightContext.js";
6
-
7
- // eslint-disable-next-line @react-native/no-deep-imports
8
-
9
6
  import NativeBottomTabs from "./BottomTabsNativeComponent";
10
7
  import useLatestCallback from "use-latest-callback";
11
8
  import DelayedFreeze from "./DelayedFreeze.js";
12
9
  import NativeBottomTabsDrawer from "./BottomTabsDrawerNativeComponent";
13
10
  import { BottomAccessoryView } from "./BottomAccessoryView.js";
14
11
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
- const isAppleSymbol = icon => icon?.sfSymbol;
12
+ const isAppleSymbol = icon => Boolean(typeof icon === "object" && icon !== null && "sfSymbol" in icon && typeof icon.sfSymbol === "string");
13
+ const ONEKEY_ICON_URI_PREFIX = "onekey-icon://";
14
+ export const resolveTabIconSource = icon => {
15
+ if (!icon || isAppleSymbol(icon)) {
16
+ return {
17
+ uri: "",
18
+ width: 0,
19
+ height: 0,
20
+ scale: 1
21
+ };
22
+ }
23
+ if (typeof icon === "string") {
24
+ const trimmed = icon.trim();
25
+ if (!trimmed) {
26
+ return {
27
+ uri: "",
28
+ width: 0,
29
+ height: 0,
30
+ scale: 1
31
+ };
32
+ }
33
+ const uri = trimmed.startsWith(ONEKEY_ICON_URI_PREFIX) ? trimmed : `${ONEKEY_ICON_URI_PREFIX}${trimmed}`;
34
+ return {
35
+ uri,
36
+ width: 24,
37
+ height: 24,
38
+ scale: 1
39
+ };
40
+ }
41
+ if (typeof icon === "object" && !Array.isArray(icon) && "iconName" in icon && typeof icon.iconName === "string") {
42
+ const trimmed = icon.iconName.trim();
43
+ if (!trimmed) {
44
+ return {
45
+ uri: "",
46
+ width: 0,
47
+ height: 0,
48
+ scale: 1
49
+ };
50
+ }
51
+ return {
52
+ uri: `${ONEKEY_ICON_URI_PREFIX}${trimmed}`,
53
+ width: 24,
54
+ height: 24,
55
+ scale: 1
56
+ };
57
+ }
58
+ if (typeof icon === "object" && !Array.isArray(icon) && "uri" in icon && typeof icon.uri === "string" && icon.uri.startsWith(ONEKEY_ICON_URI_PREFIX)) {
59
+ return {
60
+ uri: icon.uri,
61
+ width: typeof icon.width === "number" && icon.width > 0 ? icon.width : 24,
62
+ height: typeof icon.height === "number" && icon.height > 0 ? icon.height : 24,
63
+ scale: typeof icon.scale === "number" && icon.scale > 0 ? icon.scale : 1
64
+ };
65
+ }
66
+ const resolved = Image.resolveAssetSource(icon);
67
+ return {
68
+ uri: resolved?.uri ?? "",
69
+ width: resolved?.width ?? 0,
70
+ height: resolved?.height ?? 0,
71
+ scale: resolved?.scale ?? 1
72
+ };
73
+ };
16
74
  const ANDROID_MAX_TABS = 100;
17
75
  const BottomTabs = ({
18
76
  navigationState,
@@ -169,45 +227,11 @@ const BottomTabs = ({
169
227
  })
170
228
  };
171
229
  }), [trimmedRoutes, icons, getLabelText, getBadge, getBadgeBackgroundColor, getBadgeTextColor, getActiveTintColor, getHidden, getTestID, getRole, getPreventsDefault]);
172
- const resolvedIconAssets = React.useMemo(() => icons.map(icon => {
173
- if (icon && !isAppleSymbol(icon)) {
174
- // @ts-ignore - resolveAssetSource accepts ImageSourcePropType
175
- const resolved = Image.resolveAssetSource(icon);
176
- return {
177
- uri: resolved?.uri ?? "",
178
- width: resolved?.width ?? 0,
179
- height: resolved?.height ?? 0,
180
- scale: resolved?.scale ?? 1
181
- };
182
- }
183
- return {
184
- uri: "",
185
- width: 0,
186
- height: 0,
187
- scale: 1
188
- };
189
- }), [icons]);
230
+ const resolvedIconAssets = React.useMemo(() => icons.map(icon => resolveTabIconSource(icon)), [icons]);
190
231
 
191
232
  // Focused (solid) assets passed to iOS as selectedImage. iOS only — Android
192
233
  // ignores selectedIcons and keeps swapping via the focusedKey array above.
193
- const resolvedSelectedIconAssets = React.useMemo(() => focusedIcons.map(icon => {
194
- if (icon && !isAppleSymbol(icon)) {
195
- // @ts-ignore - resolveAssetSource accepts ImageSourcePropType
196
- const resolved = Image.resolveAssetSource(icon);
197
- return {
198
- uri: resolved?.uri ?? "",
199
- width: resolved?.width ?? 0,
200
- height: resolved?.height ?? 0,
201
- scale: resolved?.scale ?? 1
202
- };
203
- }
204
- return {
205
- uri: "",
206
- width: 0,
207
- height: 0,
208
- scale: 1
209
- };
210
- }), [focusedIcons]);
234
+ const resolvedSelectedIconAssets = React.useMemo(() => focusedIcons.map(icon => resolveTabIconSource(icon)), [focusedIcons]);
211
235
  const jumpTo = useLatestCallback(key => {
212
236
  const index = trimmedRoutes.findIndex(route => route.key === key);
213
237
  if (index === -1) {
@@ -258,15 +282,9 @@ const BottomTabs = ({
258
282
  if (!isIOS || !sideAccessoryIcon) {
259
283
  return undefined;
260
284
  }
261
- // @ts-ignore - resolveAssetSource accepts ImageSourcePropType
262
- const resolved = Image.resolveAssetSource(sideAccessoryIcon);
263
- return resolved?.uri ? {
264
- uri: resolved.uri,
265
- width: resolved.width ?? 0,
266
- height: resolved.height ?? 0,
267
- scale: resolved.scale ?? 1
268
- } : undefined;
269
- }, [sideAccessoryIcon]);
285
+ const resolved = resolveTabIconSource(sideAccessoryIcon);
286
+ return resolved.uri ? resolved : undefined;
287
+ }, [isIOS, sideAccessoryIcon]);
270
288
  useLayoutEffect(() => {
271
289
  if (renderCustomTabBar && customTabBarWrapperRef.current) {
272
290
  customTabBarWrapperRef.current.measure((_x, _y, _width, height) => {
@@ -1,8 +1,14 @@
1
1
  import React from "react";
2
2
  import { type ColorValue, type ImageSourcePropType, type StyleProp, type ViewStyle } from "react-native";
3
- import type { ImageSource } from "react-native/Libraries/Image/ImageSource";
4
- import type { AppleIcon, BaseRoute, NavigationState, TabBarPlacement, TabRole } from "./types";
3
+ import type { BaseRoute, BottomTabIconSource, NamedIcon, NavigationState, TabBarPlacement, TabRole } from "./types";
5
4
  import { type RNBottomTabsAccessoryViewProps } from "./BottomAccessoryView";
5
+ type ResolvedTabIconSource = {
6
+ uri: string;
7
+ width: number;
8
+ height: number;
9
+ scale: number;
10
+ };
11
+ export declare const resolveTabIconSource: (icon: BottomTabIconSource | undefined | null) => ResolvedTabIconSource;
6
12
  interface Props<Route extends BaseRoute> {
7
13
  labeled?: boolean;
8
14
  /** Optional physical-left menu; all motion and input are owned by native. */
@@ -120,10 +126,10 @@ interface Props<Route extends BaseRoute> {
120
126
  /**
121
127
  * Get icon for the tab.
122
128
  */
123
- getIcon?: (props: {
129
+ getIcon?: ({ route, focused, }: {
124
130
  route: Route;
125
131
  focused: boolean;
126
- }) => ImageSource | AppleIcon | undefined | null;
132
+ }) => BottomTabIconSource | undefined | null;
127
133
  /**
128
134
  * Get hidden for the tab.
129
135
  */
@@ -220,7 +226,7 @@ interface Props<Route extends BaseRoute> {
220
226
  * Icon of a round button shown above a vertical tab bar. The button only
221
227
  * appears while the tab bar is vertical and visible. iOS only.
222
228
  */
223
- sideAccessoryIcon?: ImageSourcePropType;
229
+ sideAccessoryIcon?: ImageSourcePropType | NamedIcon | string;
224
230
  /**
225
231
  * Accessibility label of the side accessory button.
226
232
  */
@@ -12,5 +12,5 @@ export { BottomTabBarHeightContext } from './utils/BottomTabBarHeightContext';
12
12
  /**
13
13
  * Types
14
14
  */
15
- export type { AppleIcon, TabBarPlacement, TabRole } from './types';
15
+ export type { AppleIcon, BottomTabIconSource, NamedIcon, TabBarPlacement, TabRole, } from './types';
16
16
  //# sourceMappingURL=index.d.ts.map
@@ -4,6 +4,10 @@ export type IconSource = string | ImageSourcePropType;
4
4
  export type AppleIcon = {
5
5
  sfSymbol: SFSymbol;
6
6
  };
7
+ export type NamedIcon = {
8
+ iconName: string;
9
+ };
10
+ export type BottomTabIconSource = ImageSourcePropType | AppleIcon | NamedIcon | string;
7
11
  export type TabRole = 'search';
8
12
  export type TabBarPlacement = 'bottom' | 'leading' | 'trailing';
9
13
  export type BaseRoute = {
@@ -13,8 +17,8 @@ export type BaseRoute = {
13
17
  badgeBackgroundColor?: string;
14
18
  badgeTextColor?: string;
15
19
  lazy?: boolean;
16
- focusedIcon?: ImageSourcePropType | AppleIcon;
17
- unfocusedIcon?: ImageSourcePropType | AppleIcon;
20
+ focusedIcon?: BottomTabIconSource;
21
+ unfocusedIcon?: BottomTabIconSource;
18
22
  activeTintColor?: string;
19
23
  hidden?: boolean;
20
24
  testID?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-bottom-tabs",
3
- "version": "6.7.0-alpha.409",
3
+ "version": "6.7.0-alpha.412",
4
4
  "description": "Native bottom tabs (tab bar + scene container) for React Native on iOS and Android",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/module/index.js",
@@ -71,6 +71,7 @@
71
71
  "typescript": "^5.9.2"
72
72
  },
73
73
  "peerDependencies": {
74
+ "@onekeyfe/react-native-image": "6.7.0-alpha.412",
74
75
  "react": "*",
75
76
  "react-native": "*"
76
77
  },
@@ -32,5 +32,7 @@ Pod::Spec.new do |s|
32
32
  'DEFINES_MODULE' => 'YES'
33
33
  }
34
34
 
35
+ s.dependency "OneKeyImage", package["peerDependencies"]["@onekeyfe/react-native-image"]
36
+
35
37
  install_modules_dependencies(s)
36
38
  end
@@ -13,9 +13,6 @@ import {
13
13
  processColor,
14
14
  } from "react-native";
15
15
  import { BottomTabBarHeightContext } from "./utils/BottomTabBarHeightContext";
16
-
17
- // eslint-disable-next-line @react-native/no-deep-imports
18
- import type { ImageSource } from "react-native/Libraries/Image/ImageSource";
19
16
  import NativeBottomTabs, {
20
17
  type BottomTabsItems,
21
18
  } from "./BottomTabsNativeComponent";
@@ -23,6 +20,8 @@ import useLatestCallback from "use-latest-callback";
23
20
  import type {
24
21
  AppleIcon,
25
22
  BaseRoute,
23
+ BottomTabIconSource,
24
+ NamedIcon,
26
25
  NavigationState,
27
26
  TabBarPlacement,
28
27
  TabRole,
@@ -34,8 +33,81 @@ import {
34
33
  type RNBottomTabsAccessoryViewProps,
35
34
  } from "./BottomAccessoryView";
36
35
 
37
- const isAppleSymbol = (icon: any): icon is { sfSymbol: string } =>
38
- icon?.sfSymbol;
36
+ const isAppleSymbol = (
37
+ icon: BottomTabIconSource | undefined | null
38
+ ): icon is AppleIcon =>
39
+ Boolean(
40
+ typeof icon === "object" &&
41
+ icon !== null &&
42
+ "sfSymbol" in icon &&
43
+ typeof icon.sfSymbol === "string"
44
+ );
45
+
46
+ const ONEKEY_ICON_URI_PREFIX = "onekey-icon://";
47
+
48
+ type ResolvedTabIconSource = {
49
+ uri: string;
50
+ width: number;
51
+ height: number;
52
+ scale: number;
53
+ };
54
+
55
+ export const resolveTabIconSource = (
56
+ icon: BottomTabIconSource | undefined | null
57
+ ): ResolvedTabIconSource => {
58
+ if (!icon || isAppleSymbol(icon)) {
59
+ return { uri: "", width: 0, height: 0, scale: 1 };
60
+ }
61
+ if (typeof icon === "string") {
62
+ const trimmed = icon.trim();
63
+ if (!trimmed) {
64
+ return { uri: "", width: 0, height: 0, scale: 1 };
65
+ }
66
+ const uri = trimmed.startsWith(ONEKEY_ICON_URI_PREFIX)
67
+ ? trimmed
68
+ : `${ONEKEY_ICON_URI_PREFIX}${trimmed}`;
69
+ return { uri, width: 24, height: 24, scale: 1 };
70
+ }
71
+ if (
72
+ typeof icon === "object" &&
73
+ !Array.isArray(icon) &&
74
+ "iconName" in icon &&
75
+ typeof icon.iconName === "string"
76
+ ) {
77
+ const trimmed = icon.iconName.trim();
78
+ if (!trimmed) {
79
+ return { uri: "", width: 0, height: 0, scale: 1 };
80
+ }
81
+ return {
82
+ uri: `${ONEKEY_ICON_URI_PREFIX}${trimmed}`,
83
+ width: 24,
84
+ height: 24,
85
+ scale: 1,
86
+ };
87
+ }
88
+ if (
89
+ typeof icon === "object" &&
90
+ !Array.isArray(icon) &&
91
+ "uri" in icon &&
92
+ typeof icon.uri === "string" &&
93
+ icon.uri.startsWith(ONEKEY_ICON_URI_PREFIX)
94
+ ) {
95
+ return {
96
+ uri: icon.uri,
97
+ width: typeof icon.width === "number" && icon.width > 0 ? icon.width : 24,
98
+ height:
99
+ typeof icon.height === "number" && icon.height > 0 ? icon.height : 24,
100
+ scale: typeof icon.scale === "number" && icon.scale > 0 ? icon.scale : 1,
101
+ };
102
+ }
103
+ const resolved = Image.resolveAssetSource(icon as ImageSourcePropType);
104
+ return {
105
+ uri: resolved?.uri ?? "",
106
+ width: resolved?.width ?? 0,
107
+ height: resolved?.height ?? 0,
108
+ scale: resolved?.scale ?? 1,
109
+ };
110
+ };
39
111
 
40
112
  interface Props<Route extends BaseRoute> {
41
113
  /*
@@ -142,10 +214,13 @@ interface Props<Route extends BaseRoute> {
142
214
  /**
143
215
  * Get icon for the tab.
144
216
  */
145
- getIcon?: (props: {
217
+ getIcon?: ({
218
+ route,
219
+ focused,
220
+ }: {
146
221
  route: Route;
147
222
  focused: boolean;
148
- }) => ImageSource | AppleIcon | undefined | null;
223
+ }) => BottomTabIconSource | undefined | null;
149
224
 
150
225
  /**
151
226
  * Get hidden for the tab.
@@ -240,7 +315,7 @@ interface Props<Route extends BaseRoute> {
240
315
  * Icon of a round button shown above a vertical tab bar. The button only
241
316
  * appears while the tab bar is vertical and visible. iOS only.
242
317
  */
243
- sideAccessoryIcon?: ImageSourcePropType;
318
+ sideAccessoryIcon?: ImageSourcePropType | NamedIcon | string;
244
319
  /**
245
320
  * Accessibility label of the side accessory button.
246
321
  */
@@ -415,40 +490,14 @@ const BottomTabs = <Route extends BaseRoute>({
415
490
  );
416
491
 
417
492
  const resolvedIconAssets = React.useMemo(
418
- () =>
419
- icons.map((icon) => {
420
- if (icon && !isAppleSymbol(icon)) {
421
- // @ts-ignore - resolveAssetSource accepts ImageSourcePropType
422
- const resolved = Image.resolveAssetSource(icon);
423
- return {
424
- uri: resolved?.uri ?? "",
425
- width: resolved?.width ?? 0,
426
- height: resolved?.height ?? 0,
427
- scale: resolved?.scale ?? 1,
428
- };
429
- }
430
- return { uri: "", width: 0, height: 0, scale: 1 };
431
- }),
493
+ () => icons.map((icon) => resolveTabIconSource(icon)),
432
494
  [icons]
433
495
  );
434
496
 
435
497
  // Focused (solid) assets passed to iOS as selectedImage. iOS only — Android
436
498
  // ignores selectedIcons and keeps swapping via the focusedKey array above.
437
499
  const resolvedSelectedIconAssets = React.useMemo(
438
- () =>
439
- focusedIcons.map((icon) => {
440
- if (icon && !isAppleSymbol(icon)) {
441
- // @ts-ignore - resolveAssetSource accepts ImageSourcePropType
442
- const resolved = Image.resolveAssetSource(icon);
443
- return {
444
- uri: resolved?.uri ?? "",
445
- width: resolved?.width ?? 0,
446
- height: resolved?.height ?? 0,
447
- scale: resolved?.scale ?? 1,
448
- };
449
- }
450
- return { uri: "", width: 0, height: 0, scale: 1 };
451
- }),
500
+ () => focusedIcons.map((icon) => resolveTabIconSource(icon)),
452
501
  [focusedIcons]
453
502
  );
454
503
 
@@ -514,17 +563,9 @@ const BottomTabs = <Route extends BaseRoute>({
514
563
  if (!isIOS || !sideAccessoryIcon) {
515
564
  return undefined;
516
565
  }
517
- // @ts-ignore - resolveAssetSource accepts ImageSourcePropType
518
- const resolved = Image.resolveAssetSource(sideAccessoryIcon);
519
- return resolved?.uri
520
- ? {
521
- uri: resolved.uri,
522
- width: resolved.width ?? 0,
523
- height: resolved.height ?? 0,
524
- scale: resolved.scale ?? 1,
525
- }
526
- : undefined;
527
- }, [sideAccessoryIcon]);
566
+ const resolved = resolveTabIconSource(sideAccessoryIcon);
567
+ return resolved.uri ? resolved : undefined;
568
+ }, [isIOS, sideAccessoryIcon]);
528
569
 
529
570
  useLayoutEffect(() => {
530
571
  if (renderCustomTabBar && customTabBarWrapperRef.current) {
package/src/index.tsx CHANGED
@@ -15,4 +15,10 @@ export { BottomTabBarHeightContext } from './utils/BottomTabBarHeightContext';
15
15
  /**
16
16
  * Types
17
17
  */
18
- export type { AppleIcon, TabBarPlacement, TabRole } from './types';
18
+ export type {
19
+ AppleIcon,
20
+ BottomTabIconSource,
21
+ NamedIcon,
22
+ TabBarPlacement,
23
+ TabRole,
24
+ } from './types';
package/src/types.ts CHANGED
@@ -5,6 +5,14 @@ export type IconSource = string | ImageSourcePropType;
5
5
 
6
6
  export type AppleIcon = { sfSymbol: SFSymbol };
7
7
 
8
+ export type NamedIcon = { iconName: string };
9
+
10
+ export type BottomTabIconSource =
11
+ | ImageSourcePropType
12
+ | AppleIcon
13
+ | NamedIcon
14
+ | string;
15
+
8
16
  export type TabRole = 'search';
9
17
 
10
18
  // 'leading' and 'trailing' mean a vertical tab bar on that edge.
@@ -17,8 +25,8 @@ export type BaseRoute = {
17
25
  badgeBackgroundColor?: string;
18
26
  badgeTextColor?: string;
19
27
  lazy?: boolean;
20
- focusedIcon?: ImageSourcePropType | AppleIcon;
21
- unfocusedIcon?: ImageSourcePropType | AppleIcon;
28
+ focusedIcon?: BottomTabIconSource;
29
+ unfocusedIcon?: BottomTabIconSource;
22
30
  activeTintColor?: string;
23
31
  hidden?: boolean;
24
32
  testID?: string;
package/ios/SVG/CoreSVG.h DELETED
@@ -1,13 +0,0 @@
1
- #import <UIKit/UIKit.h>
2
- typedef UIImage PlatformImage;
3
-
4
- @interface CoreSVGWrapper : NSObject
5
-
6
- + (instancetype)shared;
7
-
8
- - (PlatformImage *)imageFromSVGData:(NSData *)data;
9
-
10
- + (BOOL)isSVGData:(NSData *)data;
11
- + (BOOL)supportsVectorSVGImage;
12
-
13
- @end
@@ -1,177 +0,0 @@
1
- #import "CoreSVG.h"
2
- #import <dlfcn.h>
3
- #import <objc/runtime.h>
4
-
5
- #define kSVGTagEnd @"</svg>"
6
-
7
- typedef struct CF_BRIDGED_TYPE(id) CGSVGDocument *CGSVGDocumentRef;
8
-
9
- static CGSVGDocumentRef (*CoreSVGDocumentRetain)(CGSVGDocumentRef);
10
- static void (*CoreSVGDocumentRelease)(CGSVGDocumentRef);
11
- static CGSVGDocumentRef (*CoreSVGDocumentCreateFromData)(CFDataRef data, CFDictionaryRef options);
12
- static void (*CoreSVGContextDrawSVGDocument)(CGContextRef context, CGSVGDocumentRef document);
13
- static CGSize (*CoreSVGDocumentGetCanvasSize)(CGSVGDocumentRef document);
14
-
15
- #if !TARGET_OS_OSX
16
- static SEL CoreSVGImageWithDocumentSEL = NULL;
17
- static SEL CoreSVGDocumentSEL = NULL;
18
- #endif
19
- #if TARGET_OS_OSX
20
- static Class CoreSVGImageRepClass = NULL;
21
- static Ivar CoreSVGImageRepDocumentIvar = NULL;
22
- #endif
23
-
24
- static inline NSString *Base64DecodedString(NSString *base64String) {
25
- NSData *data = [[NSData alloc] initWithBase64EncodedString:base64String options:NSDataBase64DecodingIgnoreUnknownCharacters];
26
- if (!data) {
27
- return nil;
28
- }
29
- return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
30
- }
31
-
32
- @implementation CoreSVGWrapper
33
-
34
- + (instancetype)shared {
35
- static dispatch_once_t onceToken;
36
- static CoreSVGWrapper *wrapper;
37
- dispatch_once(&onceToken, ^{
38
- wrapper = [[CoreSVGWrapper alloc] init];
39
- });
40
- return wrapper;
41
- }
42
-
43
- + (void)initialize {
44
- CoreSVGDocumentRetain = (CGSVGDocumentRef (*)(CGSVGDocumentRef))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dTVkdEb2N1bWVudFJldGFpbg==").UTF8String);
45
- CoreSVGDocumentRelease = (void (*)(CGSVGDocumentRef))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dTVkdEb2N1bWVudFJlbGVhc2U=").UTF8String);
46
- CoreSVGDocumentCreateFromData = (CGSVGDocumentRef (*)(CFDataRef data, CFDictionaryRef options))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dTVkdEb2N1bWVudENyZWF0ZUZyb21EYXRh").UTF8String);
47
- CoreSVGContextDrawSVGDocument = (void (*)(CGContextRef context, CGSVGDocumentRef document))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dDb250ZXh0RHJhd1NWR0RvY3VtZW50").UTF8String);
48
- CoreSVGDocumentGetCanvasSize = (CGSize (*)(CGSVGDocumentRef document))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dTVkdEb2N1bWVudEdldENhbnZhc1NpemU=").UTF8String);
49
-
50
- #if !TARGET_OS_OSX
51
- CoreSVGImageWithDocumentSEL = NSSelectorFromString(Base64DecodedString(@"X2ltYWdlV2l0aENHU1ZHRG9jdW1lbnQ6"));
52
- CoreSVGDocumentSEL = NSSelectorFromString(Base64DecodedString(@"X0NHU1ZHRG9jdW1lbnQ="));
53
- #endif
54
- #if TARGET_OS_OSX
55
- CoreSVGImageRepClass = NSClassFromString(Base64DecodedString(@"X05TU1ZHSW1hZ2VSZXA="));
56
- if (CoreSVGImageRepClass) {
57
- CoreSVGImageRepDocumentIvar = class_getInstanceVariable(CoreSVGImageRepClass, Base64DecodedString(@"X2RvY3VtZW50").UTF8String);
58
- }
59
- #endif
60
- }
61
-
62
- - (PlatformImage *)imageFromSVGData:(NSData *)data {
63
- if (!data) {
64
- return nil;
65
- }
66
-
67
- if (![self.class supportsVectorSVGImage]) {
68
- return nil;
69
- }
70
-
71
- return [self createVectorSVGWithData:data];
72
- }
73
-
74
- - (PlatformImage *)createVectorSVGWithData:(NSData *)data {
75
- if (!data) return nil;
76
-
77
- PlatformImage *image;
78
-
79
- #if TARGET_OS_OSX
80
- if (!CoreSVGImageRepClass) {
81
- return nil;
82
- }
83
- Class imageRepClass = CoreSVGImageRepClass;
84
- NSImageRep *imageRep = [[imageRepClass alloc] initWithData:data];
85
- if (!imageRep) {
86
- return nil;
87
- }
88
- image = [[NSImage alloc] initWithSize:imageRep.size];
89
- [image addRepresentation:imageRep];
90
- #else
91
- if (!CoreSVGDocumentCreateFromData || !CoreSVGDocumentRelease) {
92
- return nil;
93
- }
94
- CGSVGDocumentRef document = CoreSVGDocumentCreateFromData((__bridge CFDataRef)data, NULL);
95
-
96
- if (!document) {
97
- return nil;
98
- }
99
-
100
- image = ((UIImage *(*)(id,SEL,CGSVGDocumentRef))[UIImage.class methodForSelector:CoreSVGImageWithDocumentSEL])(UIImage.class, CoreSVGImageWithDocumentSEL, document);
101
- CoreSVGDocumentRelease(document);
102
- #endif
103
-
104
- #if TARGET_OS_OSX
105
- // Test render to catch potential CoreSVG crashes on macOS
106
- NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc]
107
- initWithBitmapDataPlanes:NULL
108
- pixelsWide:1
109
- pixelsHigh:1
110
- bitsPerSample:8
111
- samplesPerPixel:4
112
- hasAlpha:YES
113
- isPlanar:NO
114
- colorSpaceName:NSCalibratedRGBColorSpace
115
- bytesPerRow:0
116
- bitsPerPixel:0];
117
-
118
- NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithBitmapImageRep:bitmap];
119
- [NSGraphicsContext saveGraphicsState];
120
- [NSGraphicsContext setCurrentContext:context];
121
-
122
- @try {
123
- [image drawInRect:NSMakeRect(0, 0, 1, 1)];
124
- } @catch (...) {
125
- [NSGraphicsContext restoreGraphicsState];
126
- return nil;
127
- }
128
-
129
- [NSGraphicsContext restoreGraphicsState];
130
- #else
131
- // Test render to catch potential CoreSVG crashes
132
- UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 1.0);
133
- @try {
134
- [image drawInRect:CGRectMake(0, 0, 1, 1)];
135
- } @catch (...) {
136
- UIGraphicsEndImageContext();
137
- return nil;
138
- }
139
- UIGraphicsEndImageContext();
140
- #endif
141
-
142
-
143
- return image;
144
- }
145
-
146
- + (BOOL)isSVGData:(NSData *)data {
147
- if (!data) {
148
- return NO;
149
- }
150
- // Check end with SVG tag
151
- return [data rangeOfData:[kSVGTagEnd dataUsingEncoding:NSUTF8StringEncoding] options:NSDataSearchBackwards range: NSMakeRange(data.length - MIN(100, data.length), MIN(100, data.length))].location != NSNotFound;
152
- }
153
-
154
- + (BOOL)supportsVectorSVGImage {
155
- static dispatch_once_t onceToken;
156
- static BOOL supports;
157
- dispatch_once(&onceToken, ^{
158
- #if TARGET_OS_OSX
159
- // macOS 10.15+ supports SVG built-in rendering, use selector to check is more accurate
160
- if (CoreSVGImageRepClass) {
161
- supports = YES;
162
- } else {
163
- supports = NO;
164
- }
165
- #else
166
- // iOS 13+ supports SVG built-in rendering, use selector to check is more accurate
167
- if ([UIImage respondsToSelector:CoreSVGImageWithDocumentSEL]) {
168
- supports = YES;
169
- } else {
170
- supports = NO;
171
- }
172
- #endif
173
- });
174
- return supports;
175
- }
176
-
177
- @end