diff --git a/android/build.gradle b/android/build.gradle index 0fc108f8..17081ae5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,6 +25,7 @@ def isNewArchitectureEnabled() { apply plugin: "com.android.library" apply plugin: "kotlin-android" +apply plugin: "kotlin-kapt" if (isNewArchitectureEnabled()) { apply plugin: "com.facebook.react" @@ -110,12 +111,18 @@ repositories { def kotlin_version = getExtOrDefault("kotlinVersion") dependencies { + def GLIDE_VERSION = "4.16.0" // For < 0.71, this will be from the local maven repo // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'com.google.android.material:material:1.13.0-alpha06' + + api "com.github.bumptech.glide:glide:${GLIDE_VERSION}" + kapt "com.github.bumptech.glide:compiler:${GLIDE_VERSION}" + + api 'com.caverock:androidsvg-aar:1.4' } if (isNewArchitectureEnabled()) { diff --git a/android/src/main/java/com/rcttabview/RCTTabView.kt b/android/src/main/java/com/rcttabview/RCTTabView.kt index 753f8ff2..1b062071 100644 --- a/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -1,5 +1,6 @@ package com.rcttabview +import android.annotation.SuppressLint import android.content.Context import android.content.res.ColorStateList import android.graphics.Typeface @@ -7,6 +8,7 @@ import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.os.Build +import android.util.Log import android.util.TypedValue import android.view.Choreographer import android.view.HapticFeedbackConstants @@ -15,11 +17,10 @@ import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources -import com.facebook.common.references.CloseableReference -import com.facebook.datasource.DataSources -import com.facebook.drawee.backends.pipeline.Fresco -import com.facebook.imagepipeline.image.CloseableBitmap -import com.facebook.imagepipeline.request.ImageRequestBuilder +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.WritableMap @@ -69,6 +70,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context override fun requestLayout() { super.requestLayout() @Suppress("SENSELESS_COMPARISON") // layoutCallback can be null here since this method can be called in init + if (!isLayoutEnqueued && layoutCallback != null) { isLayoutEnqueued = true // we use NATIVE_ANIMATED_MODULE choreographer queue because it allows us to catch the current @@ -102,7 +104,9 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context val menuItem = getOrCreateItem(index, item.title) menuItem.isVisible = !item.hidden if (icons.containsKey(index)) { - menuItem.icon = getDrawable(icons[index]!!) + getDrawable(icons[index]!!) { + menuItem.icon = it + } } if (item.badge.isNotEmpty()) { @@ -150,7 +154,9 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context // Update existing item if exists. menu.findItem(idx)?.let { menuItem -> - menuItem.icon = getDrawable(imageSource) + getDrawable(imageSource) { + menuItem.icon = it + } } } } @@ -169,22 +175,35 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context itemRippleColor = color } - private fun getDrawable(imageSource: ImageSource): Drawable? { - try { - val imageRequest = ImageRequestBuilder.newBuilderWithSource(imageSource.uri).build() - val dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context) - val result = DataSources.waitForFinalResult(dataSource) as CloseableReference - val bitmap = result.get().underlyingBitmap - - CloseableReference.closeSafely(result) - dataSource.close() - - return BitmapDrawable(resources, bitmap) - } catch (_: Exception) { - // Asset doesn't exist - } + @SuppressLint("CheckResult") + private fun getDrawable(imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) { + GlideApp.with(context) + .`as`(Drawable::class.java) + .load(imageSource.uri) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { + Log.e("RCTTabView", "Error loading image: ${imageSource.uri}", e) + return false + } - return null + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + // Update images on the main queue. + post { onDrawableReady(resource) } + return true + } + }) + .submit() } override fun onDetachedFromWindow() { diff --git a/android/src/main/java/com/rcttabview/TabViewAppGlideModule.kt b/android/src/main/java/com/rcttabview/TabViewAppGlideModule.kt new file mode 100644 index 00000000..f6c55408 --- /dev/null +++ b/android/src/main/java/com/rcttabview/TabViewAppGlideModule.kt @@ -0,0 +1,18 @@ +package com.rcttabview + +import android.content.Context +import android.util.Log +import com.bumptech.glide.GlideBuilder +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.module.AppGlideModule + +@GlideModule +class TabViewAppGlideModule : AppGlideModule() { + override fun applyOptions(context: Context, builder: GlideBuilder) { + super.applyOptions(context, builder) + + builder.setLogLevel( + Log.ERROR + ) + } +} diff --git a/android/src/main/java/com/rcttabview/svg/SVGDecoder.kt b/android/src/main/java/com/rcttabview/svg/SVGDecoder.kt new file mode 100644 index 00000000..8f098a05 --- /dev/null +++ b/android/src/main/java/com/rcttabview/svg/SVGDecoder.kt @@ -0,0 +1,40 @@ +package com.rcttabview.svg + +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.ResourceDecoder +import com.bumptech.glide.load.engine.Resource +import com.bumptech.glide.load.resource.SimpleResource +import com.caverock.androidsvg.SVG +import com.caverock.androidsvg.SVGParseException +import java.io.IOException +import java.io.InputStream + + +class SVGDecoder : ResourceDecoder { + override fun handles(source: InputStream, options: Options) = true + + companion object { + const val DEFAULT_SIZE = 40f + } + + @Throws(IOException::class) + override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource? { + return try { + val svg: SVG = SVG.getFromInputStream(source) + // Taken from https://github.com/expo/expo/blob/215d8a13a7ef3f0b36b14eead41291e2d2d6cd0c/packages/expo-image/android/src/main/java/expo/modules/image/svg/SVGDecoder.kt#L28 + if (svg.documentViewBox == null) { + val documentWidth = svg.documentWidth + val documentHeight = svg.documentHeight + if (documentWidth != -1f && documentHeight != -1f) { + svg.setDocumentViewBox(0f, 0f, documentWidth, documentHeight) + } + } + + svg.documentWidth = DEFAULT_SIZE + svg.documentHeight = DEFAULT_SIZE + SimpleResource(svg) + } catch (ex: SVGParseException) { + throw IOException("Cannot load SVG from stream", ex) + } + } +} diff --git a/android/src/main/java/com/rcttabview/svg/SVGDrawableTranscoder.kt b/android/src/main/java/com/rcttabview/svg/SVGDrawableTranscoder.kt new file mode 100644 index 00000000..91ad2db4 --- /dev/null +++ b/android/src/main/java/com/rcttabview/svg/SVGDrawableTranscoder.kt @@ -0,0 +1,32 @@ +package com.rcttabview.svg + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.PictureDrawable +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.engine.Resource +import com.bumptech.glide.load.resource.SimpleResource +import com.bumptech.glide.load.resource.transcode.ResourceTranscoder +import com.caverock.androidsvg.SVG + +class SVGDrawableTranscoder(val context: Context) : ResourceTranscoder { + override fun transcode(toTranscode: Resource, options: Options): Resource { + val svg = toTranscode.get() + val picture = svg.renderToPicture() + val drawable = PictureDrawable(picture) + + val returnedBitmap = Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + + val canvas = Canvas(returnedBitmap) + canvas.drawPicture(drawable.picture) + val bitMapDrawable = BitmapDrawable(context.resources, returnedBitmap) + return SimpleResource(bitMapDrawable) + } +} diff --git a/android/src/main/java/com/rcttabview/svg/TabViewGlideModule.kt b/android/src/main/java/com/rcttabview/svg/TabViewGlideModule.kt new file mode 100644 index 00000000..db6579d3 --- /dev/null +++ b/android/src/main/java/com/rcttabview/svg/TabViewGlideModule.kt @@ -0,0 +1,25 @@ +package com.rcttabview.svg + +import android.content.Context +import android.graphics.drawable.Drawable +import com.bumptech.glide.Glide +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.module.LibraryGlideModule +import com.caverock.androidsvg.SVG +import java.io.InputStream + + +@GlideModule +class SvgModule: LibraryGlideModule() { + override fun registerComponents( + context: Context, glide: Glide, registry: Registry + ) { + registry + .register( + SVG::class.java, + Drawable::class.java, SVGDrawableTranscoder(context) + ) + .append(InputStream::class.java, SVG::class.java, SVGDecoder()) + } +} diff --git a/docs/docs/docs/getting-started/quick-start.mdx b/docs/docs/docs/getting-started/quick-start.mdx index bab492c0..50ef1344 100644 --- a/docs/docs/docs/getting-started/quick-start.mdx +++ b/docs/docs/docs/getting-started/quick-start.mdx @@ -41,7 +41,8 @@ import { PackageManagerTabs } from '@theme'; ### Expo -Add the library plugin in your `app.json` config file and [create a new build](https://docs.expo.dev/develop/development-builds/create-a-build/): +Add the library plugin in your `app.json` config file and [create a new build](https://docs.expo.dev/develop/development-builds/create-a-build/). + ```diff "expo": { @@ -50,6 +51,27 @@ Add the library plugin in your `app.json` config file and [create a new build](h } ``` +You also need to enable static linking for iOS by adding `"useFrameworks": "static"` in the `expo-build-properties` plugin. + +```diff +{ + "expo": { + "plugins": [ + "react-native-bottom-tabs", ++ [ ++ "expo-build-properties", ++ { ++ "ios": { ++ "useFrameworks": "static" ++ } ++ } ++ ] ++ ] + } +} +``` + + :::warning This library is not supported in [Expo Go](https://expo.dev/go). @@ -72,6 +94,12 @@ Edit `android/app/src/main/res/values/styles.xml` to inherit from provided theme Here you can read more about [Android Native Styling](/docs/guides/android-native-styling). +To enable static linking for iOS, Open the `./ios/Podfile` file and add the following: + +```ruby +use_frameworks!, :linkage => :static +``` + ## Example usage diff --git a/docs/docs/docs/guides/usage-with-react-navigation.mdx b/docs/docs/docs/guides/usage-with-react-navigation.mdx index 6067d409..1601a4da 100644 --- a/docs/docs/docs/guides/usage-with-react-navigation.mdx +++ b/docs/docs/docs/guides/usage-with-react-navigation.mdx @@ -189,8 +189,12 @@ Function that given `{ focused: boolean }` returns `ImageSource` or `AppleIcon` component={Albums} options={{ tabBarIcon: () => require('person.png'), + // SVG is also supported + tabBarIcon: () => require('person.svg'), // or tabBarIcon: () => ({ sfSymbol: 'person' }), + // You can also pass a URL + tabBarIcon: () => ({ uri: 'https://example.com/icon.png' }), }} /> ``` diff --git a/example/assets/icons/book-image.svg b/example/assets/icons/book-image.svg new file mode 100644 index 00000000..6aebd82f --- /dev/null +++ b/example/assets/icons/book-image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/icons/message-circle-code.svg b/example/assets/icons/message-circle-code.svg new file mode 100644 index 00000000..c0901807 --- /dev/null +++ b/example/assets/icons/message-circle-code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/icons/newspaper.svg b/example/assets/icons/newspaper.svg new file mode 100644 index 00000000..11e787fe --- /dev/null +++ b/example/assets/icons/newspaper.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/icons/user-round-search.svg b/example/assets/icons/user-round-search.svg new file mode 100644 index 00000000..5e005d85 --- /dev/null +++ b/example/assets/icons/user-round-search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/icons/user-round.svg b/example/assets/icons/user-round.svg new file mode 100644 index 00000000..4ea27d97 --- /dev/null +++ b/example/assets/icons/user-round.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/ios/Podfile b/example/ios/Podfile index 7a7dc9cc..6dfbd4f5 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -6,4 +6,9 @@ require "#{ws_dir}/node_modules/react-native-test-app/test_app.rb" workspace 'ReactNativeBottomTabsExample.xcworkspace' -use_test_app! +use_test_app! do |test_app| + # Workaround for not using use_frameworks! in the Podfile + pod 'SDWebImage', :modular_headers => true + pod 'SDWebImageSVGCoder', :modular_headers => true +end + diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index d1cbacfd..ede40c18 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1209,7 +1209,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-bottom-tabs (0.0.12): + - react-native-bottom-tabs (0.4.0): - DoubleConversion - glog - RCT-Folly (= 2024.01.01.00) @@ -1222,6 +1222,7 @@ PODS: - React-graphics - React-ImageManager - React-jsi + - react-native-bottom-tabs/common (= 0.4.0) - React-NativeModulesApple - React-RCTFabric - React-rendererdebug @@ -1229,6 +1230,32 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - SDWebImage (~> 5.11.1) + - SDWebImageSVGCoder (~> 1.7.0) + - SwiftUIIntrospect (~> 1.0) + - Yoga + - react-native-bottom-tabs/common (0.4.0): + - DoubleConversion + - glog + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SDWebImage (~> 5.11.1) + - SDWebImageSVGCoder (~> 1.7.0) - SwiftUIIntrospect (~> 1.0) - Yoga - react-native-safe-area-context (4.11.0): @@ -1563,7 +1590,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - ReactTestApp-DevSupport (3.10.10): + - ReactTestApp-DevSupport (3.10.21): - React-Core - React-jsi - ReactTestApp-Resources (1.0.0-dev) @@ -1588,7 +1615,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNScreens (3.34.0): + - RNScreens (3.35.0): - DoubleConversion - glog - RCT-Folly (= 2024.01.01.00) @@ -1609,9 +1636,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNScreens/common (= 3.34.0) + - RNScreens/common (= 3.35.0) - Yoga - - RNScreens/common (3.34.0): + - RNScreens/common (3.35.0): - DoubleConversion - glog - RCT-Folly (= 2024.01.01.00) @@ -1633,6 +1660,32 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - RNVectorIcons (10.2.0): + - DoubleConversion + - glog + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - SDWebImage (5.11.1): + - SDWebImage/Core (= 5.11.1) + - SDWebImage/Core (5.11.1) + - SDWebImageSVGCoder (1.7.0): + - SDWebImage/Core (~> 5.6) - SocketRocket (0.7.0) - SwiftUIIntrospect (1.3.0) - Yoga (0.0.0) @@ -1707,10 +1760,15 @@ DEPENDENCIES: - ReactTestApp-Resources (from `..`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNScreens (from `../node_modules/react-native-screens`) + - RNVectorIcons (from `../node_modules/react-native-vector-icons`) + - SDWebImage + - SDWebImageSVGCoder - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: trunk: + - SDWebImage + - SDWebImageSVGCoder - SocketRocket - SwiftUIIntrospect @@ -1847,6 +1905,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-gesture-handler" RNScreens: :path: "../node_modules/react-native-screens" + RNVectorIcons: + :path: "../node_modules/react-native-vector-icons" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" @@ -1866,15 +1926,15 @@ SPEC CHECKSUMS: React-CoreModules: 2d68c251bc4080028f2835fa47504e8f20669a21 React-cxxreact: bb0dc212b515d6dba6c6ddc4034584e148857db9 React-debug: fd0ed8ecd5f8a23c7daf5ceaca8aa722a4d083fd - React-defaultsnativemodule: 371dc516e5020f8b87f1d32f8fa6872cafcc2081 - React-domnativemodule: 5d1288b9b8666b818a1004b56a03befc00eb5698 + React-defaultsnativemodule: 8dd41726048ad16c135e1585797de81480c161fc + React-domnativemodule: a2f6c53b4edd50da888e240ba92b038a7264e713 React-Fabric: c12ce848f72cba42fb9e97a73a7c99abc6353f23 React-FabricComponents: 7813d5575c8ea2cda0fef9be4ff9d10987cba512 React-FabricImage: c511a5d612479cb4606edf3557c071956c8735f6 React-featureflags: cf78861db9318ae29982fa8953c92d31b276c9ac - React-featureflagsnativemodule: e774cf495486b0e2a8b324568051d6b4c722fa93 + React-featureflagsnativemodule: 7ee9bb16c9b3039c78eea088bc99819827981e12 React-graphics: 7572851bca7242416b648c45d6af87d93d29281e - React-idlecallbacksnativemodule: d2009bad67ef232a0ee586f53193f37823e81ef1 + React-idlecallbacksnativemodule: 2369a5e611553b9d43ec56577ad76d8d6b8e2474 React-ImageManager: aedf54d34d4475c66f4c3da6b8359b95bee904e4 React-jsc: 92ac98e0e03ee54fdaa4ac3936285a4fdb166fab React-jserrorhandler: 0c8949672a00f2a502c767350e591e3ec3d82fb3 @@ -1884,9 +1944,9 @@ SPEC CHECKSUMS: React-jsitracing: 3935b092f85bb1e53b8cf8a00f572413648af46b React-logger: 4072f39df335ca443932e0ccece41fbeb5ca8404 React-Mapbuffer: 714f2fae68edcabfc332b754e9fbaa8cfc68fdd4 - React-microtasksnativemodule: 987cf7e0e0e7129250a48b807e70d3b906c726cf - react-native-bottom-tabs: 30906150a76d9735a58080792f363dc9ccee9c04 - react-native-safe-area-context: 851c62c48dce80ccaa5637b6aa5991a1bc36eca9 + React-microtasksnativemodule: 2eb1a69d35e700f752944644c0295cf7161d06c5 + react-native-bottom-tabs: 930f7bdb6e9122a519f7a50a83908e6fe04090e6 + react-native-safe-area-context: c6e59b0ac0acb3ddc3247235215775441ca1b2ff React-nativeconfig: 4a9543185905fe41014c06776bf126083795aed9 React-NativeModulesApple: 651670a799672bd54469f2981d91493dda361ddf React-perflogger: 3bbb82f18e9ac29a1a6931568e99d6305ef4403b @@ -1912,15 +1972,18 @@ SPEC CHECKSUMS: React-utils: b2baee839fb869f732d617b97dcfa384b4b4fdb3 ReactCodegen: f177b8fd67788c5c6ff45a39c7482c5f8d77ace6 ReactCommon: 627bd3192ef01a351e804e9709673d3741d38fec - ReactNativeHost: 99c0ffb175cd69de2ac9a70892cd22dac65ea79d - ReactTestApp-DevSupport: b7cd76a3aeee6167f5e14d82f09685059152c426 - ReactTestApp-Resources: 7db90c026cccdf40cfa495705ad436ccc4d64154 - RNGestureHandler: 18b9b5d65c77c4744a640f69b7fccdd47ed935c0 - RNScreens: 5288a8dbeedb3c5051aa2d5658c1c553c050b80a + ReactNativeHost: 8f602474a76f43f2cd7823fa00575b4beb107b21 + ReactTestApp-DevSupport: 74ff23aba1f35caa74d1dd5346c2835e0af31770 + ReactTestApp-Resources: f9d4fd5651f8e68f6362f7e5374c7aca3b381c94 + RNGestureHandler: 6a34af1ea5d9321af615933c271b0c37a00ff473 + RNScreens: d4551ceaec50b2fd6648e36d2e47dd42ef9ccfef + RNVectorIcons: a1344e212e80e6e0f4537a9960148201175f4225 + SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d + SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d Yoga: 4ef80d96a5534f0e01b3055f17d1e19a9fc61b63 -PODFILE CHECKSUM: 539add55dc6c2e7f9754e288b1ce4fd8583819ae +PODFILE CHECKSUM: ec8970ff5a624fb462d3b2cd6e966645a41c4daa -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/example/package.json b/example/package.json index 53ee13fd..206078bd 100644 --- a/example/package.json +++ b/example/package.json @@ -38,7 +38,7 @@ "@rnx-kit/metro-config": "^2.0.0", "@types/react-native-vector-icons": "^6.4.18", "react-native-builder-bob": "^0.30.2", - "react-native-test-app": "^3.10.10" + "react-native-test-app": "^3.10.21" }, "engines": { "node": ">=18" diff --git a/example/src/App.tsx b/example/src/App.tsx index 21db84d2..d516e4e0 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -32,6 +32,8 @@ import NativeBottomTabs from './Examples/NativeBottomTabs'; import TintColorsExample from './Examples/TintColors'; import NativeBottomTabsVectorIcons from './Examples/NativeBottomTabsVectorIcons'; import NativeBottomTabsEmbeddedStacks from './Examples/NativeBottomTabsEmbeddedStacks'; +import NativeBottomTabsSVGs from './Examples/NativeBottomTabsSVGs'; +import NativeBottomTabsRemoteIcons from './Examples/NativeBottomTabsRemoteIcons'; const FourTabsIgnoreSafeArea = () => { return ; @@ -126,6 +128,14 @@ const examples = [ component: NativeBottomTabsVectorIcons, name: 'Native Bottom Tabs with Vector Icons', }, + { + component: NativeBottomTabsSVGs, + name: 'Native Bottom Tabs with SVG Icons', + }, + { + component: NativeBottomTabsRemoteIcons, + name: 'Native Bottom Tabs with SVG Remote Icons', + }, { component: NativeBottomTabs, name: 'Native Bottom Tabs' }, { component: JSBottomTabs, name: 'JS Bottom Tabs' }, { diff --git a/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx b/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx index 4230ea43..3a9f771a 100644 --- a/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx +++ b/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx @@ -94,7 +94,8 @@ function NativeBottomTabsEmbeddedStacks() { name="Chat" component={ChatStackScreen} options={{ - tabBarIcon: () => require('../../assets/icons/chat_dark.png'), + tabBarIcon: () => + require('../../assets/icons/message-circle-code.svg'), }} /> diff --git a/example/src/Examples/NativeBottomTabsRemoteIcons.tsx b/example/src/Examples/NativeBottomTabsRemoteIcons.tsx new file mode 100644 index 00000000..7f3dbe6e --- /dev/null +++ b/example/src/Examples/NativeBottomTabsRemoteIcons.tsx @@ -0,0 +1,54 @@ +import { Article } from '../Screens/Article'; +import { Albums } from '../Screens/Albums'; +import { Contacts } from '../Screens/Contacts'; +import { Chat } from '../Screens/Chat'; +// This import works properly when library is published +import createNativeBottomTabNavigator from '../../../src/react-navigation/navigators/createNativeBottomTabNavigator'; + +const Tab = createNativeBottomTabNavigator(); + +function NativeBottomTabsRemoteIcons() { + return ( + + ({ + uri: 'https://www.svgrepo.com/show/533824/water-container.svg', + }), + }} + /> + ({ + uri: 'https://www.svgrepo.com/show/533813/hat-chef.svg', + }), + }} + /> + ({ + uri: 'https://www.svgrepo.com/show/533826/shop.svg', + }), + }} + /> + ({ + uri: 'https://www.svgrepo.com/show/533828/cheese.svg', + }), + }} + /> + + ); +} + +export default NativeBottomTabsRemoteIcons; diff --git a/example/src/Examples/NativeBottomTabsSVGs.tsx b/example/src/Examples/NativeBottomTabsSVGs.tsx new file mode 100644 index 00000000..cc627b36 --- /dev/null +++ b/example/src/Examples/NativeBottomTabsSVGs.tsx @@ -0,0 +1,50 @@ +import { Article } from '../Screens/Article'; +import { Albums } from '../Screens/Albums'; +import { Contacts } from '../Screens/Contacts'; +import { Chat } from '../Screens/Chat'; +// This import works properly when library is published +import createNativeBottomTabNavigator from '../../../src/react-navigation/navigators/createNativeBottomTabNavigator'; + +const Tab = createNativeBottomTabNavigator(); + +function NativeBottomTabsSVGs() { + return ( + + require('../../assets/icons/newspaper.svg'), + }} + /> + require('../../assets/icons/book-image.svg'), + }} + /> + + focused + ? require('../../assets/icons/user-round-search.svg') + : require('../../assets/icons/user-round.svg'), + }} + /> + + require('../../assets/icons/message-circle-code.svg'), + }} + /> + + ); +} + +export default NativeBottomTabsSVGs; diff --git a/example/src/Examples/TintColors.tsx b/example/src/Examples/TintColors.tsx index bab9b8bd..e1514983 100644 --- a/example/src/Examples/TintColors.tsx +++ b/example/src/Examples/TintColors.tsx @@ -30,7 +30,9 @@ export default function TintColorsExample() { }, { key: 'chat', - focusedIcon: require('../../assets/icons/chat_dark.png'), + focusedIcon: { + uri: 'https://upload.wikimedia.org/wikipedia/commons/f/fa/Apple_logo_black.svg', + }, title: 'Chat', }, ]); diff --git a/ios/TabViewProvider.swift b/ios/TabViewProvider.swift index e2b2a100..11ea934a 100644 --- a/ios/TabViewProvider.swift +++ b/ios/TabViewProvider.swift @@ -1,6 +1,8 @@ import Foundation import SwiftUI import React +import SDWebImage +import SDWebImageSVGCoder @objc public final class TabInfo: NSObject { @objc public let key: String @@ -161,6 +163,7 @@ import React self.init() self.delegate = delegate self.imageLoader = imageLoader + SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared) } public override func didUpdateReactSubviews() { @@ -194,27 +197,43 @@ import React private func loadIcons(_ icons: NSArray?) { // TODO: Diff the arrays and update only changed items. // Now if the user passes `unfocusedIcon` we update every item. - if let imageSources = icons as? [RCTImageSource?] { - for (index, imageSource) in imageSources.enumerated() { - guard let imageSource, let imageLoader else { continue } - imageLoader.loadImage( - with: imageSource.request, - size: imageSource.size, - scale: imageSource.scale, - clipped: false, - resizeMode: RCTResizeMode.cover, - progressBlock: { _,_ in }, - partialLoad: { _ in }, - completionBlock: { error, image in - if error != nil { - print("[TabView] Error loading image: \(error!.localizedDescription)") - return - } - guard let image else { return } - DispatchQueue.main.async { + guard let imageSources = icons as? [RCTImageSource?] else { return } + + for (index, imageSource) in imageSources.enumerated() { + guard let imageSource = imageSource, + let url = imageSource.request.url else { continue } + + let isSVG = url.pathExtension.lowercased() == "svg" + + var options: SDWebImageOptions = [.continueInBackground, + .scaleDownLargeImages, + .avoidDecodeImage, + .highPriority] + + if isSVG { + options.insert(.decodeFirstFrameOnly) + } + + let context: [SDWebImageContextOption: Any]? = isSVG ? [ + .imageThumbnailPixelSize: iconSize + ] : nil + + SDWebImageManager.shared.loadImage( + with: url, + options: options, + context: context, + progress: nil + ) { [weak self] (image, _, _, _, _, _) in + guard let self = self else { return } + DispatchQueue.main.async { + if let image { + if isSVG { + self.props.icons[index] = image + } else { self.props.icons[index] = image.resizeImageTo(size: self.iconSize) } - }) + } + } } } } diff --git a/react-native-bottom-tabs.podspec b/react-native-bottom-tabs.podspec index ea18b756..94cde62b 100644 --- a/react-native-bottom-tabs.podspec +++ b/react-native-bottom-tabs.podspec @@ -16,6 +16,7 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/okwasniewski/react-native-bottom-tabs.git", :tag => "#{s.version}" } s.source_files = "ios/**/*.{h,m,mm,cpp,swift}" + s.static_framework = true if new_arch_enabled s.subspec "common" do |ss| @@ -24,12 +25,14 @@ Pod::Spec.new do |s| end end + s.dependency "SwiftUIIntrospect", '~> 1.0' + s.dependency 'SDWebImage', '~> 5.11.1' + s.dependency 'SDWebImageSVGCoder', '~> 1.7.0' + s.pod_target_xcconfig = { - "DEFINES_MODULE" => "YES" + 'DEFINES_MODULE' => 'YES' } - s.dependency "SwiftUIIntrospect", '~> 1.0' - # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. if respond_to?(:install_modules_dependencies, true) diff --git a/yarn.lock b/yarn.lock index c7f2c92d..821bced5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13757,7 +13757,7 @@ __metadata: react-native-paper: ^5.12.5 react-native-safe-area-context: ^4.11.0 react-native-screens: ^3.35.0 - react-native-test-app: ^3.10.10 + react-native-test-app: ^3.10.21 react-native-vector-icons: ^10.2.0 languageName: unknown linkType: soft @@ -13887,9 +13887,9 @@ __metadata: languageName: node linkType: hard -"react-native-test-app@npm:^3.10.10": - version: 3.10.10 - resolution: "react-native-test-app@npm:3.10.10" +"react-native-test-app@npm:^3.10.21": + version: 3.10.21 + resolution: "react-native-test-app@npm:3.10.21" dependencies: "@rnx-kit/react-native-host": ^0.5.0 ajv: ^8.0.0 @@ -13899,12 +13899,12 @@ __metadata: semver: ^7.3.5 uuid: ^10.0.0 peerDependencies: - "@callstack/react-native-visionos": 0.73 - 0.75 + "@callstack/react-native-visionos": 0.73 - 0.76 "@expo/config-plugins": ">=5.0" react: 17.0.1 - 19.0 - react-native: 0.66 - 0.75 || >=0.76.0-0 <0.76.0 - react-native-macos: ^0.0.0-0 || 0.66 || 0.68 || 0.71 - 0.75 - react-native-windows: ^0.0.0-0 || 0.66 - 0.75 + react-native: 0.66 - 0.76 || >=0.77.0-0 <0.77.0 + react-native-macos: ^0.0.0-0 || 0.66 || 0.68 || 0.71 - 0.76 + react-native-windows: ^0.0.0-0 || 0.66 - 0.76 peerDependenciesMeta: "@callstack/react-native-visionos": optional: true @@ -13919,7 +13919,7 @@ __metadata: init: scripts/init.mjs init-test-app: scripts/init.mjs install-windows-test-app: windows/test-app.mjs - checksum: eab41098fb0358766802198fa5bed16d8344c4d8b1e2a325857ec1ee3b1d6617287c8c5620e35c9e815c40eaee29fc9582d73ad21b03256b568755f5209cbaaa + checksum: ce86f478ddc6f1f150b7790166a4743c812f248405dbe957adcf969889176da6e33010e45213d2833a36d78623330eab83cebd7e56a7a79afb9f537d780341db languageName: node linkType: hard