Files
NativeScript/packages/core/utils/native-helper.ios.ts
Nathan Walker 248a85f6e0 chore: cleanup
2025-07-19 01:18:12 -07:00

384 lines
11 KiB
TypeScript

import { platformCheck } from './platform-check';
import { getClass, isNullOrUndefined, numberHasDecimals, numberIs64Bit } from './types';
import { Color } from '../color';
import { Trace } from '../trace';
import { CORE_ANIMATION_DEFAULTS, getDurationWithDampingFromSpring } from './animation-helpers';
import { SDK_VERSION } from './constants';
export function dataDeserialize(nativeData?: any) {
if (isNullOrUndefined(nativeData)) {
// some native values will already be js null values
// calling types.getClass below on null/undefined will cause crash
return null;
} else {
switch (getClass(nativeData)) {
case 'NSNull':
return null;
case 'NSMutableDictionary':
case 'NSDictionary': {
const obj = {};
const length = nativeData.count;
const keysArray = nativeData.allKeys as NSArray<any>;
for (let i = 0; i < length; i++) {
const nativeKey = keysArray.objectAtIndex(i);
obj[nativeKey] = dataDeserialize(nativeData.objectForKey(nativeKey));
}
return obj;
}
case 'NSMutableArray':
case 'NSArray': {
const array = [];
const len = nativeData.count;
for (let i = 0; i < len; i++) {
array[i] = dataDeserialize(nativeData.objectAtIndex(i));
}
return array;
}
default:
return nativeData;
}
}
}
export function dataSerialize(data: any, wrapPrimitives: boolean = false) {
switch (typeof data) {
case 'string':
case 'boolean': {
return data;
}
case 'number': {
const hasDecimals = numberHasDecimals(data);
if (numberIs64Bit(data)) {
if (hasDecimals) {
return NSNumber.alloc().initWithDouble(data);
} else {
return NSNumber.alloc().initWithLongLong(data);
}
} else {
if (hasDecimals) {
return NSNumber.alloc().initWithFloat(data);
} else {
return data;
}
}
}
case 'object': {
if (data instanceof Date) {
return NSDate.dateWithTimeIntervalSince1970(data.getTime() / 1000);
}
if (!data) {
return null;
}
if (Array.isArray(data)) {
return NSArray.arrayWithArray(data.map((el) => dataSerialize(el, wrapPrimitives)).filter((el) => el !== null));
}
const node = Object.fromEntries(
Object.entries(data)
.map(([key, value]) => [key, dataSerialize(value, wrapPrimitives)])
.filter(([, value]) => value !== null),
);
// cast to any avoids signature overload on tsc build
return NSDictionary.dictionaryWithDictionary(node as any);
}
default:
return null;
}
}
function getCurrentAppPath(): string {
if (!global.__dirname) {
global.__dirname = typeof __dirname !== 'undefined' ? __dirname : import.meta.dirname;
}
const currentDir = global.__dirname;
const tnsModulesIndex = currentDir.indexOf('/tns_modules');
// Module not hosted in ~/tns_modules when bundled. Use current dir.
let appPath = currentDir;
if (tnsModulesIndex !== -1) {
// Strip part after tns_modules to obtain app root
appPath = currentDir.substring(0, tnsModulesIndex);
}
return appPath;
}
function joinPaths(...paths: string[]): string {
if (!paths || paths.length === 0) {
return '';
}
return NSString.stringWithString(NSString.pathWithComponents(<any>paths)).stringByStandardizingPath;
}
declare let UIImagePickerControllerSourceType: any;
const radToDeg = Math.PI / 180;
function isOrientationLandscape(orientation: number) {
return orientation === UIDeviceOrientation.LandscapeLeft /* 3 */ || orientation === UIDeviceOrientation.LandscapeRight /* 4 */;
}
function openFileAtRootModule(filePath: string): boolean {
try {
const appPath = getCurrentAppPath();
const path = isRealDevice() ? filePath.replace('~', appPath) : filePath;
const controller = UIDocumentInteractionController.interactionControllerWithURL(NSURL.fileURLWithPath(path));
controller.delegate = createUIDocumentInteractionControllerDelegate();
return controller.presentPreviewAnimated(true);
} catch (e) {
Trace.write('Error in openFile', Trace.categories.Error, Trace.messageType.error);
}
return false;
}
// TODO: remove for NativeScript 9.0
export function getter<T>(_this: any, property: T | { (): T }): T {
console.log('utils.ios.getter() is deprecated; use the respective native property instead');
if (typeof property === 'function') {
return (<{ (): T }>property).call(_this);
} else {
return <T>property;
}
}
namespace collections {
export function jsArrayToNSArray<T>(str: T[]): NSArray<T> {
return NSArray.arrayWithArray(str);
}
export function nsArrayToJSArray<T>(a: NSArray<T>): Array<T> {
const arr = [];
if (a !== undefined) {
const count = a.count;
for (let i = 0; i < count; i++) {
arr.push(a.objectAtIndex(i));
}
}
return arr;
}
}
function getRootViewController(): UIViewController {
const win = getWindow();
let vc = win && win.rootViewController;
while (vc && vc.presentedViewController) {
vc = vc.presentedViewController;
}
return vc;
}
export function getWindow(): UIWindow {
let window: UIWindow;
if (SDK_VERSION >= 15 && typeof NativeScriptViewFactory !== 'undefined') {
// UIWindowScene.keyWindow is only available 15+
window = NativeScriptViewFactory.getKeyWindow();
}
if (window) {
return window;
}
const app = UIApplication.sharedApplication;
if (!app) {
return;
}
return app.keyWindow || (app.windows && app.windows.count > 0 && app.windows.objectAtIndex(0));
}
function getMainScreen(): UIScreen {
const window = getWindow();
return window ? window.screen : UIScreen.mainScreen;
}
function setWindowBackgroundColor(value: string) {
const win = getWindow();
if (win) {
const bgColor = new Color(value);
win.backgroundColor = bgColor.ios;
const rootVc = getRootViewController();
if (rootVc?.view) {
rootVc.view.backgroundColor = bgColor.ios;
}
}
}
function isLandscape(): boolean {
console.log('utils.ios.isLandscape() is deprecated; use application.orientation instead');
const deviceOrientation = UIDevice.currentDevice.orientation;
const statusBarOrientation = UIApplication.sharedApplication.statusBarOrientation;
const isDeviceOrientationLandscape = isOrientationLandscape(deviceOrientation);
const isStatusBarOrientationLandscape = isOrientationLandscape(statusBarOrientation);
return isDeviceOrientationLandscape || isStatusBarOrientationLandscape;
}
/**
* @deprecated use Utils.SDK_VERSION instead which is a float of the {major}.{minor} verison
*/
const MajorVersion = NSString.stringWithString(UIDevice.currentDevice.systemVersion).intValue;
export function openFile(filePath: string): boolean {
console.log('utils.ios.openFile() is deprecated; use utils.openFile() instead');
return openFileAtRootModule(filePath);
}
function getVisibleViewController(rootViewController: UIViewController): UIViewController {
let viewController = rootViewController;
while (viewController && viewController.presentedViewController) {
viewController = viewController.presentedViewController;
}
return viewController;
}
function applyRotateTransform(transform: CATransform3D, x: number, y: number, z: number): CATransform3D {
if (x) {
transform = CATransform3DRotate(transform, x * radToDeg, 1, 0, 0);
}
if (y) {
transform = CATransform3DRotate(transform, y * radToDeg, 0, 1, 0);
}
if (z) {
transform = CATransform3DRotate(transform, z * radToDeg, 0, 0, 1);
}
return transform;
}
function createUIDocumentInteractionControllerDelegate(): NSObject {
@NativeClass
class UIDocumentInteractionControllerDelegateImpl extends NSObject implements UIDocumentInteractionControllerDelegate {
public static ObjCProtocols = [UIDocumentInteractionControllerDelegate];
public getViewController(): UIViewController {
return getWindow().rootViewController;
}
public documentInteractionControllerViewControllerForPreview(controller: UIDocumentInteractionController) {
return this.getViewController();
}
public documentInteractionControllerViewForPreview(controller: UIDocumentInteractionController) {
return this.getViewController().view;
}
public documentInteractionControllerRectForPreview(controller: UIDocumentInteractionController): CGRect {
return this.getViewController().view.frame;
}
}
return new UIDocumentInteractionControllerDelegateImpl();
}
export function isRealDevice() {
try {
if (NSProcessInfo.processInfo.environment.valueForKey('SIMULATOR_DEVICE_NAME')) {
return false;
}
return true;
} catch (e) {
return true;
}
}
function printCGRect(rect: CGRect) {
if (rect) {
return `CGRect(${rect.origin.x} ${rect.origin.y} ${rect.size.width} ${rect.size.height})`;
}
}
function snapshotView(view: UIView, scale: number): UIImage {
if (view instanceof UIImageView) {
return view.image;
}
// console.log('snapshotView view.frame:', printRect(view.frame));
const originalOpacity = view.layer.opacity;
view.layer.opacity = originalOpacity > 0 ? originalOpacity : 1;
UIGraphicsBeginImageContextWithOptions(CGSizeMake(view.frame.size.width, view.frame.size.height), false, scale);
view.layer.renderInContext(UIGraphicsGetCurrentContext());
const image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
setTimeout(() => {
// ensure set back properly on next tick
view.layer.opacity = originalOpacity;
});
return image;
}
function copyLayerProperties(view: UIView, toView: UIView, customProperties?: { view?: Array<keyof UIView>; layer?: Array<keyof CALayer> }) {
const viewPropertiesToMatch: Array<keyof UIView> = customProperties?.view || ['backgroundColor'];
const layerPropertiesToMatch: Array<keyof CALayer> = customProperties?.layer || ['cornerRadius', 'borderWidth', 'borderColor'];
viewPropertiesToMatch.forEach((property) => {
if (view[property] !== toView[property]) {
// console.log('| -- matching view property:', property);
view[property as any] = toView[property];
}
});
layerPropertiesToMatch.forEach((property) => {
if (view.layer[property] !== toView.layer[property]) {
// console.log('| -- matching layer property:', property);
view.layer[property as any] = toView.layer[property];
}
});
}
function animateWithSpring(options?: { tension?: number; friction?: number; mass?: number; delay?: number; velocity?: number; animateOptions?: UIViewAnimationOptions; animations?: () => void; completion?: (finished?: boolean) => void }) {
// for convenience, default spring settings are provided
const opt = {
...CORE_ANIMATION_DEFAULTS.spring,
delay: 0,
animateOptions: null,
animations: null,
completion: null,
...(options || {}),
};
const { duration, damping } = getDurationWithDampingFromSpring(opt);
if (duration === 0) {
UIView.animateWithDurationAnimationsCompletion(0, opt.animations, opt.completion);
return;
}
UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(duration, opt.delay, damping, opt.velocity, opt.animateOptions, opt.animations, opt.completion);
}
// these don't exist on iOS. Stub them to empty functions.
export const ad = platformCheck('Utils.ad');
export const android = platformCheck('Utils.android');
export const ios = {
collections,
createUIDocumentInteractionControllerDelegate,
getCurrentAppPath,
getRootViewController,
getVisibleViewController,
getWindow,
getMainScreen,
setWindowBackgroundColor,
isLandscape,
applyRotateTransform,
snapshotView,
joinPaths,
printCGRect,
copyLayerProperties,
animateWithSpring,
MajorVersion,
};
/**
* @deprecated Use `Utils.ios` instead.
*/
export const iOSNativeHelper = ios;