Skip to content

Commit 80f3ff2

Browse files
authored
feat(ios): SF Symbol scale support via iosSymbolScale (#10569)
1 parent 893b858 commit 80f3ff2

File tree

12 files changed

+118
-54
lines changed

12 files changed

+118
-54
lines changed

apps/toolbox/src/pages/image-handling.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ export function navigatingTo(args: EventData) {
1010

1111
export class DemoModel extends Observable {
1212
addingPhoto = false;
13-
symbolWiggleEffect: ImageSymbolEffects.Wiggle;
14-
symbolBounceEffect: ImageSymbolEffects.Bounce;
15-
symbolBreathEffect: ImageSymbolEffects.Breathe;
16-
symbolRotateEffect: ImageSymbolEffects.Rotate;
13+
symbolWiggleEffect = ImageSymbolEffects.Wiggle;
14+
symbolBounceEffect = ImageSymbolEffects.Bounce;
15+
symbolBreathEffect = ImageSymbolEffects.Breathe;
16+
symbolRotateEffect = ImageSymbolEffects.Rotate;
1717

1818
pickImage() {
1919
const context = create({

apps/toolbox/src/pages/image-handling.xml

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,23 @@
1313
<ios>
1414
<!-- SF Symbols with Effects -->
1515
<ContentView height="1" width="100%" backgroundColor="#efefef" margin="10"></ContentView>
16-
<GridLayout rows="auto,auto,auto" columns="*,*">
17-
<Image src="sys://photo.on.rectangle.angled" width="100" tintColor="green" symbolEffect="{{symbolWiggleEffect}}" padding="8"/>
18-
<Image col="1" src="sys://steeringwheel.and.hands" width="100" tintColor="black" symbolEffect="{{symbolWiggleEffect}}" padding="8" />
16+
<GridLayout rows="auto,auto,auto,auto,auto" columns="*,*">
17+
<Image src="sys://photo.on.rectangle.angled" width="100" tintColor="green" iosSymbolEffect="{{symbolBounceEffect}}" padding="8"/>
18+
<Image col="1" src="sys://photo.on.rectangle.angled" width="100" tintColor="green" iosSymbolEffect="{{symbolBounceEffect}}" iosSymbolScale="small" padding="8" />
1919

20-
<Image row="1" src="sys://airpods.pro.chargingcase.wireless.radiowaves.left.and.right.fill" width="100" symbolEffect="{{symbolBounceEffect}}" padding="8" />
21-
<Image row="1" col="1" src="sys://lungs.fill" width="100" symbolEffect="{{symbolBreathEffect}}" padding="8" />
20+
<Image row="1" src="sys://photo.on.rectangle.angled" width="100" tintColor="green" iosSymbolEffect="{{symbolBounceEffect}}" iosSymbolScale="medium" padding="8"/>
21+
<Image row="1" col="1" src="sys://photo.on.rectangle.angled" width="100" tintColor="green" iosSymbolEffect="{{symbolBounceEffect}}" iosSymbolScale="large" padding="8"/>
2222

23+
<Image row="2" src="sys://airpods.pro.chargingcase.wireless.radiowaves.left.and.right.fill" width="100" iosSymbolEffect="{{symbolBounceEffect}}" padding="8" />
24+
<Image row="2" col="1" src="sys://lungs.fill" width="100" iosSymbolEffect="{{symbolBreathEffect}}" padding="8" />
25+
26+
27+
<Image row="3" src="sys://clock.arrow.trianglehead.2.counterclockwise.rotate.90" width="100" iosSymbolEffect="{{symbolRotateEffect}}" padding="8" />
28+
<Image row="3" col="1" src="sys://square.and.arrow.up" width="100" iosSymbolEffect="{{symbolWiggleEffect}}" padding="8" />
29+
30+
<Image row="4" src="sys://steeringwheel.and.hands" width="100" tintColor="black" iosSymbolEffect="{{symbolWiggleEffect}}" padding="8" />
31+
<Image row="4" col="1" src="sys://steeringwheel.and.hands" width="100" tintColor="black" iosSymbolEffect="{{symbolWiggleEffect}}" iosSymbolScale="large" padding="8" />
2332

24-
<Image row="2" src="sys://clock.arrow.trianglehead.2.counterclockwise.rotate.90" width="100" symbolEffect="{{symbolRotateEffect}}" padding="8" />
25-
<Image row="2" col="1" src="sys://square.and.arrow.up" width="100" symbolEffect="{{symbolWiggleEffect}}" padding="8" />
2633
</GridLayout>
2734
</ios>
2835

packages/core/image-source/index.android.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Definitions.
2-
import { ImageSource as ImageSourceDefinition } from '.';
2+
import { ImageSource as ImageSourceDefinition, iosSymbolScaleType } from '.';
33
import { ImageAsset } from '../image-asset';
44
import * as httpModule from '../http';
55

@@ -149,6 +149,10 @@ export class ImageSource implements ImageSourceDefinition {
149149
return ImageSource.fromFileSync(path);
150150
}
151151

152+
static iosSymbolScaleFor(scale: iosSymbolScaleType): number {
153+
return 0;
154+
}
155+
152156
static fromSystemImageSync(name: string): ImageSource {
153157
return ImageSource.fromResourceSync(name);
154158
}

packages/core/image-source/index.d.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,23 @@ export class ImageSource {
5454
*/
5555
static fromResource(name: string): Promise<ImageSource>;
5656

57+
/**
58+
* (iOS only) Get system symbol scale
59+
* @param scale symbol scale type
60+
*/
61+
static iosSymbolScaleFor(scale: iosSymbolScaleType): number;
62+
5763
/**
5864
* Loads this instance from the specified system image name.
5965
* @param name the name of the system image
6066
*/
61-
static fromSystemImageSync(name: string): ImageSource;
67+
static fromSystemImageSync(name: string, scale?: iosSymbolScaleType): ImageSource;
6268

6369
/**
6470
* Loads this instance from the specified system image name asynchronously.
6571
* @param name the name of the system image
6672
*/
67-
static fromSystemImage(name: string): Promise<ImageSource>;
73+
static fromSystemImage(name: string, scale?: iosSymbolScaleType): Promise<ImageSource>;
6874

6975
/**
7076
* Loads this instance from the specified file.
@@ -259,6 +265,12 @@ export class ImageSource {
259265
resizeAsync(maxSize: number, options?: any): Promise<ImageSource>;
260266
}
261267

268+
/**
269+
* iOS only
270+
* SF Symbol scale
271+
*/
272+
export type iosSymbolScaleType = 'default' | 'small' | 'medium' | 'large';
273+
262274
/**
263275
* @deprecated Use ImageSource.fromAsset() instead.
264276
* Creates a new ImageSource instance and loads it from the specified image asset asynchronously.

packages/core/image-source/index.ios.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Definitions.
2-
import { ImageSource as ImageSourceDefinition } from '.';
2+
import { ImageSource as ImageSourceDefinition, iosSymbolScaleType } from '.';
33
import { ImageAsset } from '../image-asset';
44
import * as httpModule from '../http';
55
import { Font } from '../ui/styling/font';
@@ -73,16 +73,39 @@ export class ImageSource implements ImageSourceDefinition {
7373
return http.getImage(url);
7474
}
7575

76-
static fromSystemImageSync(name: string): ImageSource {
77-
const image = UIImage.systemImageNamed(name);
76+
static iosSystemScaleFor(scale: iosSymbolScaleType) {
77+
switch (scale) {
78+
case 'small':
79+
return UIImageSymbolScale.Small;
80+
case 'medium':
81+
return UIImageSymbolScale.Medium;
82+
case 'large':
83+
return UIImageSymbolScale.Large;
84+
default:
85+
return UIImageSymbolScale.Default;
86+
}
87+
}
88+
89+
static fromSystemImageSync(name: string, scale?: iosSymbolScaleType): ImageSource {
90+
if (scale) {
91+
const image = UIImage.systemImageNamedWithConfiguration(name, UIImageSymbolConfiguration.configurationWithScale(ImageSource.iosSystemScaleFor(scale)));
92+
return image ? new ImageSource(image) : null;
93+
} else {
94+
const image = UIImage.systemImageNamed(name);
7895

79-
return image ? new ImageSource(image) : null;
96+
return image ? new ImageSource(image) : null;
97+
}
8098
}
8199

82-
static fromSystemImage(name: string): Promise<ImageSource> {
100+
static fromSystemImage(name: string, scale?: iosSymbolScaleType): Promise<ImageSource> {
83101
return new Promise<ImageSource>((resolve, reject) => {
84102
try {
85-
const image = UIImage.systemImageNamed(name);
103+
let image: UIImage;
104+
if (scale) {
105+
image = UIImage.systemImageNamedWithConfiguration(name, UIImageSymbolConfiguration.configurationWithScale(ImageSource.iosSystemScaleFor(scale)));
106+
} else {
107+
image = UIImage.systemImageNamed(name);
108+
}
86109
if (image) {
87110
resolve(new ImageSource(image));
88111
} else {

packages/core/references.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!CFNetwork.d.ts" />
33
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!CoreText.d.ts" />
44
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!Darwin.d.ts" />
5+
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!DarwinFoundation.d.ts" />
56
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!Symbols.d.ts" />
67
/// <reference path="../types-android/src/lib/android-29.d.ts" />
78
/// <reference path="./platforms/ios/typings/objc!MaterialComponents.d.ts" />

packages/core/ui/core/view/index.ios.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -901,8 +901,8 @@ export class View extends ViewCommon implements ViewDefinition {
901901

902902
let notification: number;
903903
let args: string | UIView | null = this.nativeViewProtected;
904-
if (typeof msg === 'string' && msg) {
905-
args = msg;
904+
if (options?.message) {
905+
args = options.message;
906906
}
907907

908908
switch (options.iosNotificationType) {

packages/core/ui/image/image-common.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { View, CSSType } from '../core/view';
33
import { booleanConverter } from '../core/view-base';
44
import { CoreTypes } from '../../core-types';
55
import { ImageAsset } from '../../image-asset';
6-
import { ImageSource } from '../../image-source';
6+
import { ImageSource, iosSymbolScaleType } from '../../image-source';
77
import { isDataURI, isFontIconURI, isFileOrResourcePath, RESOURCE_PREFIX, SYSTEM_PREFIX } from '../../utils';
88
import { Color } from '../../color';
99
import { Style } from '../styling/style';
@@ -21,6 +21,7 @@ export abstract class ImageBase extends View implements ImageDefinition {
2121
public loadMode: 'sync' | 'async';
2222
public decodeWidth: CoreTypes.LengthType;
2323
public decodeHeight: CoreTypes.LengthType;
24+
public iosSymbolScale: iosSymbolScaleType;
2425

2526
get tintColor(): Color {
2627
return this.style.tintColor;
@@ -86,10 +87,10 @@ export abstract class ImageBase extends View implements ImageDefinition {
8687
} else if (value.indexOf(SYSTEM_PREFIX) === 0) {
8788
const sysPath = value.slice(SYSTEM_PREFIX.length);
8889
if (sync) {
89-
imageLoaded(ImageSource.fromSystemImageSync(sysPath));
90+
imageLoaded(ImageSource.fromSystemImageSync(sysPath, this.iosSymbolScale));
9091
} else {
9192
this.imageSource = null;
92-
ImageSource.fromSystemImage(sysPath).then(imageLoaded);
93+
ImageSource.fromSystemImage(sysPath, this.iosSymbolScale).then(imageLoaded);
9394
}
9495
} else {
9596
if (sync) {
@@ -196,4 +197,12 @@ export const iosSymbolEffectProperty = new Property<ImageBase, ImageSymbolEffect
196197
});
197198
iosSymbolEffectProperty.register(ImageBase);
198199

200+
/**
201+
* iOS only
202+
*/
203+
export const iosSymbolScaleProperty = new Property<ImageBase, iosSymbolScaleType>({
204+
name: 'iosSymbolScale',
205+
});
206+
iosSymbolScaleProperty.register(ImageBase);
207+
199208
export { ImageSymbolEffect, ImageSymbolEffects };

packages/core/ui/image/index.ios.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { ImageBase, stretchProperty, imageSourceProperty, tintColorProperty, srcProperty, iosSymbolEffectProperty, ImageSymbolEffect, ImageSymbolEffects } from './image-common';
2-
import { ImageSource } from '../../image-source';
1+
import { ImageBase, stretchProperty, imageSourceProperty, tintColorProperty, srcProperty, iosSymbolEffectProperty, ImageSymbolEffect, ImageSymbolEffects, iosSymbolScaleProperty } from './image-common';
2+
import { ImageSource, iosSymbolScaleType } from '../../image-source';
33
import { ImageAsset } from '../../image-asset';
44
import { Color } from '../../color';
55
import { Trace } from '../../trace';
@@ -192,19 +192,35 @@ export class Image extends ImageBase {
192192
this._setNativeImage(value ? value.ios : null);
193193
}
194194

195-
[srcProperty.setNative](value: string | ImageSource | ImageAsset) {
195+
private _setSrc(value: string | ImageSource | ImageAsset) {
196196
this._createImageSourceFromSrc(value);
197+
if (this.iosSymbolScale) {
198+
// when applying symbol scale, contentMode must be center
199+
// https://stackoverflow.com/a/65787627
200+
this.nativeViewProtected.contentMode = UIViewContentMode.Center;
201+
}
202+
}
203+
204+
[srcProperty.setNative](value: string | ImageSource | ImageAsset) {
205+
this._setSrc(value);
197206
}
198207

199208
[iosSymbolEffectProperty.setNative](value: ImageSymbolEffect | ImageSymbolEffects) {
200209
if (SDK_VERSION < 17) {
201210
return;
202211
}
203212
const symbol = typeof value === 'string' ? ImageSymbolEffect.fromSymbol(value) : value;
204-
if (symbol && symbol.effect) {
213+
if (symbol?.effect) {
214+
// Note: https://developer.apple.com/documentation/symbols/symboleffectoptions/4197883-repeating
215+
// Will want to move to https://developer.apple.com/documentation/symbols/nssymboleffectoptionsrepeatbehavior?language=objc as fallback once optionsWithRepeating is removed
205216
this.nativeViewProtected.addSymbolEffectOptionsAnimatedCompletion(symbol.effect, symbol.options || NSSymbolEffectOptions.optionsWithRepeating(), true, symbol.completion || null);
206217
} else {
207218
this.nativeViewProtected.removeAllSymbolEffects();
208219
}
209220
}
221+
222+
[iosSymbolScaleProperty.setNative](value: iosSymbolScaleType) {
223+
// reset src to configure scale
224+
this._setSrc(this.src);
225+
}
210226
}
Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { ImageSymbolEffectCommon, ImageSymbolEffects } from './symbol-effects-common';
2-
import type { ImageSymbolEffect as ImageSymbolEffectDefinition } from './symbol-effects.d.ts';
3-
export { ImageSymbolEffects };
1+
import { ImageSymbolEffectCommon } from './symbol-effects-common';
2+
export { ImageSymbolEffects } from './symbol-effects-common';
43

5-
export const ImageSymbolEffect: typeof ImageSymbolEffectDefinition = class ImageSymbolEffect extends ImageSymbolEffectCommon implements ImageSymbolEffectDefinition {
6-
static fromSymbol(symbol: string): ImageSymbolEffectDefinition {
4+
export class ImageSymbolEffect extends ImageSymbolEffectCommon {
5+
static fromSymbol(symbol: string) {
76
return new ImageSymbolEffect();
87
}
9-
};
8+
}

packages/core/ui/image/symbol-effects.ios.ts

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { SDK_VERSION } from '../../utils/constants';
22
import { ImageSymbolEffectCommon, ImageSymbolEffects } from './symbol-effects-common';
3-
import type { ImageSymbolEffect as ImageSymbolEffectDefinition } from './symbol-effects.d.ts';
3+
export { ImageSymbolEffects } from './symbol-effects-common';
44

5-
export const ImageSymbolEffect: typeof ImageSymbolEffectDefinition = class ImageSymbolEffect extends ImageSymbolEffectCommon implements ImageSymbolEffectDefinition {
5+
export class ImageSymbolEffect extends ImageSymbolEffectCommon {
66
constructor(symbol: NSSymbolEffect) {
77
super();
88
this.effect = symbol;
99
}
10-
static fromSymbol(symbol: string): ImageSymbolEffectDefinition | null {
10+
static fromSymbol(symbol: string): ImageSymbolEffect | null {
1111
if (SDK_VERSION < 17) {
1212
return null;
1313
}
@@ -44,52 +44,37 @@ export const ImageSymbolEffect: typeof ImageSymbolEffectDefinition = class Image
4444
if (SDK_VERSION < 18) {
4545
return null;
4646
}
47-
// TODO: remove ts-expect-error once we bump the types package
4847
switch (symbol) {
4948
case ImageSymbolEffects.Breathe:
50-
// @ts-expect-error added on iOS 18
5149
return new ImageSymbolEffect(NSSymbolBreatheEffect.effect());
5250
case ImageSymbolEffects.BreathePlain:
53-
// @ts-expect-error added on iOS 18
5451
return new ImageSymbolEffect(NSSymbolBreatheEffect.breathePlainEffect());
5552
case ImageSymbolEffects.Rotate:
56-
// @ts-expect-error added on iOS 18
5753
return new ImageSymbolEffect(NSSymbolRotateEffect.effect());
5854
case ImageSymbolEffects.RotateClockwise:
59-
// @ts-expect-error added on iOS 18
6055
return new ImageSymbolEffect(NSSymbolRotateEffect.rotateClockwiseEffect());
6156
case ImageSymbolEffects.RotateCounterClockwise:
62-
// @ts-expect-error added on iOS 18
6357
return new ImageSymbolEffect(NSSymbolRotateEffect.rotateCounterClockwiseEffect());
6458
case ImageSymbolEffects.Wiggle:
65-
// @ts-expect-error added on iOS 18
6659
return new ImageSymbolEffect(NSSymbolWiggleEffect.effect());
6760
case ImageSymbolEffects.WiggleBackward:
68-
// @ts-expect-error added on iOS 18
6961
return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleBackwardEffect());
7062
case ImageSymbolEffects.WiggleClockwise:
71-
// @ts-expect-error added on iOS 18
7263
return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleClockwiseEffect());
7364
case ImageSymbolEffects.WiggleCounterClockwise:
74-
// @ts-expect-error added on iOS 18
7565
return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleCounterClockwiseEffect());
7666
case ImageSymbolEffects.WiggleDown:
77-
// @ts-expect-error added on iOS 18
7867
return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleDownEffect());
7968
case ImageSymbolEffects.WiggleForward:
80-
// @ts-expect-error added on iOS 18
8169
return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleForwardEffect());
8270
case ImageSymbolEffects.WiggleUp:
83-
// @ts-expect-error added on iOS 18
8471
return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleUpEffect());
8572
case ImageSymbolEffects.WiggleLeft:
86-
// @ts-expect-error added on iOS 18
8773
return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleLeftEffect());
8874
case ImageSymbolEffects.WiggleRight:
89-
// @ts-expect-error added on iOS 18
9075
return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleRightEffect());
9176
}
9277

9378
return null;
9479
}
95-
};
80+
}

packages/types-ios/src/lib/ios/objc-x86_64/objc!NativeScriptEmbedder.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,11 @@ declare var NativeScriptEmbedderDelegate: {
2828

2929
prototype: NativeScriptEmbedderDelegate;
3030
};
31+
32+
declare class NativeScriptViewFactory extends NSObject {
33+
static getKeyWindow(): UIWindow;
34+
static shared: NativeScriptViewFactory;
35+
views: NSMutableDictionary<string, any>;
36+
viewCreator: (id: string, ctrl: UIViewController) => void;
37+
viewDestroyer: (id: string) => void;
38+
}

0 commit comments

Comments
 (0)