Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export function getNativeSelectedIndex(tabView: TabView): number {
return viewPager ? viewPager.getCurrentItem() : -1;
}

export function getNativeProminentTabIdentifier(tabView: TabView): string | null | undefined {
return undefined;
}

export function getNativeFont(tabView: TabView): any {
const tv: android.widget.TextView = (<org.nativescript.widgets.TabLayout>(<any>tabView)._tabLayout).getTextViewForItemAt(0);
if (tv) {
Expand Down
2 changes: 2 additions & 0 deletions apps/automated/src/ui/tab-view/tab-view-tests-native.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ import { TabView } from '@nativescript/core/ui/tab-view';
export function getNativeTabCount(tabView: TabView): number;
export function selectNativeTab(tabView: TabView, index: number): void;
export function getNativeSelectedIndex(tabView: TabView): number;
/** `undefined` where the platform has no prominent tab (Android, iOS < 27). */
export function getNativeProminentTabIdentifier(tabView: TabView): string | null | undefined;
export function getNativeFont(tabView: TabView): any;
export function getOriginalFont(tabView: TabView): any;
16 changes: 11 additions & 5 deletions apps/automated/src/ui/tab-view/tab-view-tests-native.ios.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { Font, TabView } from '@nativescript/core';
import { Font, TabView, Utils } from '@nativescript/core';

export function getNativeTabCount(tabView: TabView): number {
if (!tabView.ios.viewControllers) {
return 0;
}
const controllers = tabView.ios.viewControllers ?? (Utils.SDK_VERSION >= 27 ? tabView.ios.tabs : null);

return tabView.ios.viewControllers.count;
return controllers ? controllers.count : 0;
}

export function selectNativeTab(tabView: TabView, index: number): void {
Expand All @@ -17,6 +15,14 @@ export function getNativeSelectedIndex(tabView: TabView): number {
return tabView.ios.selectedIndex;
}

export function getNativeProminentTabIdentifier(tabView: TabView): string | null | undefined {
if (Utils.SDK_VERSION < 27) {
return undefined;
}

return tabView.ios.prominentTabIdentifier;
}

export function getNativeFont(tabView: TabView): UIFont {
const tabBar = <UITabBar>tabView.ios.tabBar;
if (tabBar.items.count > 0) {
Expand Down
38 changes: 38 additions & 0 deletions apps/automated/src/ui/tab-view/tab-view-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,44 @@ export class TabViewTest extends UITest<tabViewModule.TabView> {
TKUnit.assertEqual(actualNewIndex, expectedNewIndex, 'expectedNewIndex');
};

public test_SearchRoleTabIsProminent = function () {
const tabView = this.testView;
const items = this._createItems(3);
items[2].role = 'search';
tabView.items = items;
this.waitUntilTestElementIsLoaded();

const actual = tabViewTestsNative.getNativeProminentTabIdentifier(tabView);
if (actual !== undefined) {
TKUnit.assertEqual(actual, '2', 'search tab should be prominent');
}
};

public test_ProminentRoleTakesPrecedenceOverSearchRole = function () {
const tabView = this.testView;
const items = this._createItems(3);
items[0].role = 'search';
items[1].role = 'prominent';
tabView.items = items;
this.waitUntilTestElementIsLoaded();

const actual = tabViewTestsNative.getNativeProminentTabIdentifier(tabView);
if (actual !== undefined) {
TKUnit.assertEqual(actual, '1', 'prominent tab should win over search tab');
}
};

public test_NoProminentTabWithoutRoles = function () {
const tabView = this.testView;
tabView.items = this._createItems(3);
this.waitUntilTestElementIsLoaded();

const actual = tabViewTestsNative.getNativeProminentTabIdentifier(tabView);
if (actual !== undefined) {
TKUnit.assertNull(actual, 'no tab should be prominent');
}
};

// public test_FontIsReappliedWhenTabItemsChange = function () {
// const assertFontsAreEqual = (actual: any, expected: any, message?: string) => {
// if (this.testView.ios) {
Expand Down
2 changes: 1 addition & 1 deletion apps/toolbox/src/pages/tabview.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
</TabViewItem>

<!-- Third tab -->
<!-- change role to 'search' to see different behavior on iOS -->
<!-- change role to 'search' or 'prominent' (iOS 27+) to see different behavior on iOS -->
<TabViewItem title="Third" iconSource="font://&#x43;" role="none">
<Label text="Third Tab Content" textAlignment="center" verticalAlignment="center" />
</TabViewItem>
Expand Down
8 changes: 5 additions & 3 deletions packages/core/ui/tab-view/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export class TabViewItem extends ViewBase {
public iconSource: string;

/**
* Gets or sets the role of the TabViewItem.
* Gets or sets the role of the TabViewItem (iOS 18+).
* - `search`: a system search tab, shown separated from the other tabs.
* - `prominent`: gives the tab the separated, emphasized placement on iOS 27+ (takes precedence over `search`).
*/
public role: string;

Expand Down Expand Up @@ -150,12 +152,12 @@ export class TabView extends View {
/**
* Gets the native [android widget](http://developer.android.com/reference/android/support/v4/view/ViewPager.html) that represents the user interface for this component. Valid only when running on Android OS.
*/
android: any /* android.view.View */; //androidx.core.view.ViewPager;
android: any; /* android.view.View */ //androidx.core.view.ViewPager;

/**
* Gets the native iOS [UITabBarController](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITabBarController_Class/) that represents the user interface for this component. Valid only when running on iOS.
*/
ios: any /* UITabBarController */;
ios: any; /* UITabBarController */

/**
* Gets or set the UIImageRenderingMode of the tab icons in iOS. Defaults to "automatic"
Expand Down
83 changes: 69 additions & 14 deletions packages/core/ui/tab-view/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,24 +115,45 @@ class UITabBarControllerDelegateImpl extends NSObject implements UITabBarControl
Trace.write('TabView.delegate.SHOULD_select(' + tabBarController + ', ' + viewController + ');', Trace.categories.Debug);
}

const owner = this._owner?.deref();
if (owner) {
// "< More" cannot be visible after clicking on the main tab bar buttons.
owner._handleTwoNavigationBars(false);
return this._shouldSelect(tabBarController, tabBarController.selectedViewController === viewController);
}

public tabBarControllerDidSelectViewController(tabBarController: UITabBarController, viewController: UIViewController): void {
if (Trace.isEnabled()) {
Trace.write('TabView.delegate.DID_select(' + tabBarController + ', ' + viewController + ');', Trace.categories.Debug);
}

if (tabBarController.selectedViewController === viewController) {
return false;
this._didSelect(tabBarController, viewController);
}

// Tabs-configured controllers (iOS 27+) report selection only through the UITab callbacks.
public tabBarControllerShouldSelectTab(tabBarController: UITabBarController, tab: UITab): boolean {
if (tabBarController.viewControllers) {
return true;
}

return true;
return this._shouldSelect(tabBarController, tabBarController.selectedTab === tab);
}

public tabBarControllerDidSelectViewController(tabBarController: UITabBarController, viewController: UIViewController): void {
if (Trace.isEnabled()) {
Trace.write('TabView.delegate.DID_select(' + tabBarController + ', ' + viewController + ');', Trace.categories.Debug);
public tabBarControllerDidSelectTabPreviousTab(tabBarController: UITabBarController, selectedTab: UITab, previousTab: UITab): void {
if (tabBarController.viewControllers) {
return;
}

this._didSelect(tabBarController, selectedTab.viewController);
}

private _shouldSelect(tabBarController: UITabBarController, isAlreadySelected: boolean): boolean {
const owner = this._owner?.deref();
if (owner) {
// "< More" cannot be visible after clicking on the main tab bar buttons.
owner._handleTwoNavigationBars(false);
}

return !isAlreadySelected;
}

private _didSelect(tabBarController: UITabBarController, viewController: UIViewController): void {
const owner = this._owner?.deref();
if (owner) {
// iOS lazily initializes moreNavigationController (e.g. on first access of
Expand Down Expand Up @@ -170,7 +191,7 @@ class UINavigationControllerDelegateImpl extends NSObject implements UINavigatio
if (owner) {
// If viewController is one of our tab item controllers, then "< More" will be visible shortly.
// Otherwise viewController is the UIMoreListController which shows the list of all tabs beyond the 4th tab.
const backToMoreWillBeVisible = navigationController.tabBarController?.viewControllers?.containsObject(viewController);
const backToMoreWillBeVisible = owner._indexOfViewController(navigationController.tabBarController, viewController) > -1;
owner._handleTwoNavigationBars(backToMoreWillBeVisible);
}
}
Expand Down Expand Up @@ -443,15 +464,37 @@ export class TabView extends TabViewBase {
if (Trace.isEnabled()) {
Trace.write('TabView._onViewControllerShown(' + viewController + ');', Trace.categories.Debug);
}
if (tabBarController?.viewControllers && tabBarController.viewControllers.containsObject(viewController)) {
this.selectedIndex = tabBarController.viewControllers.indexOfObject(viewController);
const index = this._indexOfViewController(tabBarController, viewController);
if (index > -1) {
this.selectedIndex = index;
} else {
if (Trace.isEnabled()) {
Trace.write('TabView._onViewControllerShown: viewController is not one of our viewControllers', Trace.categories.Debug);
}
}
}

public _indexOfViewController(tabBarController: UITabBarController, viewController: UIViewController): number {
if (!tabBarController || !viewController) {
return -1;
}

if (tabBarController.viewControllers) {
return tabBarController.viewControllers.containsObject(viewController) ? tabBarController.viewControllers.indexOfObject(viewController) : -1;
}

// Controllers configured through tabs are not reported by viewControllers.
const tabs = SDK_VERSION >= 18 ? tabBarController.tabs : null;
for (let i = 0; i < (tabs?.count ?? 0); i++) {
const tab = tabs.objectAtIndex(i);
if (tab.viewController === viewController) {
return parseInt(tab.identifier);
}
}

return -1;
}

public _handleTwoNavigationBars(backToMoreWillBeVisible: boolean) {
if (Trace.isEnabled()) {
Trace.write(`TabView._handleTwoNavigationBars(backToMoreWillBeVisible: ${backToMoreWillBeVisible})`, Trace.categories.Debug);
Expand Down Expand Up @@ -551,14 +594,20 @@ export class TabView extends TabViewBase {
// iOS 18+: build UITab instances and assign them to the controller.
const tabs = [];
const controllers = [];
let prominentIdentifier: string = null;
let searchIdentifier: string = null;
items.forEach((item, i) => {
const controller = this.getViewController(item);
controllers.push(controller);
const icon = this._getIcon(item);
const title = item.title || '';
const identifier = `${i}`;
let tab: UITab;
if (item.role === 'prominent') {
prominentIdentifier ??= identifier;
}
if (item.role === 'search') {
searchIdentifier ??= identifier;
tab = UISearchTab.alloc().initWithTitleImageIdentifierViewControllerProvider(title, icon, identifier, (t) => {
return controller;
});
Expand All @@ -576,7 +625,13 @@ export class TabView extends TabViewBase {
// Prefer animated setter when available.
this._ios.tabs = NSArray.arrayWithArray(tabs);
} catch (e) {}
this._ios.viewControllers = NSArray.arrayWithArray(controllers);
if (SDK_VERSION >= 27) {
// Assigning viewControllers replaces the tabs configuration on iOS 27+, dropping UISearchTab placement.
// Search tabs are only separated by default when they automatically activate search.
this._ios.prominentTabIdentifier = prominentIdentifier ?? searchIdentifier;
} else {
this._ios.viewControllers = NSArray.arrayWithArray(controllers);
}
this._ios.customizableViewControllers = null;
} else {
// iOS < 18: keep using UITabBarItem-based configuration.
Expand Down
Loading