Make icon symbols generic between Android and iOS platforms (#846)

This commit is contained in:
Leendert de Borst
2025-05-20 11:47:48 +02:00
parent 680f5ba926
commit bbba8d1393
6 changed files with 65 additions and 33 deletions

View File

@@ -3,6 +3,7 @@ import React, { useEffect } from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import { IconSymbol } from '@/components/ui/IconSymbol';
import { IconSymbolName } from '@/components/ui/IconSymbolName';
import TabBarBackground from '@/components/ui/TabBarBackground';
import { useColors } from '@/hooks/useColorScheme';
import { useAuth } from '@/context/AuthContext';
@@ -94,7 +95,7 @@ export default function TabLayout() : React.ReactNode {
/**
* Icon for the credentials tab.
*/
tabBarIcon: ({ color }) => <IconSymbol size={28} name="key.fill" color={color} />,
tabBarIcon: ({ color }) => <IconSymbol size={28} name={IconSymbolName.Key} color={color} />,
}}
/>
<Tabs.Screen
@@ -104,7 +105,7 @@ export default function TabLayout() : React.ReactNode {
/**
* Icon for the emails tab.
*/
tabBarIcon: ({ color }) => <IconSymbol size={28} name="envelope.fill" color={color} />,
tabBarIcon: ({ color }) => <IconSymbol size={28} name={IconSymbolName.Envelope} color={color} />,
}}
/>
<Tabs.Screen
@@ -116,7 +117,7 @@ export default function TabLayout() : React.ReactNode {
*/
tabBarIcon: ({ color }) => (
<View style={styles.iconContainer}>
<IconSymbol size={28} name="gear" color={color} />
<IconSymbol size={28} name={IconSymbolName.Gear} color={color} />
{Platform.OS === 'ios' && authContext.shouldShowIosAutofillReminder && (
<View style={styles.iconNotificationContainer}>
<ThemedText style={styles.iconNotificationText}>1</ThemedText>

View File

@@ -13,6 +13,7 @@ import { ThemedText } from '@/components/themed/ThemedText';
import EncryptionUtility from '@/utils/EncryptionUtility';
import { useColors } from '@/hooks/useColorScheme';
import { IconSymbol } from '@/components/ui/IconSymbol';
import { IconSymbolName } from '@/components/ui/IconSymbolName';
import emitter from '@/utils/EventEmitter';
import { ThemedView } from '@/components/themed/ThemedView';
@@ -384,7 +385,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
{associatedCredential && (
<>
<TouchableOpacity onPress={handleOpenCredential} style={styles.metadataCredential}>
<IconSymbol size={16} name="key.fill" color={colors.primary} style={styles.metadataCredentialIcon} />
<IconSymbol size={16} name={IconSymbolName.Key} color={colors.primary} style={styles.metadataCredentialIcon} />
<ThemedText style={[styles.metadataText, { color: colors.primary }]}>
{associatedCredential.ServiceName}
</ThemedText>

View File

@@ -8,6 +8,7 @@ import { MailboxEmail } from '@/utils/types/webapi/MailboxEmail';
import { useDb } from '@/context/DbContext';
import { Credential } from '@/utils/types/Credential';
import { IconSymbol } from '@/components/ui/IconSymbol';
import { IconSymbolName } from '@/components/ui/IconSymbolName';
type EmailCardProps = {
email: MailboxEmail;
@@ -138,7 +139,7 @@ export function EmailCard({ email }: EmailCardProps) : React.ReactNode {
</ThemedText>
{associatedCredential && (
<View style={styles.serviceContainer}>
<IconSymbol size={14} name="key.fill" color={colors.primary} style={styles.serviceIcon} />
<IconSymbol size={14} name={IconSymbolName.Key} color={colors.primary} style={styles.serviceIcon} />
<ThemedText style={styles.serviceName}>
{associatedCredential.ServiceName}
</ThemedText>

View File

@@ -1,8 +1,25 @@
import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols';
import { SymbolView, SymbolWeight } from 'expo-symbols';
import { StyleProp, ViewStyle } from 'react-native';
import { IconSymbolName } from './IconSymbolName';
/**
* Icon symbol component.
* Mapping from IconSymbolName to SF Symbols names.
* This is the iOS-specific translation layer.
*/
const SF_SYMBOLS_MAPPING: Record<IconSymbolName, import('expo-symbols').SymbolViewProps['name']> = {
[IconSymbolName.Key]: 'key.fill',
[IconSymbolName.Envelope]: 'envelope.fill',
[IconSymbolName.Gear]: 'gear',
[IconSymbolName.House]: 'house.fill',
[IconSymbolName.Paperplane]: 'paperplane.fill',
[IconSymbolName.ChevronRight]: 'chevron.right',
[IconSymbolName.ChevronLeftRight]: 'chevron.left.forwardslash.chevron.right',
};
/**
* Icon symbol component for iOS.
* Uses native SF Symbols for optimal performance and consistency.
* Handles translation from IconSymbolName to SF Symbols names.
*/
export function IconSymbol({
name,
@@ -11,7 +28,7 @@ export function IconSymbol({
style,
weight = 'regular',
}: {
name: SymbolViewProps['name'];
name: IconSymbolName;
size?: number;
color: string;
style?: StyleProp<ViewStyle>;
@@ -22,7 +39,7 @@ export function IconSymbol({
weight={weight}
tintColor={color}
resizeMode="scaleAspectFit"
name={name}
name={SF_SYMBOLS_MAPPING[name]}
style={[
{
width: size,

View File

@@ -2,31 +2,28 @@
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import React from 'react';
import { OpaqueColorValue, StyleProp, ViewStyle } from 'react-native';
import { OpaqueColorValue, StyleProp, TextStyle } from 'react-native';
// Add your SFSymbol to MaterialIcons mappings here.
const MAPPING = {
/*
* See MaterialIcons here: https://icons.expo.fyi
* See SF Symbols in the SF Symbols app on Mac.
*/
'house.fill': 'home',
'paperplane.fill': 'send',
'chevron.left.forwardslash.chevron.right': 'code',
'chevron.right': 'chevron-right',
} as Partial<
Record<
import('expo-symbols').SymbolViewProps['name'],
React.ComponentProps<typeof MaterialIcons>['name']
>
>;
export type IconSymbolName = keyof typeof MAPPING;
import { IconSymbolName } from './IconSymbolName';
/**
* An icon component that uses native SFSymbols on iOS, and MaterialIcons on Android and web. This ensures a consistent look across platforms, and optimal resource usage.
*
* Icon `name`s are based on SFSymbols and require manual mapping to MaterialIcons.
* Mapping from IconSymbolName to MaterialIcons names.
* This is the Android-specific translation layer.
*/
const MATERIAL_ICONS_MAPPING: Record<IconSymbolName, React.ComponentProps<typeof MaterialIcons>['name']> = {
[IconSymbolName.Key]: 'key',
[IconSymbolName.Envelope]: 'mail',
[IconSymbolName.Gear]: 'settings',
[IconSymbolName.House]: 'home',
[IconSymbolName.Paperplane]: 'send',
[IconSymbolName.ChevronRight]: 'chevron-right',
[IconSymbolName.ChevronLeftRight]: 'code',
};
/**
* An icon component that uses MaterialIcons on Android and web.
* This ensures a consistent look across platforms, and optimal resource usage.
* Handles translation from IconSymbolName to MaterialIcons names.
*/
export function IconSymbol({
name,
@@ -37,7 +34,7 @@ export function IconSymbol({
name: IconSymbolName;
size?: number;
color: string | OpaqueColorValue;
style?: StyleProp<ViewStyle>;
style?: StyleProp<TextStyle>;
}): React.ReactNode {
return <MaterialIcons color={color} size={size} name={MAPPING[name]} style={style} />;
return <MaterialIcons color={color} size={size} name={MATERIAL_ICONS_MAPPING[name]} style={style} />;
}

View File

@@ -0,0 +1,15 @@
/**
* Enum representing all available icon names in the app.
* This provides type safety and consistency across platforms.
* The actual icon names for each platform are defined in their respective IconSymbol implementations.
*/
export enum IconSymbolName {
// Navigation icons
Key,
Envelope,
Gear,
House,
Paperplane,
ChevronRight,
ChevronLeftRight,
}