├── src ├── index.ts ├── SweetSFSymbolsView.ts └── SweetSFSymbolsView.ios.tsx ├── example ├── assets │ ├── icon.png │ ├── splash.png │ ├── favicon.png │ └── adaptive-icon.png ├── ios │ ├── Podfile.properties.json │ ├── sweetsfsymbolsexample │ │ ├── Images.xcassets │ │ │ ├── Contents.json │ │ │ ├── SplashScreen.imageset │ │ │ │ ├── image.png │ │ │ │ └── Contents.json │ │ │ ├── SplashScreenBackground.imageset │ │ │ │ ├── image.png │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── App-Icon-1024x1024@1x.png │ │ │ │ └── Contents.json │ │ ├── noop-file.swift │ │ ├── sweetsfsymbolsexample-Bridging-Header.h │ │ ├── AppDelegate.h │ │ ├── main.m │ │ ├── sweetsfsymbolsexample.entitlements │ │ ├── Supporting │ │ │ └── Expo.plist │ │ ├── Info.plist │ │ ├── AppDelegate.mm │ │ └── SplashScreen.storyboard │ ├── sweetsfsymbolsexample.xcworkspace │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── contents.xcworkspacedata │ ├── .gitignore │ ├── .xcode.env │ ├── Podfile │ ├── sweetsfsymbolsexample.xcodeproj │ │ ├── xcshareddata │ │ │ └── xcschemes │ │ │ │ └── sweetsfsymbolsexample.xcscheme │ │ └── project.pbxproj │ └── Podfile.lock ├── tsconfig.json ├── .gitignore ├── webpack.config.js ├── babel.config.js ├── package.json ├── app.json ├── metro.config.js └── App.tsx ├── expo-module.config.json ├── .eslintrc.js ├── .npmignore ├── tsconfig.json ├── .gitignore ├── ios ├── SweetSFSymbols.podspec ├── SweetSFSymbolsExpoView.swift ├── SweetSFSymbolsView.swift ├── SweetSFSymbolsModule.swift ├── SweetSFSymbolsProps.swift └── SweetSFSymbolsModifiers.swift ├── package.json └── README.md /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./SweetSFSymbolsView"; 2 | -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-levy/sweet-sfsymbols/HEAD/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-levy/sweet-sfsymbols/HEAD/example/assets/splash.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-levy/sweet-sfsymbols/HEAD/example/assets/favicon.png -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-levy/sweet-sfsymbols/HEAD/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /example/ios/Podfile.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo.jsEngine": "hermes", 3 | "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true" 4 | } 5 | -------------------------------------------------------------------------------- /expo-module.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "platforms": ["ios"], 3 | "ios": { 4 | "modules": ["SweetSFSymbolsModule"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['universe/native', 'universe/web'], 4 | ignorePatterns: ['build'], 5 | }; 6 | -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "expo" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample/noop-file.swift: -------------------------------------------------------------------------------- 1 | // 2 | // @generated 3 | // A blank Swift file must be created for native modules with Swift files to work correctly. 4 | // 5 | -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample/sweetsfsymbolsexample-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | @interface AppDelegate : EXAppDelegateWrapper 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample/Images.xcassets/SplashScreen.imageset/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-levy/sweet-sfsymbols/HEAD/example/ios/sweetsfsymbolsexample/Images.xcassets/SplashScreen.imageset/image.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Exclude all top-level hidden directories by convention 2 | /.*/ 3 | 4 | __mocks__ 5 | __tests__ 6 | 7 | /babel.config.js 8 | /android/src/androidTest/ 9 | /android/src/test/ 10 | /android/build/ 11 | /example/ 12 | -------------------------------------------------------------------------------- /src/SweetSFSymbolsView.ts: -------------------------------------------------------------------------------- 1 | // Sweet-SFSymbols is not supported on any platform other than iOS 2 | 3 | import { SweetSFSymbolsViewProps } from "./SweetSFSymbols.types"; 4 | 5 | export default (props: SweetSFSymbolsViewProps) => null; 6 | 7 | -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample/Images.xcassets/SplashScreenBackground.imageset/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-levy/sweet-sfsymbols/HEAD/example/ios/sweetsfsymbolsexample/Images.xcassets/SplashScreenBackground.imageset/image.png -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-levy/sweet-sfsymbols/HEAD/example/ios/sweetsfsymbolsexample/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "sweet-sfsymbols": ["../src/index"], 7 | "sweet-sfsymbols/*": ["../src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | { 3 | "extends": "expo-module-scripts/tsconfig.base", 4 | "compilerOptions": { 5 | "outDir": "./build" 6 | }, 7 | "include": ["./src"], 8 | "exclude": ["**/__mocks__/*", "**/__tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample/sweetsfsymbolsexample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "App-Icon-1024x1024@1x.png", 5 | "idiom": "universal", 6 | "platform": "ios", 7 | "size": "1024x1024" 8 | } 9 | ], 10 | "info": { 11 | "version": 1, 12 | "author": "expo" 13 | } 14 | } -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample/Images.xcassets/SplashScreen.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "image.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "scale": "2x" 11 | }, 12 | { 13 | "idiom": "universal", 14 | "scale": "3x" 15 | } 16 | ], 17 | "info": { 18 | "version": 1, 19 | "author": "expo" 20 | } 21 | } -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample/Images.xcassets/SplashScreenBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "image.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "scale": "2x" 11 | }, 12 | { 13 | "idiom": "universal", 14 | "scale": "3x" 15 | } 16 | ], 17 | "info": { 18 | "version": 1, 19 | "author": "expo" 20 | } 21 | } -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | .xcode.env.local 25 | 26 | # Bundle artifacts 27 | *.jsbundle 28 | 29 | # CocoaPods 30 | /Pods/ 31 | -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample/Supporting/Expo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EXUpdatesCheckOnLaunch 6 | ALWAYS 7 | EXUpdatesEnabled 8 | 9 | EXUpdatesLaunchWaitMs 10 | 0 11 | EXUpdatesSDKVersion 12 | 50.0.0 13 | 14 | -------------------------------------------------------------------------------- /example/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const createConfigAsync = require('@expo/webpack-config'); 2 | const path = require('path'); 3 | 4 | module.exports = async (env, argv) => { 5 | const config = await createConfigAsync( 6 | { 7 | ...env, 8 | babel: { 9 | dangerouslyAddModulePathsToTranspile: ['sweet-sfsymbols'], 10 | }, 11 | }, 12 | argv 13 | ); 14 | config.resolve.modules = [ 15 | path.resolve(__dirname, './node_modules'), 16 | path.resolve(__dirname, '../node_modules'), 17 | ]; 18 | 19 | return config; 20 | }; 21 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = function (api) { 3 | api.cache(true); 4 | return { 5 | presets: ['babel-preset-expo'], 6 | plugins: [ 7 | [ 8 | 'module-resolver', 9 | { 10 | extensions: ['.tsx', '.ts', '.js', '.json'], 11 | alias: { 12 | // For development, we want to alias the library to the source 13 | 'sweet-sfsymbols': path.join(__dirname, '..', 'src', 'index.ts'), 14 | }, 15 | }, 16 | ], 17 | ], 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sweet-sfsymbols-example", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo run:android", 8 | "ios": "expo run:ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "expo": "^50.0.14", 13 | "expo-splash-screen": "~0.26.4", 14 | "expo-status-bar": "~1.11.1", 15 | "react": "18.2.0", 16 | "react-native": "0.73.4" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.20.0", 20 | "@types/react": "~18.2.45", 21 | "typescript": "^5.3.0" 22 | }, 23 | "private": true, 24 | "expo": { 25 | "autolinking": { 26 | "nativeModulesDir": ".." 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # VSCode 6 | .vscode/ 7 | jsconfig.json 8 | 9 | # Xcode 10 | # 11 | build/ 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata 21 | *.xccheckout 22 | *.moved-aside 23 | DerivedData 24 | *.hmap 25 | *.ipa 26 | *.xcuserstate 27 | project.xcworkspace 28 | 29 | # Android/IJ 30 | # 31 | .classpath 32 | .cxx 33 | .gradle 34 | .idea 35 | .project 36 | .settings 37 | local.properties 38 | android.iml 39 | android/app/libs 40 | android/keystores/debug.keystore 41 | 42 | # Cocoapods 43 | # 44 | example/ios/Pods 45 | 46 | # Ruby 47 | example/vendor/ 48 | 49 | # node.js 50 | # 51 | node_modules/ 52 | npm-debug.log 53 | yarn-debug.log 54 | yarn-error.log 55 | 56 | # Expo 57 | .expo/* 58 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "sweet-sfsymbols-example", 4 | "slug": "sweet-sfsymbols-example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "assetBundlePatterns": [ 15 | "**/*" 16 | ], 17 | "ios": { 18 | "supportsTablet": true, 19 | "bundleIdentifier": "expo.modules.sweetsfsymbols.example" 20 | }, 21 | "android": { 22 | "adaptiveIcon": { 23 | "foregroundImage": "./assets/adaptive-icon.png", 24 | "backgroundColor": "#ffffff" 25 | }, 26 | "package": "expo.modules.sweetsfsymbols.example" 27 | }, 28 | "web": { 29 | "favicon": "./assets/favicon.png" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ios/SweetSFSymbols.podspec: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json'))) 4 | 5 | Pod::Spec.new do |s| 6 | s.name = 'SweetSFSymbols' 7 | s.version = package['version'] 8 | s.summary = package['description'] 9 | s.description = package['description'] 10 | s.license = package['license'] 11 | s.author = package['author'] 12 | s.homepage = package['homepage'] 13 | s.platform = :ios, '13.0' 14 | s.swift_version = '5.4' 15 | s.source = { git: 'https://github.com/andrew-levy/sweet-sfsymbols' } 16 | s.static_framework = true 17 | 18 | s.dependency 'ExpoModulesCore' 19 | 20 | # Swift/Objective-C compatibility 21 | s.pod_target_xcconfig = { 22 | 'DEFINES_MODULE' => 'YES', 23 | 'SWIFT_COMPILATION_MODE' => 'wholemodule' 24 | } 25 | 26 | s.source_files = "**/*.{h,m,swift}" 27 | end 28 | -------------------------------------------------------------------------------- /ios/SweetSFSymbolsExpoView.swift: -------------------------------------------------------------------------------- 1 | import ExpoModulesCore 2 | import SwiftUI 3 | 4 | class SweetSFSymbolsView: ExpoView { 5 | let props = Props() 6 | 7 | required init(appContext: AppContext? = nil) { 8 | let hostingController = UIHostingController(rootView: SweetSFSymbolSwiftUIView(props: props)) 9 | 10 | super.init(appContext: appContext) 11 | 12 | hostingController.view.translatesAutoresizingMaskIntoConstraints = false 13 | hostingController.view.backgroundColor = .clear 14 | 15 | addSubview(hostingController.view) 16 | NSLayoutConstraint.activate([ 17 | hostingController.view.topAnchor.constraint(equalTo: self.topAnchor), 18 | hostingController.view.bottomAnchor.constraint(equalTo: self.bottomAnchor), 19 | hostingController.view.leftAnchor.constraint(equalTo: self.leftAnchor), 20 | hostingController.view.rightAnchor.constraint(equalTo: self.rightAnchor) 21 | ]) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more https://docs.expo.io/guides/customizing-metro 2 | const { getDefaultConfig } = require('expo/metro-config'); 3 | const path = require('path'); 4 | 5 | const config = getDefaultConfig(__dirname); 6 | 7 | // npm v7+ will install ../node_modules/react-native because of peerDependencies. 8 | // To prevent the incompatible react-native bewtween ./node_modules/react-native and ../node_modules/react-native, 9 | // excludes the one from the parent folder when bundling. 10 | config.resolver.blockList = [ 11 | ...Array.from(config.resolver.blockList ?? []), 12 | new RegExp(path.resolve('..', 'node_modules', 'react-native')), 13 | ]; 14 | 15 | config.resolver.nodeModulesPaths = [ 16 | path.resolve(__dirname, './node_modules'), 17 | path.resolve(__dirname, '../node_modules'), 18 | ]; 19 | 20 | config.watchFolders = [path.resolve(__dirname, '..')]; 21 | 22 | config.transformer.getTransformOptions = async () => ({ 23 | transform: { 24 | experimentalImportSupport: false, 25 | inlineRequires: true, 26 | }, 27 | }); 28 | 29 | module.exports = config; -------------------------------------------------------------------------------- /ios/SweetSFSymbolsView.swift: -------------------------------------------------------------------------------- 1 | import ExpoModulesCore 2 | import SwiftUI 3 | 4 | class Props: ObservableObject { 5 | @Published var name: String = "" 6 | @Published var weight: SFSymbolWeight = .regular 7 | @Published var scale: SFSymbolScale = .medium 8 | @Published var size: Double = 0 9 | @Published var colors: [UIColor] = [] 10 | @Published var renderingMode: SFSymbolRenderingMode = .monochrome 11 | @Published var variableValue: Double = 0 12 | @Published var variant: String? = nil 13 | @Published var symbolEffect: SFSymbolEffect? = nil 14 | } 15 | 16 | struct SweetSFSymbolSwiftUIView: View { 17 | @ObservedObject var props: Props 18 | 19 | var body: some View { 20 | let image: Image 21 | if #available(iOS 16.0, *) { 22 | image = Image(systemName: props.name, variableValue: props.variableValue) 23 | } else { 24 | image = Image(systemName: props.name) 25 | } 26 | return image 27 | .imageScale(props.scale.toImageScale()) 28 | .font(.system(size: props.size, weight: props.weight.toFontWeight())) 29 | .customColors(props.colors) 30 | .customRenderingMode(props.renderingMode) 31 | .customVariant(props.variant) 32 | .customSymbolEffect(props.symbolEffect) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sweet-sfsymbols", 3 | "version": "0.7.2", 4 | "description": "SF Symbols brought to you by Expo's Module API", 5 | "main": "build/index.js", 6 | "types": "build/index.d.ts", 7 | "scripts": { 8 | "build": "expo-module build", 9 | "clean": "expo-module clean", 10 | "lint": "expo-module lint", 11 | "test": "expo-module test", 12 | "prepare": "expo-module prepare", 13 | "prepublishOnly": "expo-module prepublishOnly", 14 | "expo-module": "expo-module", 15 | "open:ios": "open -a \"Xcode\" example/ios", 16 | "open:android": "open -a \"Android Studio\" example/android" 17 | }, 18 | "keywords": [ 19 | "react-native", 20 | "expo", 21 | "sweet-sfsymbols", 22 | "SweetSFSymbols" 23 | ], 24 | "repository": "https://github.com/andrew-levy/sweet-sfsymbols", 25 | "bugs": { 26 | "url": "https://github.com/andrew-levy/sweet-sfsymbols/issues" 27 | }, 28 | "author": "Andrew (https://github.com/andrew-levy)", 29 | "license": "MIT", 30 | "homepage": "https://github.com/andrew-levy/sweet-sfsymbols#readme", 31 | "dependencies": {}, 32 | "devDependencies": { 33 | "@types/react": "^18.0.25", 34 | "expo-module-scripts": "^3.0.11", 35 | "expo-modules-core": "^1.5.11" 36 | }, 37 | "peerDependencies": { 38 | "expo": "*", 39 | "react": "*", 40 | "react-native": "*" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ios/SweetSFSymbolsModule.swift: -------------------------------------------------------------------------------- 1 | import ExpoModulesCore 2 | import SwiftUI 3 | 4 | public class SweetSFSymbolsModule: Module { 5 | public func definition() -> ModuleDefinition { 6 | Name("SweetSFSymbols") 7 | View(SweetSFSymbolsView.self) { 8 | Prop("name") { (view: SweetSFSymbolsView, name: String?) in 9 | view.props.name = name ?? "" 10 | } 11 | Prop("weight") { (view:SweetSFSymbolsView, weight: SFSymbolWeight?) in 12 | view.props.weight = weight ?? .medium 13 | } 14 | Prop("scale") { (view: SweetSFSymbolsView, scale: SFSymbolScale?) in 15 | view.props.scale = scale ?? .medium 16 | } 17 | Prop("size") { (view: SweetSFSymbolsView, size: Double?) in 18 | view.props.size = size ?? 50.0 19 | } 20 | Prop("colors") { (view: SweetSFSymbolsView, colors: [UIColor]?) in 21 | view.props.colors = colors ?? [UIColor.black] 22 | } 23 | Prop("renderingMode") { (view: SweetSFSymbolsView, renderingMode: SFSymbolRenderingMode?) in 24 | view.props.renderingMode = renderingMode ?? .monochrome 25 | } 26 | Prop("variableValue") { (view: SweetSFSymbolsView, variableValue: Double?) in 27 | view.props.variableValue = variableValue ?? 1.0 28 | } 29 | Prop("variant") { (view: SweetSFSymbolsView, variant: String?) in 30 | view.props.variant = variant ?? nil 31 | } 32 | Prop("symbolEffect") { (view: SweetSFSymbolsView, symbolEffect: SFSymbolEffect?) in 33 | view.props.symbolEffect = symbolEffect ?? nil 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/SweetSFSymbolsView.ios.tsx: -------------------------------------------------------------------------------- 1 | import { requireNativeViewManager } from "expo-modules-core"; 2 | import * as React from "react"; 3 | 4 | import { ProcessedColorValue, processColor } from "react-native"; 5 | import { 6 | NativeSymbolEffect, 7 | SweetSFSymbolsViewProps, 8 | } from "./SweetSFSymbols.types"; 9 | 10 | const NativeView: React.ComponentType< 11 | Omit & { 12 | symbolEffect?: NativeSymbolEffect; 13 | colors: (string | ProcessedColorValue | null | undefined | object)[]; 14 | } 15 | > = requireNativeViewManager("SweetSFSymbols"); 16 | 17 | export default class SweetSFSymbol extends React.PureComponent { 18 | render() { 19 | const { 20 | style, 21 | size = 50, 22 | symbolEffect, 23 | colors, 24 | variableValue = 1.0, 25 | ...restProps 26 | } = this.props; 27 | const repeatValue = 28 | symbolEffect && "repeat" in symbolEffect ? symbolEffect?.repeat : 1; 29 | const repeatCount = repeatValue === true ? -1 : 1; 30 | const effect = { 31 | ...symbolEffect, 32 | repeatCount, 33 | } as NativeSymbolEffect; 34 | 35 | return ( 36 | 48 | ); 49 | } 50 | } 51 | 52 | function isHslOrRgbColor(color: string) { 53 | if (typeof color !== "string") return false; 54 | return color.startsWith("hsl") || color.startsWith("rgb"); 55 | } 56 | 57 | function getColors(colors?: SweetSFSymbolsViewProps["colors"]) { 58 | return ( 59 | colors?.map((color) => { 60 | if (typeof color === "string" && isHslOrRgbColor(color)) { 61 | return processColor(color); 62 | } 63 | return color; 64 | }) ?? [] 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | sweet-sfsymbols-example 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | 1.0.0 23 | CFBundleSignature 24 | ???? 25 | CFBundleURLTypes 26 | 27 | 28 | CFBundleURLSchemes 29 | 30 | expo.modules.sweetsfsymbols.example 31 | 32 | 33 | 34 | CFBundleVersion 35 | 1 36 | LSRequiresIPhoneOS 37 | 38 | NSAppTransportSecurity 39 | 40 | NSAllowsArbitraryLoads 41 | 42 | NSAllowsLocalNetworking 43 | 44 | 45 | UILaunchStoryboardName 46 | SplashScreen 47 | UIRequiredDeviceCapabilities 48 | 49 | armv7 50 | 51 | UIRequiresFullScreen 52 | 53 | UIStatusBarStyle 54 | UIStatusBarStyleDefault 55 | UISupportedInterfaceOrientations 56 | 57 | UIInterfaceOrientationPortrait 58 | UIInterfaceOrientationPortraitUpsideDown 59 | 60 | UISupportedInterfaceOrientations~ipad 61 | 62 | UIInterfaceOrientationPortrait 63 | UIInterfaceOrientationPortraitUpsideDown 64 | UIInterfaceOrientationLandscapeLeft 65 | UIInterfaceOrientationLandscapeRight 66 | 67 | UIUserInterfaceStyle 68 | Light 69 | UIViewControllerBasedStatusBarAppearance 70 | 71 | 72 | -------------------------------------------------------------------------------- /ios/SweetSFSymbolsProps.swift: -------------------------------------------------------------------------------- 1 | import ExpoModulesCore 2 | import SwiftUI 3 | 4 | enum SFSymbolRenderingMode: String, Enumerable { 5 | case hierarchical 6 | case palette 7 | case multicolor 8 | case monochrome 9 | 10 | @available(iOS 15.0, *) 11 | func toSFSymbolRenderingMode() -> SymbolRenderingMode { 12 | switch self { 13 | case .hierarchical: 14 | return .hierarchical 15 | case .monochrome: 16 | return .monochrome 17 | case .multicolor: 18 | return .multicolor 19 | case .palette: 20 | return .palette 21 | } 22 | } 23 | } 24 | 25 | enum SFSymbolScale: String, Enumerable { 26 | case small 27 | case medium 28 | case large 29 | 30 | func toImageScale() -> Image.Scale { 31 | switch self { 32 | case .small: 33 | return .small 34 | case .medium: 35 | return .medium 36 | case .large: 37 | return .large 38 | } 39 | } 40 | } 41 | 42 | enum SFSymbolWeight: String, Enumerable { 43 | case bold 44 | case heavy 45 | case medium 46 | case light 47 | case regular 48 | case semibold 49 | case thin 50 | case ultraLight 51 | case black 52 | 53 | func toFontWeight() -> Font.Weight { 54 | switch self { 55 | case .bold: 56 | return .bold 57 | case .heavy: 58 | return .heavy 59 | case .medium: 60 | return .medium 61 | case .light: 62 | return .light 63 | case .regular: 64 | return .regular 65 | case .semibold: 66 | return .semibold 67 | case .thin: 68 | return .thin 69 | case .ultraLight: 70 | return .ultraLight 71 | case .black: 72 | return .black 73 | } 74 | } 75 | } 76 | 77 | struct SFSymbolEffect: Record { 78 | @Field var type: String 79 | @Field var repeatCount: Int? 80 | @Field var speed: Double? 81 | @Field var reversing: Bool? 82 | @Field var direction: String? 83 | @Field var animateBy: String? 84 | @Field var inactiveLayers: String? 85 | @Field var value: Double? 86 | @Field var isActive: Bool? 87 | 88 | @available(iOS 17.0, *) 89 | func toSymbolEffectOptions() -> SymbolEffectOptions { 90 | var options: SymbolEffectOptions 91 | if repeatCount != nil { 92 | if repeatCount == -1 { 93 | options = .repeating.speed(speed ?? 1.0) 94 | } else { 95 | options = .repeat(repeatCount).speed(speed ?? 1.0) 96 | } 97 | } else { 98 | options = .nonRepeating.speed(speed ?? 1.0) 99 | } 100 | return options 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | 6 | @implementation AppDelegate 7 | 8 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 9 | { 10 | self.moduleName = @"main"; 11 | 12 | // You can add your custom initial props in the dictionary below. 13 | // They will be passed down to the ViewController used by React Native. 14 | self.initialProps = @{}; 15 | 16 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 17 | } 18 | 19 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 20 | { 21 | return [self getBundleURL]; 22 | } 23 | 24 | - (NSURL *)getBundleURL 25 | { 26 | #if DEBUG 27 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"]; 28 | #else 29 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 30 | #endif 31 | } 32 | 33 | // Linking API 34 | - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { 35 | return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; 36 | } 37 | 38 | // Universal Links 39 | - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { 40 | BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; 41 | return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; 42 | } 43 | 44 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 45 | - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken 46 | { 47 | return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; 48 | } 49 | 50 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 51 | - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error 52 | { 53 | return [super application:application didFailToRegisterForRemoteNotificationsWithError:error]; 54 | } 55 | 56 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 57 | - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler 58 | { 59 | return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; 60 | } 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") 2 | require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods") 3 | 4 | require 'json' 5 | podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} 6 | 7 | ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0' 8 | ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR'] 9 | 10 | platform :ios, podfile_properties['ios.deploymentTarget'] || '13.4' 11 | install! 'cocoapods', 12 | :deterministic_uuids => false 13 | 14 | prepare_react_native_project! 15 | 16 | # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. 17 | # because `react-native-flipper` depends on (FlipperKit,...), which will be excluded. To fix this, 18 | # you can also exclude `react-native-flipper` in `react-native.config.js` 19 | # 20 | # ```js 21 | # module.exports = { 22 | # dependencies: { 23 | # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), 24 | # } 25 | # } 26 | # ``` 27 | flipper_config = FlipperConfiguration.disabled 28 | if ENV['NO_FLIPPER'] == '1' then 29 | # Explicitly disabled through environment variables 30 | flipper_config = FlipperConfiguration.disabled 31 | elsif podfile_properties.key?('ios.flipper') then 32 | # Configure Flipper in Podfile.properties.json 33 | if podfile_properties['ios.flipper'] == 'true' then 34 | flipper_config = FlipperConfiguration.enabled(["Debug", "Release"]) 35 | elsif podfile_properties['ios.flipper'] != 'false' then 36 | flipper_config = FlipperConfiguration.enabled(["Debug", "Release"], { 'Flipper' => podfile_properties['ios.flipper'] }) 37 | end 38 | end 39 | 40 | target 'sweetsfsymbolsexample' do 41 | use_expo_modules! 42 | config = use_native_modules! 43 | 44 | use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] 45 | use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] 46 | 47 | use_react_native!( 48 | :path => config[:reactNativePath], 49 | :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes', 50 | # An absolute path to your application root. 51 | :app_path => "#{Pod::Config.instance.installation_root}/..", 52 | # Note that if you have use_frameworks! enabled, Flipper will not work if enabled 53 | :flipper_configuration => flipper_config 54 | ) 55 | 56 | post_install do |installer| 57 | react_native_post_install( 58 | installer, 59 | config[:reactNativePath], 60 | :mac_catalyst_enabled => false 61 | ) 62 | 63 | # This is necessary for Xcode 14, because it signs resource bundles by default 64 | # when building for devices. 65 | installer.target_installation_results.pod_target_installation_results 66 | .each do |pod_name, target_installation_result| 67 | target_installation_result.resource_bundle_targets.each do |resource_bundle_target| 68 | resource_bundle_target.build_configurations.each do |config| 69 | config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' 70 | end 71 | end 72 | end 73 | end 74 | 75 | post_integrate do |installer| 76 | begin 77 | expo_patch_react_imports!(installer) 78 | rescue => e 79 | Pod::UI.warn e 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample.xcodeproj/xcshareddata/xcschemes/sweetsfsymbolsexample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /example/ios/sweetsfsymbolsexample/SplashScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/hugemathguy) 2 | 3 | # Sweet SF Symbols 4 | 5 | [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/foundations/sf-symbols) are a set of over 5,000 consistent, highly configurable symbols you can use in your app. SF Symbols are designed to integrate seamlessly with the San Francisco system font, so the symbols automatically ensure optical vertical alignment with text for all weights and sizes. 6 | 7 | ## Highlights 8 | 9 | - :fire: Built with [Expo's Module API](https://docs.expo.dev/modules/module-api/) 10 | - :art: [Rendering modes](https://developer.apple.com/design/human-interface-guidelines/foundations/sf-symbols#rendering-modes) 11 | - :art: [Variable color values](https://developer.apple.com/design/human-interface-guidelines/foundations/sf-symbols#variable-color) 12 | - :no_bell: [Symbol Variants](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Design-variants) 13 | - :tada: [Symbol Effects](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Animations) 14 | - :apple: iOS only ([see why](https://developer.apple.com/design/human-interface-guidelines/foundations/sf-symbols#custom-symbols)) 15 | 16 | ## Installation 17 | 18 | Sweet SFSymbols requires Expo SDK 46+ and Xcode 15+. 19 | 20 | ### Expo 21 | 22 | Install the library: 23 | 24 | ```console 25 | npx expo install sweet-sfsymbols 26 | ``` 27 | 28 | Then rebuild your app: 29 | 30 | ```bash 31 | # Using EAS? run a build in the cloud! 32 | eas build --platform ios 33 | 34 | # Otherwise, prebuild and run a local build 35 | npx expo prebuild -p ios --clean 36 | npx expo run:ios 37 | ``` 38 | 39 | > **_NOTE:_** This library will not work with Expo Go. Use a [development build](https://docs.expo.dev/develop/development-builds/create-a-build/) instead! 40 | 41 | ## Usage 42 | 43 | See the [example app](/example). 44 | 45 | ## `` 46 | 47 | The `SFSymbol` component uses SwiftUI's `Image` view to render SF Symbols. 48 | 49 | ### `Props` 50 | 51 | #### `name` 52 | 53 | The name of the symbol. 54 | 55 | > required: yes 56 | > 57 | > type: [`SystemName`](./src/SweetSFSymbols.types.ts) 58 | > 59 | > default: `""` 60 | 61 | #### `colors` 62 | 63 | The colors of the symbol. For monochrome and hierarchical rendering modes, this is a single color. For palette rendering mode, this is an array of colors. For multicolor rendering mode, this is ignored and system default values are used. This supports hex, hsl(a), rgb(a), web standard color names, PlatformColor and DynamicColorIOS values. 64 | 65 | > required: no 66 | > 67 | > type: `(string | OpaqueColorValue)[]` 68 | > 69 | > default: `[]` 70 | 71 | #### `weight` 72 | 73 | The weight of the symbol. 74 | 75 | > required: no 76 | > 77 | > type: `"thin" | "ultraLight" | "light" | "regular" | "medium" | "semibold" | "bold" | "heavy" | "black"` 78 | > 79 | > default: `"regular"` 80 | 81 | #### `scale` 82 | 83 | The scale of the symbol. 84 | 85 | > required: no 86 | > 87 | > type: `"small" | "medium" | "large"` 88 | > 89 | > default: `"medium"` 90 | 91 | #### `renderingMode` (iOS 15+) 92 | 93 | The rendering mode of the symbol. Learn more about rendering modes [here](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Rendering-modes). 94 | 95 | > required: no 96 | > 97 | > type: `"monochrome" | "hierarchical" | "palette" | "multicolor"` 98 | > 99 | > default: `"monochrome"` 100 | 101 | #### `size` 102 | 103 | The size of the symbol. This deifines the frame of the image view. 104 | 105 | > required: no 106 | > 107 | > type: `number` 108 | > 109 | > default: `50` 110 | 111 | #### `variableValue` (iOS 16+) 112 | 113 | The variable value of the symbol. Only some symbols support variable values, ususally those that represent a change in value (like `speaker.wave.3`) The variable value determines what percentage of the symbol is filled in. Learn more about variable values [here](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Variable-color). 114 | 115 | > required: no 116 | > 117 | > type: `number` 118 | > 119 | > default: `1.0` 120 | 121 | #### `variant` (iOS 15+) 122 | 123 | The variant of the symbol. This is an alternate way to modify the symbol's appearance without modifying the symbol name. Learn more about symbol variants [here](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Design-variants). 124 | 125 | > required: no 126 | > 127 | > type: [SymbolVariant](./src/SweetSFSymbols.types.ts) 128 | > 129 | > default: `none` 130 | 131 | #### `symbolEffect` (iOS 17+) 132 | 133 | The symbol effect of the symbol. Adds an animation to the symbol. Learn more about symbol effects [here](https://blorenzop.medium.com/how-to-animate-sf-symbols-in-swiftui-c3b504af4f44). 134 | 135 | > required: no 136 | > 137 | > type: [`SymbolEffect`](./src/SweetSFSymbols.types.ts) 138 | > 139 | > default: `undefined` 140 | 141 | #### `style` 142 | 143 | The style of the symbol. 144 | 145 | > required: no 146 | > 147 | > type: `ViewStyle` 148 | > 149 | > default: `undefined` 150 | 151 | ## Disclaimer 152 | 153 | It's your responsibility to check Apple's rules about when and where certain icons can be used. You can check the [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) and use the [official SF Symbols app](https://developer.apple.com/sf-symbols/) to check for any restrictions on the icons you want to use. 154 | 155 | This library isn't associated with Apple, and only exposes a way to use them within React Native apps on iOS. 156 | 157 | ## Symbol names not up to date? 158 | 159 | If you notice that the symbol names are not up to date, either submit an issue or a PR with the updated symbol names! 160 | -------------------------------------------------------------------------------- /ios/SweetSFSymbolsModifiers.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | func customColors(_ colors: [UIColor]) -> some View { 5 | modifier(ColorsModifier(colors: colors)) 6 | } 7 | func customRenderingMode(_ renderingMode: SFSymbolRenderingMode) -> some View { 8 | modifier(RenderingModeModifier(renderingMode: renderingMode)) 9 | } 10 | func customSymbolEffect(_ symbolEffect: SFSymbolEffect?) -> some View { 11 | modifier(SymbolEffectModifier(symbolEffect: symbolEffect)) 12 | } 13 | func customVariant(_ variant: String?) -> some View { 14 | modifier(VariantModifier(variant: variant)) 15 | } 16 | } 17 | 18 | struct ColorsModifier: ViewModifier { 19 | var colors: [UIColor] 20 | func body(content: Content) -> some View { 21 | if colors.count == 1 { 22 | return AnyView(content.foregroundColor(Color(colors[0]))) 23 | } else if colors.count == 2 { 24 | if #available(iOS 15.0, *) { 25 | return AnyView(content.foregroundStyle(Color(colors[0]), Color(colors[1]))) 26 | } else { 27 | return AnyView(content.foregroundColor(Color(colors[0]))) 28 | } 29 | } else if colors.count == 3 { 30 | if #available(iOS 15.0, *) { 31 | return AnyView(content.foregroundStyle(Color(colors[0]), Color(colors[1]), Color(colors[2]))) 32 | } else { 33 | return AnyView(content.foregroundColor(Color(colors[0]))) 34 | } 35 | } else { 36 | return AnyView(content) 37 | } 38 | } 39 | } 40 | 41 | struct RenderingModeModifier: ViewModifier { 42 | var renderingMode: SFSymbolRenderingMode 43 | func body(content: Content) -> some View { 44 | if #available(iOS 15.0, *) { 45 | return AnyView(content.symbolRenderingMode(renderingMode.toSFSymbolRenderingMode())) 46 | } else { 47 | return AnyView(content) 48 | } 49 | } 50 | } 51 | 52 | struct VariantModifier: ViewModifier { 53 | var variant: String? 54 | func body(content: Content) -> some View { 55 | if #available(iOS 15.0, *) { 56 | return AnyView(content.symbolVariant(parseComposition(variant ?? ""))) 57 | } else { 58 | return AnyView(content) 59 | } 60 | } 61 | } 62 | 63 | @available(iOS 15.0, *) 64 | func parseComposition(_ composition: String) -> SymbolVariants { 65 | let variantMapping: [String: SymbolVariants] = [ 66 | "none": .none, 67 | "circle": .circle, 68 | "square": .square, 69 | "rectangle": .rectangle, 70 | "fill": .fill, 71 | "slash": .slash 72 | ] 73 | let components = composition.split(separator: ".") 74 | if components.count > 0 { 75 | var result: SymbolVariants? = variantMapping[String(components[0])] 76 | for i in 1.. SymbolVariants { 90 | switch other { 91 | case .circle: return self.circle 92 | case .fill: return self.fill 93 | case .slash: return self.slash 94 | case .rectangle: return self.rectangle 95 | case .square: return self.square 96 | default: return self 97 | } 98 | } 99 | } 100 | 101 | struct SymbolEffectModifier: ViewModifier { 102 | var symbolEffect: SFSymbolEffect? 103 | func body(content: Content) -> some View { 104 | if symbolEffect == nil { 105 | return AnyView(content) 106 | } 107 | if #available(iOS 17.0, *) { 108 | let options: SymbolEffectOptions = symbolEffect?.toSymbolEffectOptions() ?? .default 109 | let direction = symbolEffect?.direction // maybe set this to up by default for scale 110 | let animateBy = symbolEffect?.animateBy 111 | let reversing = symbolEffect?.reversing 112 | let inactiveLayers = symbolEffect?.inactiveLayers 113 | 114 | switch symbolEffect?.type { 115 | case "bounce": 116 | var bounceEffect: BounceSymbolEffect = .bounce 117 | if direction == "up" { 118 | bounceEffect = bounceEffect.up 119 | } else if direction == "down" { 120 | bounceEffect = bounceEffect.down 121 | } 122 | if animateBy == "layer" { 123 | bounceEffect = bounceEffect.byLayer 124 | } else if animateBy == "wholeSymbol" { 125 | bounceEffect = bounceEffect.wholeSymbol 126 | } 127 | if let value = symbolEffect?.value as? Double { 128 | return AnyView(content.symbolEffect(bounceEffect, options: options, value: value)) 129 | } else { 130 | return AnyView(content) 131 | } 132 | case "pulse": 133 | var pulseEffect: PulseSymbolEffect = .pulse 134 | if animateBy == "layer" { 135 | pulseEffect = pulseEffect.byLayer 136 | } else if animateBy == "wholeSymbol" { 137 | pulseEffect = pulseEffect.wholeSymbol 138 | } 139 | if let isActive = symbolEffect?.isActive { 140 | return AnyView(content.symbolEffect(pulseEffect, options: options, isActive: isActive)) 141 | } else if let value = symbolEffect?.value as? Double { 142 | return AnyView(content.symbolEffect(pulseEffect, options: options, value: value)) 143 | } else { 144 | return AnyView(content.symbolEffect(pulseEffect, options: options)) 145 | } 146 | case "variableColor": 147 | var variableColorEffect: VariableColorSymbolEffect = .variableColor 148 | if reversing == true { 149 | variableColorEffect = variableColorEffect.reversing 150 | } 151 | if inactiveLayers == "hide" { 152 | variableColorEffect = variableColorEffect.hideInactiveLayers 153 | } else if inactiveLayers == "dim" { 154 | variableColorEffect = variableColorEffect.dimInactiveLayers 155 | } 156 | if animateBy == "layer" { 157 | variableColorEffect = variableColorEffect.iterative 158 | } else if animateBy == "wholeSymbol" { 159 | variableColorEffect = variableColorEffect.cumulative 160 | } 161 | if let isActive = symbolEffect?.isActive { 162 | return AnyView(content.symbolEffect(.variableColor, options: options, isActive: isActive)) 163 | } else if let value = symbolEffect?.value as? any Equatable { 164 | return AnyView(content.symbolEffect(.variableColor, options: options, value: value)) 165 | } else { 166 | return AnyView(content.symbolEffect(.variableColor, options: options)) 167 | } 168 | case "appear": 169 | var appearEffect: AppearSymbolEffect = .appear 170 | if direction == "up" { 171 | appearEffect = appearEffect.up 172 | } else if direction == "down" { 173 | appearEffect = appearEffect.down 174 | } 175 | if animateBy == "layer" { 176 | appearEffect = appearEffect.byLayer 177 | } else if animateBy == "wholeSymbol" { 178 | appearEffect = appearEffect.wholeSymbol 179 | } 180 | return AnyView(content.symbolEffect(appearEffect, options: options, isActive: symbolEffect?.isActive ?? false)) 181 | case "disappear": 182 | var disappearEffect: DisappearSymbolEffect = .disappear 183 | if direction == "up" { 184 | disappearEffect = disappearEffect.up 185 | } else if direction == "down" { 186 | disappearEffect = disappearEffect.down 187 | } 188 | if animateBy == "layer" { 189 | disappearEffect = disappearEffect.byLayer 190 | } else if animateBy == "wholeSymbol" { 191 | disappearEffect = disappearEffect.wholeSymbol 192 | } 193 | return AnyView(content.symbolEffect(disappearEffect, options: options, isActive: symbolEffect?.isActive ?? false)) 194 | case "scale": 195 | var scaleEffect: ScaleSymbolEffect = .scale 196 | if direction == "up" || direction == nil { 197 | scaleEffect = scaleEffect.up 198 | } else if direction == "down" { 199 | scaleEffect = scaleEffect.down 200 | } 201 | if animateBy == "layer" { 202 | scaleEffect = scaleEffect.byLayer 203 | } else if animateBy == "wholeSymbol" { 204 | scaleEffect = scaleEffect.wholeSymbol 205 | } 206 | if let isActive = symbolEffect?.isActive { 207 | return AnyView(content.symbolEffect(scaleEffect, options: options, isActive: isActive)) 208 | } else { 209 | return AnyView(content.symbolEffect(scaleEffect, options: options)) 210 | } 211 | case "replace": 212 | var replaceEffect: ReplaceSymbolEffect = .replace 213 | if direction == "downUp" { 214 | replaceEffect = replaceEffect.downUp 215 | } else if direction == "upUp" { 216 | replaceEffect = replaceEffect.upUp 217 | } else if direction == "offUp" { 218 | replaceEffect = replaceEffect.offUp 219 | } 220 | if animateBy == "layer" { 221 | replaceEffect = replaceEffect.byLayer 222 | } else if animateBy == "wholeSymbol" { 223 | replaceEffect = replaceEffect.wholeSymbol 224 | } 225 | return AnyView(content.contentTransition(.symbolEffect(replaceEffect, options: options))) 226 | default: 227 | return AnyView(content) 228 | } 229 | } else { 230 | return AnyView(content) 231 | } 232 | } 233 | } 234 | 235 | 236 | -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { 3 | Button, 4 | PlatformColor, 5 | ScrollView, 6 | StyleSheet, 7 | Text, 8 | TouchableOpacity, 9 | View, 10 | } from "react-native"; 11 | import SweetSFSymbol from "sweet-sfsymbols"; 12 | 13 | export default function App() { 14 | const [appearIsActive, setAppearIsActive] = useState(false); 15 | const [disappearIsActive, setDisappearIsActive] = useState(false); 16 | const [bounceValue, setBounceValue] = useState(false); 17 | const [pulseValue, setPulseValue] = useState(0); 18 | const [pulseIsActive, setPulseIsActive] = useState(false); 19 | const [scaleIsActive, setScaleIsActive] = useState(false); 20 | const [variableColorIsActive, setVariableColorIsActive] = useState(false); 21 | const [variableColorValue, setVariableColorValue] = useState(0); 22 | const [replaceIsActive, setReplaceIsActive] = useState(false); 23 | const [variableValue, setVariableValue] = useState(0); 24 | 25 | return ( 26 | 27 | 28 | {/* Symbol Variants */} 29 | Symbol Variants 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {/* Rendering Modes */} 38 | Rendering Modes 39 | 40 | Palette 41 | 46 | 47 | 48 | Hierarchical 49 | 54 | 55 | 56 | Monochrome 57 | 62 | 63 | 64 | Multicolor 65 | 70 | 71 | {/* Variable Value */} 72 | Variable Values 73 | 74 | 75 | setVariableValue(variableValue + 0.1)} 78 | onDecrement={() => setVariableValue(variableValue - 0.1)} 79 | /> 80 | 85 | 86 | {/* Symbol Effect */} 87 | Discrete Symbol Effect 88 | 89 | 90 | Bounce 91 |