@@ -3,7 +3,7 @@ import { Font } from '../styling/font';
33
44import { IOSHelper , View } from '../core/view' ;
55import { ViewBase } from '../core/view-base' ;
6- import { TabViewBase , TabViewItemBase , itemsProperty , selectedIndexProperty , tabTextColorProperty , tabTextFontSizeProperty , tabBackgroundColorProperty , selectedTabTextColorProperty , iosIconRenderingModeProperty , traceMissingIcon } from './tab-view-common' ;
6+ import { TabViewBase , TabViewItemBase , itemsProperty , selectedIndexProperty , tabTextColorProperty , tabTextFontSizeProperty , tabBackgroundColorProperty , selectedTabTextColorProperty , iosIconRenderingModeProperty , traceMissingIcon , iosBottomAccessoryProperty , iosTabBarMinimizeBehaviorProperty } from './tab-view-common' ;
77import { Color } from '../../color' ;
88import { Trace } from '../../trace' ;
99import { fontInternalProperty } from '../styling/style-properties' ;
@@ -15,7 +15,7 @@ import { Frame } from '../frame';
1515import { layout } from '../../utils/layout-helper' ;
1616import { FONT_PREFIX , isFontIconURI , isSystemURI , SYSTEM_PREFIX } from '../../utils/common' ;
1717import { SDK_VERSION } from '../../utils/constants' ;
18- import { Device } from '../../platform' ;
18+ import { Device , Screen } from '../../platform' ;
1919export * from './tab-view-common' ;
2020
2121@NativeClass
@@ -285,6 +285,7 @@ export class TabView extends TabViewBase {
285285 private _delegate : UITabBarControllerDelegateImpl ;
286286 private _moreNavigationControllerDelegate : UINavigationControllerDelegateImpl ;
287287 private _iconsCache = { } ;
288+ private _bottomAccessoryNsView : View ;
288289 private _ios : UITabBarControllerImpl ;
289290 private _actionBarHiddenByTabView : boolean ;
290291
@@ -328,6 +329,11 @@ export class TabView extends TabViewBase {
328329 if ( this . _ios ) {
329330 this . _ios . delegate = this . _delegate ;
330331 }
332+
333+ // Re-apply bottom accessory if set
334+ if ( this . iosBottomAccessory ) {
335+ this . _applyBottomAccessory ( this . iosBottomAccessory , false ) ;
336+ }
331337 }
332338
333339 public onUnloaded ( ) {
@@ -338,6 +344,8 @@ export class TabView extends TabViewBase {
338344 this . _ios . moreNavigationController . delegate = null ;
339345 }
340346 }
347+ // Avoid retaining custom view when unloading
348+ this . _applyBottomAccessory ( null , false ) ;
341349 super . onUnloaded ( ) ;
342350 }
343351
@@ -682,11 +690,169 @@ export class TabView extends TabViewBase {
682690 for ( let i = 0 , length = items . length ; i < length ; i ++ ) {
683691 const item = items [ i ] ;
684692 if ( item . iconSource ) {
685- ( < TabViewItem > item ) . _update ( ) ;
693+ ( item as TabViewItem ) . _update ( ) ;
686694 }
687695 }
688696 }
689697 }
698+
699+ // iOS 26+: bottom accessory support
700+ [ iosBottomAccessoryProperty . getDefault ] ( ) : View {
701+ return null ;
702+ }
703+ [ iosBottomAccessoryProperty . setNative ] ( value : View ) {
704+ this . _applyBottomAccessory ( value , false ) ;
705+ }
706+
707+ // iOS 26+: tab bar minimize behavior
708+ [ iosTabBarMinimizeBehaviorProperty . getDefault ] ( ) : 'automatic' | 'never' | 'onScrollDown' | 'onScrollUp' {
709+ return 'automatic' ;
710+ }
711+ [ iosTabBarMinimizeBehaviorProperty . setNative ] ( value : 'automatic' | 'never' | 'onScrollDown' | 'onScrollUp' ) {
712+ if ( SDK_VERSION < 26 ) {
713+ return ;
714+ }
715+ let mapped : UITabBarMinimizeBehavior ;
716+ switch ( value ) {
717+ case 'never' :
718+ mapped = UITabBarMinimizeBehavior . Never ;
719+ break ;
720+ case 'onScrollDown' :
721+ mapped = UITabBarMinimizeBehavior . OnScrollDown ;
722+ break ;
723+ case 'onScrollUp' :
724+ mapped = UITabBarMinimizeBehavior . OnScrollUp ;
725+ break ;
726+ case 'automatic' :
727+ default :
728+ mapped = UITabBarMinimizeBehavior . Automatic ;
729+ }
730+ this . _ios . tabBarMinimizeBehavior = mapped ;
731+ }
732+
733+ private _applyBottomAccessory ( value : View | null , animated : boolean ) {
734+ // Guard for platform availability
735+ if ( SDK_VERSION < 26 ) {
736+ return ;
737+ }
738+
739+ const setAccessory = ( accessory : UITabAccessory | null ) => {
740+ try {
741+ this . _ios . setBottomAccessoryAnimated ( accessory , animated ) ;
742+ } catch ( err ) {
743+ // Fallback to property if needed
744+ this . _ios . bottomAccessory = accessory ;
745+ }
746+ } ;
747+
748+ // Clear previous
749+ if ( ! value ) {
750+ // Clear on controller
751+ setAccessory ( null ) ;
752+ // Tear down previously managed NS view
753+ if ( this . _bottomAccessoryNsView ) {
754+ // Do not remove from a parent; we didn't add it to the NS view tree.
755+ try {
756+ this . _bottomAccessoryNsView . _tearDownUI ( true ) ;
757+ } catch ( _ ) { }
758+ this . _bottomAccessoryNsView = null ;
759+ }
760+ return ;
761+ }
762+
763+ // Ensure the NativeScript view has a native view
764+ const nsView = value ;
765+ if ( ! nsView . nativeViewProtected ) {
766+ // mirror dialogs approach to setup UI for a detached view
767+ nsView . _setupUI ( { } as any ) ;
768+ }
769+ // Just mark it loaded, if not already, so measurement & styling are applied.
770+ if ( ! nsView . isLoaded ) {
771+ // In detached scenarios we simply callLoaded after setup.
772+ nsView . callLoaded ( ) ;
773+ }
774+ const contentView = nsView . nativeViewProtected as UIView ;
775+ if ( ! contentView ) {
776+ return ;
777+ }
778+
779+ // Use frame-based sizing; keep autoresizing mask-based behavior enabled (no Auto Layout constraints added here).
780+ contentView . translatesAutoresizingMaskIntoConstraints = true ;
781+ // Measure desired height with the tab bar width
782+ let tabBarWidth = this . _ios ?. tabBar ?. frame ?. size ?. width || Screen . mainScreen . screen . bounds . size . width ;
783+ // Account for safe area insets so accessory doesn't extend visually past rounded corners
784+ if ( this . _ios ?. tabBar ?. safeAreaInsets ) {
785+ const insets = this . _ios . tabBar . safeAreaInsets ;
786+ // Reduce usable width by left+right safe area (typically 0, but defensive)
787+ const horizontalInsets = insets . left + insets . right ;
788+ if ( horizontalInsets > 0 && horizontalInsets < tabBarWidth ) {
789+ tabBarWidth -= horizontalInsets ;
790+ }
791+ }
792+ const tabBarWidthPx = layout . toDevicePixels ( tabBarWidth ) ;
793+ // Prefer flooring to avoid overshooting container by +1px due to FP rounding
794+ const tabBarWidthPxRounded = Math . floor ( tabBarWidthPx ) ;
795+ let measuredHeight = 0 ;
796+ // Measure using device-pixel width; flooring prevents +1px expansion
797+ const widthSpec = layout . makeMeasureSpec ( tabBarWidthPxRounded , layout . EXACTLY ) ;
798+ const heightSpec = layout . makeMeasureSpec ( 0 , layout . UNSPECIFIED ) ;
799+ nsView . measure ( widthSpec , heightSpec ) ;
800+ measuredHeight = layout . toDeviceIndependentPixels ( nsView . getMeasuredHeight ( ) ) ;
801+
802+ // Use a sensible minimum height (44pt button row) if measurement is tiny
803+ const minHeight = 44 ;
804+ const finalHeight = Math . max ( minHeight , measuredHeight || 0 ) ;
805+ // Rely on container height constraint (below) and frame-based layout inside container.
806+ const container = NSTabAccessoryContainer . initWithOwner ( new WeakRef ( nsView ) ) ;
807+ container . translatesAutoresizingMaskIntoConstraints = true ;
808+ container . autoresizingMask = UIViewAutoresizing . FlexibleWidth | UIViewAutoresizing . FlexibleHeight ;
809+ // Mask any subpixel spill just in case
810+ container . clipsToBounds = true ;
811+ container . addSubview ( contentView ) ;
812+ // Constrain the container height (not the content) so UIKit has a concrete size.
813+ const containerHeight = container . heightAnchor . constraintEqualToConstant ( finalHeight ) ;
814+ containerHeight . priority = 999 ;
815+ NSLayoutConstraint . activateConstraints ( [ containerHeight ] ) ;
816+
817+ const accessory = UITabAccessory . alloc ( ) . initWithContentView ( container ) ;
818+ setAccessory ( accessory ) ;
819+ // Keep references for later teardown
820+ this . _bottomAccessoryNsView = nsView ;
821+ }
822+ }
823+
824+ @NativeClass
825+ class NSTabAccessoryContainer extends UIView {
826+ _owner : WeakRef < View > ;
827+ static initWithOwner ( owner : WeakRef < View > ) : NSTabAccessoryContainer {
828+ const v = NSTabAccessoryContainer . new ( ) as NSTabAccessoryContainer ;
829+ v . _owner = owner ;
830+ return v ;
831+ }
832+
833+ override layoutSubviews ( ) {
834+ super . layoutSubviews ( ) ;
835+ const owner = this . _owner ?. deref ( ) ;
836+ if ( ! owner ?. nativeViewProtected ) return ;
837+ owner . nativeViewProtected . frame = this . bounds ;
838+ const w = this . bounds . size . width ;
839+ const h = this . bounds . size . height ;
840+ try {
841+ // Convert to device pixels and floor to avoid +1px overshoot from rounding
842+ let wp = Math . floor ( layout . toDevicePixels ( w ) ) ;
843+ const hp = Math . floor ( layout . toDevicePixels ( h ) ) ;
844+ // Clamp width to <= container pixel width (defensive)
845+ const containerPxWidth = Math . floor ( layout . toDevicePixels ( this . bounds . size . width ) ) ;
846+ if ( wp > containerPxWidth ) {
847+ wp = containerPxWidth ;
848+ }
849+ // Ensure NS view and its children are measured with the final container width
850+ const widthSpec = layout . makeMeasureSpec ( wp , layout . EXACTLY ) ;
851+ const heightSpec = layout . makeMeasureSpec ( hp , layout . EXACTLY ) ;
852+ owner . measure ( widthSpec , heightSpec ) ;
853+ owner . layout ( 0 , 0 , wp , hp ) ;
854+ } catch ( _ ) { }
855+ }
690856}
691857
692858interface TabStates {
0 commit comments