UNPKG

expo-splash-screen

Version:

Provides a module to allow keeping the native Splash Screen visible until you choose to hide it.

547 lines (489 loc) 14.1 kB
import crypto from 'crypto'; import { Builder, Parser } from 'xml2js'; export type IBBoolean = 'YES' | 'NO' | boolean; export type IBItem< H extends Record<string, any>, B extends Record<string, any[]> = { [key: string]: any }, > = { $: H; } & B; export type Rect = { key: string; x: number; y: number; width: number; height: number; }; export type IBRect = IBItem<Rect>; export type IBAutoresizingMask = IBItem<{ /** @example `autoresizingMask` */ key: string; flexibleMaxX: IBBoolean; flexibleMaxY: IBBoolean; }>; /** @example `<color key="textColor" systemColor="linkColor"/>` */ export type IBColor = IBItem< { /** @example `textColor` */ key: string; } & ( | /** Custom color */ { /** @example `0.86584504117670746` */ red: number; /** @example `0.26445041990630447` */ green: number; /** @example `0.3248577810203549` */ blue: number; /** @example `1` */ alpha: number; colorSpace: 'custom' | string; customColorSpace: 'displayP3' | 'sRGB' | string; } /** Built-in color */ | { systemColor: 'linkColor' | string; } ) >; export type IBFontDescription = IBItem<{ /** @example `fontDescription` */ key: string; /** Font size */ pointSize: number; /** Custom font */ name?: 'HelveticaNeue' | string; family?: 'Helvetica Neue' | string; /** Built-in font */ type?: 'system' | 'boldSystem' | 'UICTFontTextStyleCallout' | 'UICTFontTextStyleBody' | string; }>; export type ImageContentMode = 'scaleAspectFit' | 'scaleAspectFill'; export type ConstraintAttribute = 'top' | 'bottom' | 'trailing' | 'leading' | 'centerX' | 'centerY'; export type IBImageView = IBItem< { id: string; userLabel: string; image: string; clipsSubviews?: IBBoolean; userInteractionEnabled: IBBoolean; contentMode: IBContentMode; horizontalHuggingPriority?: number; verticalHuggingPriority?: number; insetsLayoutMarginsFromSafeArea?: IBBoolean; translatesAutoresizingMaskIntoConstraints?: IBBoolean; }, { rect: IBRect[]; } >; export type IBLabel = IBItem< { id: string; /** The main value. */ text: string; opaque: IBBoolean; fixedFrame: IBBoolean; textAlignment?: IBTextAlignment; lineBreakMode: | 'clip' | 'characterWrap' | 'wordWrap' | 'headTruncation' | 'middleTruncation' | 'tailTruncation'; baselineAdjustment?: 'none' | 'alignBaselines'; adjustsFontSizeToFit: IBBoolean; userInteractionEnabled: IBBoolean; contentMode: IBContentMode; horizontalHuggingPriority: number; verticalHuggingPriority: number; translatesAutoresizingMaskIntoConstraints?: IBBoolean; }, { /** @example `<rect key="frame" x="175" y="670" width="35" height="17"/>` */ rect: IBRect[]; /** @example `<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>` */ autoresizingMask?: IBAutoresizingMask[]; /** @example `<fontDescription key="fontDescription" type="system" pointSize="19"/>` */ fontDescription?: IBFontDescription[]; /** @example `<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>` */ color?: IBColor[]; nil?: IBItem<{ /** @example `textColor` `highlightedColor` */ key: string; }>[]; } >; export type IBTextAlignment = 'left' | 'center' | 'right' | 'justified' | 'natural'; export type IBContentMode = string | 'left' | 'scaleAspectFill'; export type IBConstraint = IBItem<{ firstItem: string; firstAttribute: ConstraintAttribute; secondItem: string; secondAttribute: ConstraintAttribute; constant?: number; id: string; }>; export type IBViewController = IBItem< { id: string; placeholderIdentifier?: string; userLabel: string; sceneMemberID: string; }, { view: IBItem< { id: string; key: string; userInteractionEnabled: IBBoolean; contentMode: string | 'scaleToFill'; insetsLayoutMarginsFromSafeArea: IBBoolean; userLabel: string; }, { rect: IBRect[]; autoresizingMask: IBItem<{ key: string; flexibleMaxX: IBBoolean; flexibleMaxY: IBBoolean; }>[]; subviews: IBItem< object, { imageView: IBImageView[]; label: IBLabel[]; } >[]; color: IBItem<{ key: string | 'backgroundColor'; name?: string; systemColor?: string | 'systemBackgroundColor'; red?: string; green?: string; blue?: string; alpha?: string; colorSpace?: string; customColorSpace?: string; }>[]; constraints: IBItem< object, { constraint: IBConstraint[]; } >[]; viewLayoutGuide: IBItem<{ id: string; key: string | 'safeArea'; }>[]; } >[]; } >; export type IBPoint = IBItem<{ key: string | 'canvasLocation'; x: number; y: number; }>; export type IBScene = IBItem< { sceneID: string }, { objects: { viewController: IBViewController[]; placeholder: IBItem<{ id: string; placeholderIdentifier?: string; userLabel: string; sceneMemberID: string; }>[]; }[]; point: IBPoint[]; } >; export type IBResourceImage = IBItem<{ name: string; width: number; height: number; }>; export type IBResourceNamedColor = IBItem<{ name?: string; systemColor?: string | 'systemBackgroundColor'; red?: string; green?: string; blue?: string; alpha?: string; colorSpace?: string; customColorSpace?: string; }>; export type IBDevice = IBItem<{ id: string; orientation: string | 'portrait'; appearance: string | 'light'; }>; export type IBSplashScreenDocument = { document: IBItem< { type: 'com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB' | string; version: '3.0' | string; toolsVersion: number; targetRuntime: 'iOS.CocoaTouch' | string; propertyAccessControl: 'none' | string; useAutolayout: IBBoolean; launchScreen: IBBoolean; useTraitCollections: IBBoolean; useSafeAreas: IBBoolean; colorMatched: IBBoolean; initialViewController: string; }, { device: IBDevice[]; dependencies: unknown[]; scenes: { scene: IBScene[]; }[]; resources: { image: IBResourceImage[]; namedColor?: IBItem<{ name: string }, { color: IBResourceNamedColor[] }>[]; }[]; } >; }; export function createConstraint( [firstItem, firstAttribute]: [string, ConstraintAttribute], [secondItem, secondAttribute]: [string, ConstraintAttribute], constant?: number ): IBConstraint { return { $: { firstItem, firstAttribute, secondItem, secondAttribute, constant, // Prevent updating between runs id: createConstraintId(firstItem, firstAttribute, secondItem, secondAttribute), }, }; } export function createConstraintId(...attributes: string[]) { return crypto.createHash('sha1').update(attributes.join('-')).digest('hex'); } const IMAGE_ID = 'EXPO-SplashScreen'; const CONTAINER_ID = 'EXPO-ContainerView'; export function removeImageFromSplashScreen( xml: IBSplashScreenDocument, { imageName }: { imageName: string } ) { const mainView = xml.document.scenes[0]?.scene[0]?.objects[0]?.viewController[0]?.view[0]; if (mainView != null) { if (mainView.subviews[0] != null) { removeExisting(mainView.subviews[0].imageView, IMAGE_ID); } // Remove Constraints getAbsoluteConstraints(IMAGE_ID, CONTAINER_ID).forEach((constraint) => { // <constraint firstItem="EXPO-SplashScreen" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="2VS-Uz-0LU"/> if (mainView.constraints[0] != null) { removeExisting(mainView.constraints[0].constraint, constraint); } }); } // Remove resource if (xml.document.resources[0] != null) { xml.document.resources[0].image = xml.document.resources[0].image ?? []; const imageSection = xml.document.resources[0].image; const existingImageIndex = imageSection.findIndex((image) => image.$.name === imageName); if (existingImageIndex && existingImageIndex > -1) { imageSection?.splice(existingImageIndex, 1); } } return xml; } function getAbsoluteConstraints(childId: string, parentId: string, legacy: boolean = false) { if (legacy) { return [ createConstraint([childId, 'top'], [parentId, 'top']), createConstraint([childId, 'leading'], [parentId, 'leading']), createConstraint([childId, 'trailing'], [parentId, 'trailing']), createConstraint([childId, 'bottom'], [parentId, 'bottom']), ]; } return [ createConstraint([childId, 'centerX'], [parentId, 'centerX']), createConstraint([childId, 'centerY'], [parentId, 'centerY']), ]; } export function applyImageToSplashScreenXML( xml: IBSplashScreenDocument, { imageName, contentMode, backgroundColor = '#ffffff', enableFullScreenImage, imageWidth = 100, }: { imageName: string; contentMode: ImageContentMode; backgroundColor?: string; enableFullScreenImage: boolean; imageWidth?: number; } ): IBSplashScreenDocument { const mainView = xml.document.scenes[0]?.scene[0]?.objects[0]?.viewController[0]?.view[0]; const rect = mainView?.rect[0]; const width = enableFullScreenImage ? 414 : imageWidth; const height = enableFullScreenImage ? 736 : imageWidth; const x = enableFullScreenImage || rect == null ? 0 : (rect.$.width - width) / 2; const y = enableFullScreenImage || rect == null ? 0 : (rect.$.height - height) / 2; const imageView: IBImageView = { $: { id: IMAGE_ID, userLabel: imageName, image: imageName, contentMode, clipsSubviews: true, userInteractionEnabled: false, translatesAutoresizingMaskIntoConstraints: false, }, rect: [ { $: { key: 'frame', x, y, width, height, }, }, ], }; if (mainView != null) { // Add ImageView if (mainView.subviews[0] != null) { ensureUniquePush(mainView.subviews[0].imageView, imageView); } if (mainView.constraints[0] != null) { mainView.constraints[0].constraint = []; } // Add Constraints getAbsoluteConstraints(IMAGE_ID, CONTAINER_ID, enableFullScreenImage).forEach( (constraint: IBConstraint) => { if (mainView.constraints[0] != null) { ensureUniquePush(mainView.constraints[0].constraint, constraint); } } ); // Clear existing color mainView.color = []; // Add background color const colorSection = mainView.color; colorSection.push({ $: { key: 'backgroundColor', name: 'SplashScreenBackground', }, }); } if (xml.document.resources[0] != null) { // Clear existing images xml.document.resources[0].image = []; // Add resource const imageSection = xml.document.resources[0].image; imageSection.push({ $: { name: imageName, width, height, }, }); // Clear existing named colors xml.document.resources[0].namedColor = []; const namedColorSection = xml.document.resources[0].namedColor; // Add background named color reference const color = parseColor(backgroundColor); namedColorSection.push({ $: { name: 'SplashScreenBackground', }, color: [ { $: { alpha: '1.000', blue: color.rgb.blue, green: color.rgb.green, red: color.rgb.red, customColorSpace: 'sRGB', colorSpace: 'custom', }, }, ], }); } return xml; } /** * IB does not allow two items to have the same ID. * This method will add an item by first removing any existing item with the same `$.id`. */ export function ensureUniquePush<TItem extends { $: { id: string } }>(array: TItem[], item: TItem) { if (!array) return array; removeExisting(array, item); array.push(item); return array; } export function removeExisting<TItem extends { $: { id: string } }>( array: TItem[], item: TItem | string ) { const id = typeof item === 'string' ? item : item.$?.id; const existingItem = array?.findIndex((existingItem) => existingItem.$.id === id); if (existingItem > -1) { array.splice(existingItem, 1); } return array; } // Attempt to copy Xcode formatting. export function toString(xml: any): string { const builder = new Builder({ // @ts-expect-error: untyped preserveChildrenOrder: true, xmldec: { version: '1.0', encoding: 'UTF-8', }, renderOpts: { pretty: true, indent: ' ', }, }); return builder.buildObject(xml); } /** Parse string contents into an object. */ export function toObjectAsync(contents: string) { return new Parser().parseStringPromise(contents); } // Function taken from react-native-bootsplash export const parseColor = (value: string): Color => { const color = value.toUpperCase().replace(/[^0-9A-F]/g, ''); if (color.length !== 3 && color.length !== 6) { console.error(`"${value}" value is not a valid hexadecimal color.`); process.exit(1); } const hex = color.length === 3 ? '#' + color[0] + color[0] + color[1] + color[1] + color[2] + color[2] : '#' + color; const rgb: Color['rgb'] = { red: (parseInt('' + hex[1] + hex[2], 16) / 255).toPrecision(15), green: (parseInt('' + hex[3] + hex[4], 16) / 255).toPrecision(15), blue: (parseInt('' + hex[5] + hex[6], 16) / 255).toPrecision(15), }; return { hex, rgb }; }; type Color = { hex: string; rgb: { red: string; green: string; blue: string; }; };