feat: iOS 26 types with improvements (ActionBar, Switch) + .ns-{platform}-{sdkVersion} css root scoping (#10775)

This provides for better ability to target platform > sdk > majorVersion specific features.
For example, iOS 26 does not render titles when a background color is set on the actionbar. this allows that style to be overridden only on iOS 26 if desired.
This commit is contained in:
Nathan Walker
2025-08-07 12:06:10 -07:00
parent b35277c8f0
commit b6e1090b23
144 changed files with 28026 additions and 15652 deletions

View File

@@ -2,6 +2,7 @@ import { Application } from '../application';
import type { View } from '../ui/core/view';
import { AccessibilityServiceEnabledObservable } from './accessibility-service';
import { FontScaleCategory, getCurrentFontScale, getFontScaleCategory, VALID_FONT_SCALES } from './font-scale';
import { SDK_VERSION } from '../utils/constants';
// CSS-classes
const fontScaleExtraSmallCategoryClass = `a11y-fontscale-xs`;
@@ -14,6 +15,9 @@ const a11yServiceEnabledClass = `a11y-service-enabled`;
const a11yServiceDisabledClass = `a11y-service-disabled`;
const a11yServiceClasses = [a11yServiceEnabledClass, a11yServiceDisabledClass];
// SDK Version CSS classes
let sdkVersionClasses: string[] = [];
let accessibilityServiceObservable: AccessibilityServiceEnabledObservable;
let fontScaleCssClasses: Map<number, string>;
@@ -29,6 +33,31 @@ function ensureClasses() {
fontScaleCssClasses = new Map(VALID_FONT_SCALES.map((fs) => [fs, `a11y-fontscale-${Number(fs * 100).toFixed(0)}`]));
accessibilityServiceObservable = new AccessibilityServiceEnabledObservable();
// Initialize SDK version CSS class once
initializeSdkVersionClass();
}
function initializeSdkVersionClass(): void {
const majorVersion = Math.floor(SDK_VERSION);
sdkVersionClasses = [];
let platformPrefix = '';
if (__APPLE__) {
platformPrefix = __VISIONOS__ ? 'ns-visionos' : 'ns-ios';
} else if (__ANDROID__) {
platformPrefix = 'ns-android';
}
if (platformPrefix) {
// Add exact version class (e.g., .ns-ios-26 or .ns-android-36)
// this acts like 'gte' for that major version range
// e.g., if user wants iOS 27, they can add .ns-ios-27 specifiers
sdkVersionClasses.push(`${platformPrefix}-${majorVersion}`);
}
// Apply the SDK version classes to root views
applySdkVersionClass();
}
function applyRootCssClass(cssClasses: string[], newCssClass: string): void {
@@ -41,6 +70,32 @@ function applyRootCssClass(cssClasses: string[], newCssClass: string): void {
const rootModalViews = <Array<View>>rootView._getRootModalViews();
rootModalViews.forEach((rootModalView) => Application.applyCssClass(rootModalView, cssClasses, newCssClass));
// Note: SDK version classes are applied separately to avoid redundant work
}
function applySdkVersionClass(): void {
if (!sdkVersionClasses.length) {
return;
}
const rootView = Application.getRootView();
if (!rootView) {
return;
}
// Batch apply all SDK version classes to root view for better performance
const classesToAdd = sdkVersionClasses.filter((className) => !rootView.cssClasses.has(className));
classesToAdd.forEach((className) => rootView.cssClasses.add(className));
// Apply to modal views only if there are any
const rootModalViews = <Array<View>>rootView._getRootModalViews();
if (rootModalViews.length > 0) {
rootModalViews.forEach((rootModalView) => {
const modalClassesToAdd = sdkVersionClasses.filter((className) => !rootModalView.cssClasses.has(className));
modalClassesToAdd.forEach((className) => rootModalView.cssClasses.add(className));
});
}
}
function applyFontScaleToRootViews(): void {

View File

@@ -7,13 +7,12 @@ import { LinearGradient } from '../styling/linear-gradient';
import { colorProperty, backgroundInternalProperty, backgroundColorProperty, backgroundImageProperty } from '../styling/style-properties';
import { ios as iosViewUtils } from '../utils';
import { ImageSource } from '../../image-source';
import { layout, iOSNativeHelper, isFontIconURI } from '../../utils';
import { layout, isFontIconURI } from '../../utils';
import { SDK_VERSION } from '../../utils/constants';
import { accessibilityHintProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityValueProperty } from '../../accessibility/accessibility-properties';
export * from './action-bar-common';
const majorVersion = iOSNativeHelper.MajorVersion;
const UNSPECIFIED = layout.makeMeasureSpec(0, layout.UNSPECIFIED);
interface NSUINavigationBar extends UINavigationBar {
@@ -271,7 +270,7 @@ export class ActionBar extends ActionBarBase {
// show the one from the old page but the new page will still be visible (because we canceled EdgeBackSwipe gesutre)
// Consider moving this to new method and call it from - navigationControllerDidShowViewControllerAnimated.
const image = img ? img.imageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) : null;
if (majorVersion >= 15) {
if (SDK_VERSION >= 15) {
const appearance = this._getAppearance(navigationBar);
appearance.setBackIndicatorImageTransitionMaskImage(image, image);
this._updateAppearance(navigationBar, appearance);
@@ -304,6 +303,9 @@ export class ActionBar extends ActionBarBase {
navigationItem.accessibilityLabel = this.accessibilityLabel;
navigationItem.accessibilityLanguage = this.accessibilityLanguage;
navigationItem.accessibilityHint = this.accessibilityHint;
// Configure large title support for this navigation item
this.checkLargeTitleSupport(navigationItem);
}
private populateMenuItems(navigationItem: UINavigationItem) {
@@ -378,7 +380,7 @@ export class ActionBar extends ActionBarBase {
}
if (color) {
const titleTextColor = NSDictionary.dictionaryWithObjectForKey(color.ios, NSForegroundColorAttributeName);
if (majorVersion >= 15) {
if (SDK_VERSION >= 15) {
const appearance = this._getAppearance(navBar);
appearance.titleTextAttributes = titleTextColor;
}
@@ -398,7 +400,7 @@ export class ActionBar extends ActionBarBase {
}
const nativeColor = color instanceof Color ? color.ios : color;
if (__VISIONOS__ || majorVersion >= 15) {
if (__VISIONOS__ || SDK_VERSION >= 15) {
const appearance = this._getAppearance(navBar);
// appearance.configureWithOpaqueBackground();
appearance.backgroundColor = nativeColor;
@@ -416,7 +418,7 @@ export class ActionBar extends ActionBarBase {
let color: UIColor;
if (__VISIONOS__ || majorVersion >= 15) {
if (__VISIONOS__ || SDK_VERSION >= 15) {
const appearance = this._getAppearance(navBar);
color = appearance.backgroundColor;
} else {
@@ -432,7 +434,7 @@ export class ActionBar extends ActionBarBase {
return;
}
if (__VISIONOS__ || majorVersion >= 15) {
if (__VISIONOS__ || SDK_VERSION >= 15) {
const appearance = this._getAppearance(navBar);
// appearance.configureWithOpaqueBackground();
appearance.backgroundImage = image;
@@ -456,7 +458,7 @@ export class ActionBar extends ActionBarBase {
let image: UIImage;
if (__VISIONOS__ || majorVersion >= 15) {
if (__VISIONOS__ || SDK_VERSION >= 15) {
const appearance = this._getAppearance(navBar);
image = appearance.backgroundImage;
} else {
@@ -507,6 +509,8 @@ export class ActionBar extends ActionBarBase {
return;
}
console.log('ActionBar._onTitlePropertyChanged', this.title);
if (page.frame) {
page.frame._updateActionBar(page);
}
@@ -517,7 +521,7 @@ export class ActionBar extends ActionBarBase {
private updateFlatness(navBar: UINavigationBar) {
if (this.flat) {
if (majorVersion >= 15) {
if (SDK_VERSION >= 15) {
const appearance = this._getAppearance(navBar);
appearance.shadowColor = UIColor.clearColor;
this._updateAppearance(navBar, appearance);
@@ -530,7 +534,7 @@ export class ActionBar extends ActionBarBase {
navBar.translucent = false;
}
} else {
if (majorVersion >= 15) {
if (SDK_VERSION >= 15) {
if (navBar.standardAppearance) {
// Not flat and never been set do nothing.
const appearance = navBar.standardAppearance;
@@ -581,7 +585,7 @@ export class ActionBar extends ActionBarBase {
public onLayout(left: number, top: number, right: number, bottom: number) {
const titleView = this.titleView;
if (titleView) {
if (majorVersion > 10) {
if (SDK_VERSION > 10) {
// On iOS 11 titleView is wrapped in another view that is centered with constraints.
View.layoutChild(this, titleView, 0, 0, titleView.getMeasuredWidth(), titleView.getMeasuredHeight());
} else {
@@ -670,4 +674,26 @@ export class ActionBar extends ActionBarBase {
this.navBar.prefersLargeTitles = value;
}
}
private checkLargeTitleSupport(navigationItem: UINavigationItem) {
const navBar = this.navBar;
if (!navBar) {
return;
}
// Configure large title display mode only when not using a custom titleView
if (SDK_VERSION >= 11) {
if (this.iosLargeTitle) {
// Always show large title for this navigation item when large titles are enabled
navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Always;
} else {
if (SDK_VERSION >= 26) {
// Explicitly disable large titles for this navigation item
// Due to overlapping title issue in iOS 26
navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Never;
} else {
navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Automatic;
}
}
}
}
}

View File

@@ -10,15 +10,6 @@ export enum NavigationType {
replace,
}
export interface TransitionState {
enterTransitionListener: any;
exitTransitionListener: any;
reenterTransitionListener: any;
returnTransitionListener: any;
transitionName: string;
entry: BackstackEntry;
}
export interface ViewEntry {
moduleName?: string;
create?: () => View;
@@ -35,6 +26,17 @@ export interface NavigationEntry extends ViewEntry {
clearHistory?: boolean;
}
export interface BackstackEntry {
entry: NavigationEntry;
resolvedPage: Page;
navDepth: number;
fragmentTag: string;
fragment?: any;
viewSavedState?: any;
frameId?: number;
recreated?: boolean;
}
export interface NavigationContext {
entry: BackstackEntry;
// TODO: remove isBackNavigation for NativeScript 7.0
@@ -49,15 +51,13 @@ export interface NavigationTransition {
curve?: any;
}
export interface BackstackEntry {
entry: NavigationEntry;
resolvedPage: Page;
navDepth: number;
fragmentTag: string;
fragment?: any;
viewSavedState?: any;
frameId?: number;
recreated?: boolean;
export interface TransitionState {
enterTransitionListener: any;
exitTransitionListener: any;
reenterTransitionListener: any;
returnTransitionListener: any;
transitionName: string;
entry: BackstackEntry;
}
export interface AndroidFrame extends Observable {

View File

@@ -3,6 +3,7 @@ import { NavigatedData, Page } from '../page';
import { Observable, EventData } from '../../data/observable';
import { Property, View } from '../core/view';
import { Transition } from '../transition';
import { BackstackEntry } from './frame-interfaces';
export * from './frame-interfaces';

View File

@@ -1,5 +1,6 @@
//Types
import { iOSFrame as iOSFrameDefinition, BackstackEntry, NavigationTransition } from '.';
import { iOSFrame as iOSFrameDefinition, NavigationTransition } from '.';
import type { BackstackEntry } from './frame-interfaces';
import { FrameBase, NavigationType } from './frame-common';
import { Page } from '../page';
import { View } from '../core/view';

View File

@@ -1,12 +1,10 @@
import { SwitchBase, checkedProperty, offBackgroundColorProperty } from './switch-common';
import { colorProperty, backgroundColorProperty, backgroundInternalProperty } from '../styling/style-properties';
import { Color } from '../../color';
import { iOSNativeHelper, layout } from '../../utils';
import { SDK_VERSION } from '../../utils/constants';
export * from './switch-common';
const majorVersion = iOSNativeHelper.MajorVersion;
@NativeClass
class SwitchChangeHandlerImpl extends NSObject {
private _owner: WeakRef<Switch>;
@@ -30,15 +28,12 @@ class SwitchChangeHandlerImpl extends NSObject {
};
}
const zeroSize = { width: 0, height: 0 };
export class Switch extends SwitchBase {
nativeViewProtected: UISwitch;
private _handler: NSObject;
constructor() {
super();
this.width = 51;
this.height = 31;
}
public createNativeView() {
@@ -75,7 +70,7 @@ export class Switch extends SwitchBase {
// only add :checked pseudo handling on supported iOS versions
// ios <13 works but causes glitchy animations when toggling
// so we decided to keep the old behavior on older versions.
if (majorVersion >= 13) {
if (SDK_VERSION >= 13) {
super._onCheckedPropertyChanged(newValue);
if (this.offBackgroundColor) {
@@ -93,17 +88,6 @@ export class Switch extends SwitchBase {
return this.nativeViewProtected;
}
public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void {
// It can't be anything different from 51x31
const nativeSize = this.nativeViewProtected.sizeThatFits(zeroSize);
this.width = nativeSize.width;
this.height = nativeSize.height;
const widthAndState = Switch.resolveSizeAndState(layout.toDevicePixels(nativeSize.width), layout.toDevicePixels(51), layout.EXACTLY, 0);
const heightAndState = Switch.resolveSizeAndState(layout.toDevicePixels(nativeSize.height), layout.toDevicePixels(31), layout.EXACTLY, 0);
this.setMeasuredDimension(widthAndState, heightAndState);
}
[checkedProperty.getDefault](): boolean {
return false;
}
@@ -130,7 +114,7 @@ export class Switch extends SwitchBase {
return this.nativeViewProtected.onTintColor;
}
[backgroundColorProperty.setNative](value: UIColor | Color) {
if (majorVersion >= 13) {
if (SDK_VERSION >= 13) {
if (!this.offBackgroundColor || this.checked) {
this.setNativeBackgroundColor(value);
}
@@ -151,7 +135,7 @@ export class Switch extends SwitchBase {
return this.nativeViewProtected.backgroundColor;
}
[offBackgroundColorProperty.setNative](value: Color | UIColor) {
if (majorVersion >= 13) {
if (SDK_VERSION >= 13) {
if (!this.checked) {
this.setNativeBackgroundColor(value);
}