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
5 changes: 5 additions & 0 deletions .changeset/major-sites-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-native-bottom-tabs': patch
---

feat: improve subview foreach on iOS
4 changes: 2 additions & 2 deletions packages/react-native-bottom-tabs/ios/TabBarFontSize.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import UIKit
import React
import UIKit

enum TabBarFontSize {
/// Returns the default font size for tab bar item labels based on the current platform
Expand Down Expand Up @@ -35,7 +35,7 @@
attributes[.font] = RCTFont.update(
nil,
withFamily: family,
size: NSNumber(value: size),

Check warning on line 38 in packages/react-native-bottom-tabs/ios/TabBarFontSize.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Legacy Objective-C Reference Type Violation: Prefer Swift value types to bridged Objective-C reference types (legacy_objc_type)
weight: weight,
style: nil,
variant: nil,
Expand All @@ -51,7 +51,7 @@
}

// Add color if provided
if let color = color {
if let color {
attributes[.foregroundColor] = color
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
}
#endif

// Unfortunately, due to iOS 26 new tab switching animations, controlling state from JavaScript is causing significant delays when switching tabs.

Check warning on line 20 in packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Line Length Violation: Line should be 120 characters or less; currently it has 150 characters (line_length)
// See: https://github.com/callstackincubator/react-native-bottom-tabs/issues/383
// Due to this, whether the tab prevents default has to be defined statically.
if let index = tabBarController.viewControllers?.firstIndex(of: viewController) {
let defaultPrevented = onClick?(index) ?? false

return !defaultPrevented
}

return false
}
}
Expand Down Expand Up @@ -60,8 +60,8 @@
}

// Create gesture handler
let handler = LongPressGestureHandler(tabBar: tabController.tabBar, handler: { key, isLongPress in _ = onTabEvent(key,isLongPress) })
let handler = LongPressGestureHandler(tabBar: tabController.tabBar) { key, isLongPress in _ = onTabEvent(key, isLongPress) }

Check warning on line 63 in packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Line Length Violation: Line should be 120 characters or less; currently it has 128 characters (line_length)
let gesture = UILongPressGestureRecognizer(target: handler, action: #selector(LongPressGestureHandler.handleLongPress(_:)))

Check warning on line 64 in packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Line Length Violation: Line should be 120 characters or less; currently it has 127 characters (line_length)
gesture.minimumPressDuration = 0.5

objc_setAssociatedObject(tabController.tabBar, &AssociatedKeys.gestureHandler, handler, .OBJC_ASSOCIATION_RETAIN)
Expand All @@ -70,7 +70,7 @@
}
}

private struct AssociatedKeys {

Check warning on line 73 in packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Convenience Type Violation: Types used for hosting only static members should be implemented as a caseless enum to avoid instantiation (convenience_type)
static var gestureHandler: UInt8 = 0
}

Expand All @@ -91,10 +91,10 @@
let location = recognizer.location(in: tabBar)

// Get buttons and sort them by frames
let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).contains("UITabBarButton") }.sorted { $0.frame.minX < $1.frame.minX }

Check warning on line 94 in packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Line Length Violation: Line should be 120 characters or less; currently it has 151 characters (line_length)

for (index, button) in tabBarButtons.enumerated() {
if button.frame.contains(location) {

Check warning on line 97 in packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Prefer For-Where Violation: `where` clauses are preferred over a single `if` inside a `for` (for_where)
handler(index, true)
break
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ struct LegacyTabView: AnyTabView {
@ViewBuilder
var body: some View {
TabView(selection: $props.selectedPage) {
ForEach(props.children.indices, id: \.self) { index in
renderTabItem(at: index)
ForEach(props.children) { child in
if let index = props.children.firstIndex(of: child) {
renderTabItem(at: index)
}
}
.measureView { size in
onLayout(size)
Expand All @@ -27,7 +29,7 @@ struct LegacyTabView: AnyTabView {

if !tabData.hidden || isFocused {
let icon = props.icons[index]
let child = props.children[safe: index] ?? PlatformView()
let child = props.children[safe: index]?.view ?? PlatformView()
let context = TabAppearContext(
index: index,
tabData: tabData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@ struct NewTabView: AnyTabView {
@ViewBuilder
var body: some View {
TabView(selection: $props.selectedPage) {
ForEach(props.children.indices, id: \.self) { index in
if let tabData = props.items[safe: index] {
ForEach(props.children) { child in
if let index = props.children.firstIndex(of: child),
let tabData = props.items[safe: index] {
let isFocused = props.selectedPage == tabData.key

if !tabData.hidden || isFocused {
let icon = props.icons[index]

let platformChild = props.children[safe: index] ?? PlatformView()
let child = RepresentableView(view: platformChild)
let context = TabAppearContext(
index: index,
tabData: tabData,
Expand All @@ -29,7 +28,7 @@ struct NewTabView: AnyTabView {
)

Tab(value: tabData.key, role: tabData.role?.convert()) {
child
RepresentableView(view: child.view)
.ignoresSafeArea(.container, edges: .all)
.tabAppear(using: context)
} label: {
Expand Down
1 change: 0 additions & 1 deletion packages/react-native-bottom-tabs/ios/TabViewImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ struct TabViewImpl: View {
}
#endif


#if !os(macOS)
private func configureTransparentAppearance(tabBar: UITabBar, props: TabViewProps) {
tabBar.barTintColor = props.barTintColor
Expand Down
13 changes: 11 additions & 2 deletions packages/react-native-bottom-tabs/ios/TabViewProps.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,30 @@

public enum TabBarRole: String {
case search

@available(iOS 18, macOS 15, visionOS 2, tvOS 18, *)
func convert() -> TabRole {
switch self {
case .search:

Check warning on line 32 in packages/react-native-bottom-tabs/ios/TabViewProps.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Switch and Case Statement Alignment Violation: Case statements should vertically aligned with their closing brace (switch_case_alignment)
return .search
}
}
}

struct IdentifiablePlatformView: Identifiable, Equatable {
let id = UUID()
let view: PlatformView

init(_ view: PlatformView) {
self.view = view
}
}

/**
Props that component accepts. SwiftUI view gets re-rendered when ObservableObject changes.
*/
class TabViewProps: ObservableObject {
@Published var children: [PlatformView] = []
@Published var children: [IdentifiablePlatformView] = []
@Published var items: [TabInfo] = []
@Published var selectedPage: String?
@Published var icons: [Int: PlatformImage] = [:]
Expand Down Expand Up @@ -70,7 +79,7 @@

var filteredItems: [TabInfo] {
items.filter {
!$0.hidden || $0.key == selectedPage

Check warning on line 82 in packages/react-native-bottom-tabs/ios/TabViewProps.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Anonymous Argument in Multiline Closure Violation: Use named arguments in multiline closures (anonymous_argument_in_multiline_closure)

Check warning on line 82 in packages/react-native-bottom-tabs/ios/TabViewProps.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Anonymous Argument in Multiline Closure Violation: Use named arguments in multiline closures (anonymous_argument_in_multiline_closure)
}
}
}
8 changes: 4 additions & 4 deletions packages/react-native-bottom-tabs/ios/TabViewProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ public final class TabInfo: NSObject {
}

override public func didUpdateReactSubviews() {
props.children = reactSubviews()
props.children = reactSubviews().map(IdentifiablePlatformView.init)
}

#if os(macOS)
Expand Down Expand Up @@ -218,15 +218,15 @@ public final class TabInfo: NSObject {
#endif
}
}

@objc(insertChild:atIndex:)
public func insertChild(_ child: UIView, at index: Int) {
guard index >= 0 && index <= props.children.count else {
return
}
props.children.insert(child, at: index)
props.children.insert(IdentifiablePlatformView(child), at: index)
}

@objc(removeChildAtIndex:)
public func removeChild(at index: Int) {
guard index >= 0 && index < props.children.count else {
Expand Down
Loading