Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,51 @@ Background color color of the control. (iOS 13+ only)

### `values`

The labels for the control's segment buttons, in order.
The labels for the control's segment buttons, in order. Supports strings, image sources (`require()`), and SF Symbol objects (iOS 13+ only).

| Type | Required |
| ------- | -------- |
| (string | number | Image)[] | No |
| Type | Required |
| --------------------------------- | -------- |
| `(string \| number \| SFSymbol)[]` | No |

#### Using SF Symbols (iOS 13+ only)

You can use [SF Symbols](https://developer.apple.com/sf-symbols/) as segment values by passing an object with a `systemImage` key:

```javascript
<SegmentedControl
values={[
{systemImage: 'list.bullet'},
{systemImage: 'square.grid.2x2'},
{systemImage: 'rectangle.grid.1x2'},
]}
selectedIndex={0}
/>
```

SF Symbol objects support the following properties:

| Property | Type | Default | Description |
| ------------- | -------- | ----------- | ---------------------------------------------------- |
| `systemImage` | string | *required* | The SF Symbol name (e.g., `'star.fill'`, `'heart'`) |
| `fontSize` | number | `19` | The point size of the symbol |
| `weight` | string | `'regular'` | Symbol weight: `'ultraLight'`, `'thin'`, `'light'`, `'regular'`, `'medium'`, `'semibold'`, `'bold'`, `'heavy'`, `'black'` |

You can also mix text and SF Symbols:

```javascript
<SegmentedControl
values={[
'All',
{systemImage: 'star.fill', fontSize: 16, weight: 'semibold'},
{systemImage: 'clock', fontSize: 16},
]}
selectedIndex={0}
/>
```

> **Note:** SF Symbols are only supported on iOS. On Android and Web, segments with SF Symbol values will render as empty.

---

### `appearance`

Expand Down
32 changes: 31 additions & 1 deletion example/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import SegmentedControl from '..';
import React, {useEffect, useState} from 'react';
import {ScrollView, StyleSheet, Text, View, useColorScheme} from 'react-native';
import {ScrollView, StyleSheet, Text, View, Platform, useColorScheme} from 'react-native';

const App = () => {
const colorScheme = useColorScheme();
Expand Down Expand Up @@ -53,6 +53,36 @@ const App = () => {
]}
/>
</View>
{Platform.OS === 'ios' && (
<View style={styles.segmentContainer}>
<Text style={[styles.text, {color: textColor}]}>
Segmented controls can have SF Symbols (iOS only)
</Text>
<SegmentedControl
values={[
{systemImage: 'list.bullet'},
{systemImage: 'square.grid.2x2'},
{systemImage: 'rectangle.grid.1x2'},
]}
selectedIndex={0}
/>
</View>
)}
{Platform.OS === 'ios' && (
<View style={styles.segmentContainer}>
<Text style={[styles.text, {color: textColor}]}>
SF Symbols can be mixed with text
</Text>
<SegmentedControl
values={[
'All',
{systemImage: 'star.fill', fontSize: 16, weight: 'semibold'},
{systemImage: 'clock', fontSize: 16},
]}
selectedIndex={0}
/>
</View>
)}
<View style={styles.segmentSection}>
<Text style={[styles.text, {color: textColor}]}>
Segmented controls can have pre-selected values
Expand Down
40 changes: 38 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,41 @@ import {

type Constructor<T> = new (...args: any[]) => T;

export type SFSymbolWeight =
| 'ultraLight'
| 'thin'
| 'light'
| 'regular'
| 'medium'
| 'semibold'
| 'bold'
| 'heavy'
| 'black';

/**
* Represents an SF Symbol configuration (iOS 13+ only).
* SF Symbols are Apple's built-in icon system providing thousands of
* configurable symbols that automatically align with text and adapt to
* the system appearance.
*/
export type SFSymbol = {
/**
* The name of the SF Symbol (e.g., 'star.fill', 'heart', 'gear').
* See https://developer.apple.com/sf-symbols/ for the full list.
*/
systemImage: string;
/**
* The point size of the symbol. Default is 19.
*/
fontSize?: number;
/**
* The weight of the symbol. Default is 'regular'.
*/
weight?: SFSymbolWeight;
};

export type SegmentValue = string | number | SFSymbol;

export interface NativeSegmentedControlIOSChangeEvent extends TargetedEvent {
value: string;
selectedSegmentIndex: number;
Expand Down Expand Up @@ -70,7 +105,7 @@ export interface SegmentedControlProps extends ViewProps {
/**
* Callback that is called when the user taps a segment; passes the segment's value as an argument
*/
onValueChange?: (value: string) => void;
onValueChange?: (value: SegmentValue) => void;

/**
* The index in props.values of the segment to be (pre)selected.
Expand All @@ -90,8 +125,9 @@ export interface SegmentedControlProps extends ViewProps {

/**
* The labels for the control's segment buttons, in order.
* Supports strings, image sources (require()), and SF Symbol objects (iOS 13+ only).
*/
values?: string[];
values?: SegmentValue[];

/**
* (iOS 13+ only)
Expand Down
88 changes: 74 additions & 14 deletions ios/RNCSegmentedControl.m
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,80 @@ - (instancetype)initWithFrame:(CGRect)frame {
}

- (void)setValues:(NSArray *)values {
[self removeAllSegments];
for (id segment in values) {
if ([segment isKindOfClass:[NSMutableDictionary class]]){
UIImage *image = [[RCTConvert UIImage:segment] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
[self insertSegmentWithImage:image
atIndex:self.numberOfSegments
animated:NO];
} else {
[self insertSegmentWithTitle:(NSString *)segment
atIndex:self.numberOfSegments
animated:NO];
}
}
super.selectedSegmentIndex = _selectedIndex;
[self removeAllSegments];

for (id segment in values) {
if ([segment isKindOfClass:[NSDictionary class]]) {

NSDictionary *dict = (NSDictionary *)segment;
UIImage *image = nil;

// 🔹 CASE 1: SF Symbol
if (dict[@"systemImage"]) {

#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \
__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
if (@available(iOS 13.0, *)) {

NSString *systemName = dict[@"systemImage"];

CGFloat fontSize = dict[@"fontSize"] ? [dict[@"fontSize"] floatValue] : 19.0;

UIImageSymbolWeight weight = UIImageSymbolWeightRegular;

if (dict[@"weight"]) {
NSString *weightString = dict[@"weight"];

if ([weightString isEqualToString:@"ultraLight"])
weight = UIImageSymbolWeightUltraLight;
else if ([weightString isEqualToString:@"thin"])
weight = UIImageSymbolWeightThin;
else if ([weightString isEqualToString:@"light"])
weight = UIImageSymbolWeightLight;
else if ([weightString isEqualToString:@"medium"])
weight = UIImageSymbolWeightMedium;
else if ([weightString isEqualToString:@"semibold"])
weight = UIImageSymbolWeightSemibold;
else if ([weightString isEqualToString:@"bold"])
weight = UIImageSymbolWeightBold;
else if ([weightString isEqualToString:@"heavy"])
weight = UIImageSymbolWeightHeavy;
else if ([weightString isEqualToString:@"black"])
weight = UIImageSymbolWeightBlack;
}

UIImageSymbolConfiguration *config =
[UIImageSymbolConfiguration configurationWithPointSize:fontSize
weight:weight];

image = [[UIImage systemImageNamed:systemName]
imageByApplyingSymbolConfiguration:config];

image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
}
#endif
}
else {
image = [[RCTConvert UIImage:segment]
imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
}

if (image) {
[self insertSegmentWithImage:image
atIndex:self.numberOfSegments
animated:NO];
}
}

else if ([segment isKindOfClass:[NSString class]]) {

[self insertSegmentWithTitle:(NSString *)segment
atIndex:self.numberOfSegments
animated:NO];
}
}

super.selectedSegmentIndex = _selectedIndex;
}

- (void)setSelectedIndex:(NSInteger)selectedIndex {
Expand Down
14 changes: 11 additions & 3 deletions js/SegmentedControl.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,17 @@ class SegmentedControlIOS extends React.Component<Props> {
}
: undefined
}
values={values.map((val) =>
typeof val === 'string' ? val : Image.resolveAssetSource(val),
)}
values={values.map((val) => {
if (typeof val === 'string') {
return val;
}
// SF Symbol objects have a systemImage key — pass through as-is
if (typeof val === 'object' && val !== null && val.systemImage) {
return val;
}
// Image sources (require() numbers or objects) need resolution
return Image.resolveAssetSource(val);
})}
{...props}
ref={forwardedRef}
style={[styles.segmentedControl, this.props.style]}
Expand Down
9 changes: 6 additions & 3 deletions js/SegmentedControlTab.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import {
Platform,
} from 'react-native';

import type {FontStyle, ViewStyle} from './types';
import type {FontStyle, ViewStyle, SFSymbol} from './types';

type Props = $ReadOnly<{|
value: string | number | Object,
value: string | number | SFSymbol,
tintColor?: ?string,
onSelect: () => void,
selected: boolean,
Expand Down Expand Up @@ -103,7 +103,10 @@ export const SegmentedControlTab = ({
ios: typeof value === 'string' ? value : testID,
})}>
<View style={styles.default}>
{typeof value === 'number' || typeof value === 'object' ? (
{typeof value === 'object' && value !== null && value.systemImage ? (
// SF Symbols are iOS-only; on Android/Web render nothing
null
) : typeof value === 'number' || typeof value === 'object' ? (
<Image source={value} style={styles.segmentImage} />
) : isBase64(value) ? (
<Image source={{uri: value}} style={styles.segmentImage} />
Expand Down
35 changes: 33 additions & 2 deletions js/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,37 @@ export type Event = SyntheticEvent<

export type ViewStyle = ViewStyleProp;

export type SFSymbolWeight =
| 'ultraLight'
| 'thin'
| 'light'
| 'regular'
| 'medium'
| 'semibold'
| 'bold'
| 'heavy'
| 'black';

/**
* Represents an SF Symbol configuration (iOS 13+ only).
*/
export type SFSymbol = $ReadOnly<{|
/**
* The name of the SF Symbol (e.g., 'star.fill', 'heart', 'gear').
*/
systemImage: string,
/**
* The point size of the symbol. Default is 19.
*/
fontSize?: number,
/**
* The weight of the symbol. Default is 'regular'.
*/
weight?: SFSymbolWeight,
|}>;

export type SegmentValue = string | number | SFSymbol;

export type FontStyle = $ReadOnly<{|
/**
* Font Color of Segmented Control
Expand Down Expand Up @@ -51,7 +82,7 @@ export type SegmentedControlProps = $ReadOnly<{|
/**
* The labels for the control's segment buttons, in order.
*/
values: $ReadOnlyArray<string | number | Object>,
values: $ReadOnlyArray<SegmentValue>,
/**
* The index in `props.values` of the segment to be (pre)selected.
*/
Expand All @@ -60,7 +91,7 @@ export type SegmentedControlProps = $ReadOnly<{|
* Callback that is called when the user taps a segment;
* passes the segment's value as an argument
*/
onValueChange?: ?(value: string | number | Object) => mixed,
onValueChange?: ?(value: SegmentValue) => mixed,
/**
* Callback that is called when the user taps a segment;
* passes the event as an argument
Expand Down