feat(ios): SF Symbol scale support via iosSymbolScale (#10569)

This commit is contained in:
Nathan Walker
2024-07-02 16:26:40 -07:00
committed by GitHub
parent 893b858b80
commit 80f3ff2042
12 changed files with 119 additions and 55 deletions

View File

@ -10,10 +10,10 @@ export function navigatingTo(args: EventData) {
export class DemoModel extends Observable { export class DemoModel extends Observable {
addingPhoto = false; addingPhoto = false;
symbolWiggleEffect: ImageSymbolEffects.Wiggle; symbolWiggleEffect = ImageSymbolEffects.Wiggle;
symbolBounceEffect: ImageSymbolEffects.Bounce; symbolBounceEffect = ImageSymbolEffects.Bounce;
symbolBreathEffect: ImageSymbolEffects.Breathe; symbolBreathEffect = ImageSymbolEffects.Breathe;
symbolRotateEffect: ImageSymbolEffects.Rotate; symbolRotateEffect = ImageSymbolEffects.Rotate;
pickImage() { pickImage() {
const context = create({ const context = create({

View File

@ -13,16 +13,23 @@
<ios> <ios>
<!-- SF Symbols with Effects --> <!-- SF Symbols with Effects -->
<ContentView height="1" width="100%" backgroundColor="#efefef" margin="10"></ContentView> <ContentView height="1" width="100%" backgroundColor="#efefef" margin="10"></ContentView>
<GridLayout rows="auto,auto,auto" columns="*,*"> <GridLayout rows="auto,auto,auto,auto,auto" columns="*,*">
<Image src="sys://photo.on.rectangle.angled" width="100" tintColor="green" symbolEffect="{{symbolWiggleEffect}}" padding="8"/> <Image src="sys://photo.on.rectangle.angled" width="100" tintColor="green" iosSymbolEffect="{{symbolBounceEffect}}" padding="8"/>
<Image col="1" src="sys://steeringwheel.and.hands" width="100" tintColor="black" symbolEffect="{{symbolWiggleEffect}}" padding="8" /> <Image col="1" src="sys://photo.on.rectangle.angled" width="100" tintColor="green" iosSymbolEffect="{{symbolBounceEffect}}" iosSymbolScale="small" padding="8" />
<Image row="1" src="sys://airpods.pro.chargingcase.wireless.radiowaves.left.and.right.fill" width="100" symbolEffect="{{symbolBounceEffect}}" padding="8" /> <Image row="1" src="sys://photo.on.rectangle.angled" width="100" tintColor="green" iosSymbolEffect="{{symbolBounceEffect}}" iosSymbolScale="medium" padding="8"/>
<Image row="1" col="1" src="sys://lungs.fill" width="100" symbolEffect="{{symbolBreathEffect}}" padding="8" /> <Image row="1" col="1" src="sys://photo.on.rectangle.angled" width="100" tintColor="green" iosSymbolEffect="{{symbolBounceEffect}}" iosSymbolScale="large" padding="8"/>
<Image row="2" src="sys://airpods.pro.chargingcase.wireless.radiowaves.left.and.right.fill" width="100" iosSymbolEffect="{{symbolBounceEffect}}" padding="8" />
<Image row="2" col="1" src="sys://lungs.fill" width="100" iosSymbolEffect="{{symbolBreathEffect}}" padding="8" />
<Image row="2" src="sys://clock.arrow.trianglehead.2.counterclockwise.rotate.90" width="100" symbolEffect="{{symbolRotateEffect}}" padding="8" /> <Image row="3" src="sys://clock.arrow.trianglehead.2.counterclockwise.rotate.90" width="100" iosSymbolEffect="{{symbolRotateEffect}}" padding="8" />
<Image row="2" col="1" src="sys://square.and.arrow.up" width="100" symbolEffect="{{symbolWiggleEffect}}" padding="8" /> <Image row="3" col="1" src="sys://square.and.arrow.up" width="100" iosSymbolEffect="{{symbolWiggleEffect}}" padding="8" />
<Image row="4" src="sys://steeringwheel.and.hands" width="100" tintColor="black" iosSymbolEffect="{{symbolWiggleEffect}}" padding="8" />
<Image row="4" col="1" src="sys://steeringwheel.and.hands" width="100" tintColor="black" iosSymbolEffect="{{symbolWiggleEffect}}" iosSymbolScale="large" padding="8" />
</GridLayout> </GridLayout>
</ios> </ios>

View File

@ -1,5 +1,5 @@
// Definitions. // Definitions.
import { ImageSource as ImageSourceDefinition } from '.'; import { ImageSource as ImageSourceDefinition, iosSymbolScaleType } from '.';
import { ImageAsset } from '../image-asset'; import { ImageAsset } from '../image-asset';
import * as httpModule from '../http'; import * as httpModule from '../http';
@ -149,6 +149,10 @@ export class ImageSource implements ImageSourceDefinition {
return ImageSource.fromFileSync(path); return ImageSource.fromFileSync(path);
} }
static iosSymbolScaleFor(scale: iosSymbolScaleType): number {
return 0;
}
static fromSystemImageSync(name: string): ImageSource { static fromSystemImageSync(name: string): ImageSource {
return ImageSource.fromResourceSync(name); return ImageSource.fromResourceSync(name);
} }

View File

@ -54,17 +54,23 @@ export class ImageSource {
*/ */
static fromResource(name: string): Promise<ImageSource>; static fromResource(name: string): Promise<ImageSource>;
/**
* (iOS only) Get system symbol scale
* @param scale symbol scale type
*/
static iosSymbolScaleFor(scale: iosSymbolScaleType): number;
/** /**
* Loads this instance from the specified system image name. * Loads this instance from the specified system image name.
* @param name the name of the system image * @param name the name of the system image
*/ */
static fromSystemImageSync(name: string): ImageSource; static fromSystemImageSync(name: string, scale?: iosSymbolScaleType): ImageSource;
/** /**
* Loads this instance from the specified system image name asynchronously. * Loads this instance from the specified system image name asynchronously.
* @param name the name of the system image * @param name the name of the system image
*/ */
static fromSystemImage(name: string): Promise<ImageSource>; static fromSystemImage(name: string, scale?: iosSymbolScaleType): Promise<ImageSource>;
/** /**
* Loads this instance from the specified file. * Loads this instance from the specified file.
@ -259,6 +265,12 @@ export class ImageSource {
resizeAsync(maxSize: number, options?: any): Promise<ImageSource>; resizeAsync(maxSize: number, options?: any): Promise<ImageSource>;
} }
/**
* iOS only
* SF Symbol scale
*/
export type iosSymbolScaleType = 'default' | 'small' | 'medium' | 'large';
/** /**
* @deprecated Use ImageSource.fromAsset() instead. * @deprecated Use ImageSource.fromAsset() instead.
* Creates a new ImageSource instance and loads it from the specified image asset asynchronously. * Creates a new ImageSource instance and loads it from the specified image asset asynchronously.

View File

@ -1,5 +1,5 @@
// Definitions. // Definitions.
import { ImageSource as ImageSourceDefinition } from '.'; import { ImageSource as ImageSourceDefinition, iosSymbolScaleType } from '.';
import { ImageAsset } from '../image-asset'; import { ImageAsset } from '../image-asset';
import * as httpModule from '../http'; import * as httpModule from '../http';
import { Font } from '../ui/styling/font'; import { Font } from '../ui/styling/font';
@ -73,16 +73,39 @@ export class ImageSource implements ImageSourceDefinition {
return http.getImage(url); return http.getImage(url);
} }
static fromSystemImageSync(name: string): ImageSource { static iosSystemScaleFor(scale: iosSymbolScaleType) {
switch (scale) {
case 'small':
return UIImageSymbolScale.Small;
case 'medium':
return UIImageSymbolScale.Medium;
case 'large':
return UIImageSymbolScale.Large;
default:
return UIImageSymbolScale.Default;
}
}
static fromSystemImageSync(name: string, scale?: iosSymbolScaleType): ImageSource {
if (scale) {
const image = UIImage.systemImageNamedWithConfiguration(name, UIImageSymbolConfiguration.configurationWithScale(ImageSource.iosSystemScaleFor(scale)));
return image ? new ImageSource(image) : null;
} else {
const image = UIImage.systemImageNamed(name); const image = UIImage.systemImageNamed(name);
return image ? new ImageSource(image) : null; return image ? new ImageSource(image) : null;
} }
}
static fromSystemImage(name: string): Promise<ImageSource> { static fromSystemImage(name: string, scale?: iosSymbolScaleType): Promise<ImageSource> {
return new Promise<ImageSource>((resolve, reject) => { return new Promise<ImageSource>((resolve, reject) => {
try { try {
const image = UIImage.systemImageNamed(name); let image: UIImage;
if (scale) {
image = UIImage.systemImageNamedWithConfiguration(name, UIImageSymbolConfiguration.configurationWithScale(ImageSource.iosSystemScaleFor(scale)));
} else {
image = UIImage.systemImageNamed(name);
}
if (image) { if (image) {
resolve(new ImageSource(image)); resolve(new ImageSource(image));
} else { } else {

View File

@ -2,6 +2,7 @@
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!CFNetwork.d.ts" /> /// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!CFNetwork.d.ts" />
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!CoreText.d.ts" /> /// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!CoreText.d.ts" />
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!Darwin.d.ts" /> /// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!Darwin.d.ts" />
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!DarwinFoundation.d.ts" />
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!Symbols.d.ts" /> /// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!Symbols.d.ts" />
/// <reference path="../types-android/src/lib/android-29.d.ts" /> /// <reference path="../types-android/src/lib/android-29.d.ts" />
/// <reference path="./platforms/ios/typings/objc!MaterialComponents.d.ts" /> /// <reference path="./platforms/ios/typings/objc!MaterialComponents.d.ts" />

View File

@ -901,8 +901,8 @@ export class View extends ViewCommon implements ViewDefinition {
let notification: number; let notification: number;
let args: string | UIView | null = this.nativeViewProtected; let args: string | UIView | null = this.nativeViewProtected;
if (typeof msg === 'string' && msg) { if (options?.message) {
args = msg; args = options.message;
} }
switch (options.iosNotificationType) { switch (options.iosNotificationType) {

View File

@ -3,7 +3,7 @@ import { View, CSSType } from '../core/view';
import { booleanConverter } from '../core/view-base'; import { booleanConverter } from '../core/view-base';
import { CoreTypes } from '../../core-types'; import { CoreTypes } from '../../core-types';
import { ImageAsset } from '../../image-asset'; import { ImageAsset } from '../../image-asset';
import { ImageSource } from '../../image-source'; import { ImageSource, iosSymbolScaleType } from '../../image-source';
import { isDataURI, isFontIconURI, isFileOrResourcePath, RESOURCE_PREFIX, SYSTEM_PREFIX } from '../../utils'; import { isDataURI, isFontIconURI, isFileOrResourcePath, RESOURCE_PREFIX, SYSTEM_PREFIX } from '../../utils';
import { Color } from '../../color'; import { Color } from '../../color';
import { Style } from '../styling/style'; import { Style } from '../styling/style';
@ -21,6 +21,7 @@ export abstract class ImageBase extends View implements ImageDefinition {
public loadMode: 'sync' | 'async'; public loadMode: 'sync' | 'async';
public decodeWidth: CoreTypes.LengthType; public decodeWidth: CoreTypes.LengthType;
public decodeHeight: CoreTypes.LengthType; public decodeHeight: CoreTypes.LengthType;
public iosSymbolScale: iosSymbolScaleType;
get tintColor(): Color { get tintColor(): Color {
return this.style.tintColor; return this.style.tintColor;
@ -86,10 +87,10 @@ export abstract class ImageBase extends View implements ImageDefinition {
} else if (value.indexOf(SYSTEM_PREFIX) === 0) { } else if (value.indexOf(SYSTEM_PREFIX) === 0) {
const sysPath = value.slice(SYSTEM_PREFIX.length); const sysPath = value.slice(SYSTEM_PREFIX.length);
if (sync) { if (sync) {
imageLoaded(ImageSource.fromSystemImageSync(sysPath)); imageLoaded(ImageSource.fromSystemImageSync(sysPath, this.iosSymbolScale));
} else { } else {
this.imageSource = null; this.imageSource = null;
ImageSource.fromSystemImage(sysPath).then(imageLoaded); ImageSource.fromSystemImage(sysPath, this.iosSymbolScale).then(imageLoaded);
} }
} else { } else {
if (sync) { if (sync) {
@ -196,4 +197,12 @@ export const iosSymbolEffectProperty = new Property<ImageBase, ImageSymbolEffect
}); });
iosSymbolEffectProperty.register(ImageBase); iosSymbolEffectProperty.register(ImageBase);
/**
* iOS only
*/
export const iosSymbolScaleProperty = new Property<ImageBase, iosSymbolScaleType>({
name: 'iosSymbolScale',
});
iosSymbolScaleProperty.register(ImageBase);
export { ImageSymbolEffect, ImageSymbolEffects }; export { ImageSymbolEffect, ImageSymbolEffects };

View File

@ -1,5 +1,5 @@
import { ImageBase, stretchProperty, imageSourceProperty, tintColorProperty, srcProperty, iosSymbolEffectProperty, ImageSymbolEffect, ImageSymbolEffects } from './image-common'; import { ImageBase, stretchProperty, imageSourceProperty, tintColorProperty, srcProperty, iosSymbolEffectProperty, ImageSymbolEffect, ImageSymbolEffects, iosSymbolScaleProperty } from './image-common';
import { ImageSource } from '../../image-source'; import { ImageSource, iosSymbolScaleType } from '../../image-source';
import { ImageAsset } from '../../image-asset'; import { ImageAsset } from '../../image-asset';
import { Color } from '../../color'; import { Color } from '../../color';
import { Trace } from '../../trace'; import { Trace } from '../../trace';
@ -192,8 +192,17 @@ export class Image extends ImageBase {
this._setNativeImage(value ? value.ios : null); this._setNativeImage(value ? value.ios : null);
} }
[srcProperty.setNative](value: string | ImageSource | ImageAsset) { private _setSrc(value: string | ImageSource | ImageAsset) {
this._createImageSourceFromSrc(value); this._createImageSourceFromSrc(value);
if (this.iosSymbolScale) {
// when applying symbol scale, contentMode must be center
// https://stackoverflow.com/a/65787627
this.nativeViewProtected.contentMode = UIViewContentMode.Center;
}
}
[srcProperty.setNative](value: string | ImageSource | ImageAsset) {
this._setSrc(value);
} }
[iosSymbolEffectProperty.setNative](value: ImageSymbolEffect | ImageSymbolEffects) { [iosSymbolEffectProperty.setNative](value: ImageSymbolEffect | ImageSymbolEffects) {
@ -201,10 +210,17 @@ export class Image extends ImageBase {
return; return;
} }
const symbol = typeof value === 'string' ? ImageSymbolEffect.fromSymbol(value) : value; const symbol = typeof value === 'string' ? ImageSymbolEffect.fromSymbol(value) : value;
if (symbol && symbol.effect) { if (symbol?.effect) {
// Note: https://developer.apple.com/documentation/symbols/symboleffectoptions/4197883-repeating
// Will want to move to https://developer.apple.com/documentation/symbols/nssymboleffectoptionsrepeatbehavior?language=objc as fallback once optionsWithRepeating is removed
this.nativeViewProtected.addSymbolEffectOptionsAnimatedCompletion(symbol.effect, symbol.options || NSSymbolEffectOptions.optionsWithRepeating(), true, symbol.completion || null); this.nativeViewProtected.addSymbolEffectOptionsAnimatedCompletion(symbol.effect, symbol.options || NSSymbolEffectOptions.optionsWithRepeating(), true, symbol.completion || null);
} else { } else {
this.nativeViewProtected.removeAllSymbolEffects(); this.nativeViewProtected.removeAllSymbolEffects();
} }
} }
[iosSymbolScaleProperty.setNative](value: iosSymbolScaleType) {
// reset src to configure scale
this._setSrc(this.src);
}
} }

View File

@ -1,9 +1,8 @@
import { ImageSymbolEffectCommon, ImageSymbolEffects } from './symbol-effects-common'; import { ImageSymbolEffectCommon } from './symbol-effects-common';
import type { ImageSymbolEffect as ImageSymbolEffectDefinition } from './symbol-effects.d.ts'; export { ImageSymbolEffects } from './symbol-effects-common';
export { ImageSymbolEffects };
export const ImageSymbolEffect: typeof ImageSymbolEffectDefinition = class ImageSymbolEffect extends ImageSymbolEffectCommon implements ImageSymbolEffectDefinition { export class ImageSymbolEffect extends ImageSymbolEffectCommon {
static fromSymbol(symbol: string): ImageSymbolEffectDefinition { static fromSymbol(symbol: string) {
return new ImageSymbolEffect(); return new ImageSymbolEffect();
} }
}; }

View File

@ -1,13 +1,13 @@
import { SDK_VERSION } from '../../utils/constants'; import { SDK_VERSION } from '../../utils/constants';
import { ImageSymbolEffectCommon, ImageSymbolEffects } from './symbol-effects-common'; import { ImageSymbolEffectCommon, ImageSymbolEffects } from './symbol-effects-common';
import type { ImageSymbolEffect as ImageSymbolEffectDefinition } from './symbol-effects.d.ts'; export { ImageSymbolEffects } from './symbol-effects-common';
export const ImageSymbolEffect: typeof ImageSymbolEffectDefinition = class ImageSymbolEffect extends ImageSymbolEffectCommon implements ImageSymbolEffectDefinition { export class ImageSymbolEffect extends ImageSymbolEffectCommon {
constructor(symbol: NSSymbolEffect) { constructor(symbol: NSSymbolEffect) {
super(); super();
this.effect = symbol; this.effect = symbol;
} }
static fromSymbol(symbol: string): ImageSymbolEffectDefinition | null { static fromSymbol(symbol: string): ImageSymbolEffect | null {
if (SDK_VERSION < 17) { if (SDK_VERSION < 17) {
return null; return null;
} }
@ -44,52 +44,37 @@ export const ImageSymbolEffect: typeof ImageSymbolEffectDefinition = class Image
if (SDK_VERSION < 18) { if (SDK_VERSION < 18) {
return null; return null;
} }
// TODO: remove ts-expect-error once we bump the types package
switch (symbol) { switch (symbol) {
case ImageSymbolEffects.Breathe: case ImageSymbolEffects.Breathe:
// @ts-expect-error added on iOS 18
return new ImageSymbolEffect(NSSymbolBreatheEffect.effect()); return new ImageSymbolEffect(NSSymbolBreatheEffect.effect());
case ImageSymbolEffects.BreathePlain: case ImageSymbolEffects.BreathePlain:
// @ts-expect-error added on iOS 18
return new ImageSymbolEffect(NSSymbolBreatheEffect.breathePlainEffect()); return new ImageSymbolEffect(NSSymbolBreatheEffect.breathePlainEffect());
case ImageSymbolEffects.Rotate: case ImageSymbolEffects.Rotate:
// @ts-expect-error added on iOS 18
return new ImageSymbolEffect(NSSymbolRotateEffect.effect()); return new ImageSymbolEffect(NSSymbolRotateEffect.effect());
case ImageSymbolEffects.RotateClockwise: case ImageSymbolEffects.RotateClockwise:
// @ts-expect-error added on iOS 18
return new ImageSymbolEffect(NSSymbolRotateEffect.rotateClockwiseEffect()); return new ImageSymbolEffect(NSSymbolRotateEffect.rotateClockwiseEffect());
case ImageSymbolEffects.RotateCounterClockwise: case ImageSymbolEffects.RotateCounterClockwise:
// @ts-expect-error added on iOS 18
return new ImageSymbolEffect(NSSymbolRotateEffect.rotateCounterClockwiseEffect()); return new ImageSymbolEffect(NSSymbolRotateEffect.rotateCounterClockwiseEffect());
case ImageSymbolEffects.Wiggle: case ImageSymbolEffects.Wiggle:
// @ts-expect-error added on iOS 18
return new ImageSymbolEffect(NSSymbolWiggleEffect.effect()); return new ImageSymbolEffect(NSSymbolWiggleEffect.effect());
case ImageSymbolEffects.WiggleBackward: case ImageSymbolEffects.WiggleBackward:
// @ts-expect-error added on iOS 18
return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleBackwardEffect()); return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleBackwardEffect());
case ImageSymbolEffects.WiggleClockwise: case ImageSymbolEffects.WiggleClockwise:
// @ts-expect-error added on iOS 18
return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleClockwiseEffect()); return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleClockwiseEffect());
case ImageSymbolEffects.WiggleCounterClockwise: case ImageSymbolEffects.WiggleCounterClockwise:
// @ts-expect-error added on iOS 18
return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleCounterClockwiseEffect()); return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleCounterClockwiseEffect());
case ImageSymbolEffects.WiggleDown: case ImageSymbolEffects.WiggleDown:
// @ts-expect-error added on iOS 18
return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleDownEffect()); return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleDownEffect());
case ImageSymbolEffects.WiggleForward: case ImageSymbolEffects.WiggleForward:
// @ts-expect-error added on iOS 18
return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleForwardEffect()); return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleForwardEffect());
case ImageSymbolEffects.WiggleUp: case ImageSymbolEffects.WiggleUp:
// @ts-expect-error added on iOS 18
return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleUpEffect()); return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleUpEffect());
case ImageSymbolEffects.WiggleLeft: case ImageSymbolEffects.WiggleLeft:
// @ts-expect-error added on iOS 18
return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleLeftEffect()); return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleLeftEffect());
case ImageSymbolEffects.WiggleRight: case ImageSymbolEffects.WiggleRight:
// @ts-expect-error added on iOS 18
return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleRightEffect()); return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleRightEffect());
} }
return null; return null;
} }
}; }

View File

@ -28,3 +28,11 @@ declare var NativeScriptEmbedderDelegate: {
prototype: NativeScriptEmbedderDelegate; prototype: NativeScriptEmbedderDelegate;
}; };
declare class NativeScriptViewFactory extends NSObject {
static getKeyWindow(): UIWindow;
static shared: NativeScriptViewFactory;
views: NSMutableDictionary<string, any>;
viewCreator: (id: string, ctrl: UIViewController) => void;
viewDestroyer: (id: string) => void;
}