Skip to content

Conversation

@kligarski
Copy link
Contributor

@kligarski kligarski commented Nov 26, 2025

Description

Fixes incorrect tab screen frame, assigned by UIKit when tabs are rendered dynamically & available space is less that window size.

We enable auto-layout and add constraints so that tabBarController.view's dimensions match RNSBottomTabsHostComponentView.

Closes https://github.com/software-mansion/react-native-screens-labs/issues/588.

before after
before3425_vid.mov
after3425_vid.mov
--- ---
before3425_vh after3425_vh

We might want to investigate in more detail why we receive the incorrect frame. I created an issue for this: https://github.com/software-mansion/react-native-screens-labs/issues/596.

Changes

  • add layout constraints for tabBarController.view.

Test code and steps to reproduce

Run Test3425.

Checklist

  • Included code example that can be used to test this change
  • Ensured that CI passes

@kligarski kligarski marked this pull request as ready for review November 26, 2025 12:22
@kligarski
Copy link
Contributor Author

I've tested tabs with padding/other views around them on iPad as well and everything seems to be working correctly.

// On Fabric, when tabs are rendered dynamically, tab screens receive incorrect frame from UIKit layout
// (frame matches full window size instead of the size of the host). To mitigate this, we override
// `layoutSubviews` and assign correct frame ourselves.
// TODO: investigate why we receive incorrect frame, fix and remove this workaround
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you include an issue number for the TODO ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this is not a workaround anymore and layout constraints should stay so no need to mark them for removal. I will keep the ticket for research because it might be useful to know why this happens (but it's not crucial).

Copy link
Collaborator

@kmichalikk kmichalikk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀

padded-bottom-tabs-uh-oh.mov

@kligarski kligarski marked this pull request as draft November 27, 2025 09:18
@kligarski
Copy link
Contributor Author

👀
padded-bottom-tabs-uh-oh.mov

Thanks for catching this!

I confirmed that this issue visible on the recording is reproducible in bare UIKit app on iOS 26.1. It seems to be fixed on iOS 26.2 beta 3 (but we still need to fix the size of the tabBarController.view).

iOS 26.1 iOS 26.2 beta 3
3425_iOS_26_1.mov
3425_iOS_26_2_beta_3.mov
UIKit repro
import UIKit

class ReproductionViewController: UIViewController {

    private let hostView: UIView = {
        let view = UIView()
        view.backgroundColor = .systemGray6
        view.layer.borderColor = UIColor.green.cgColor
        view.layer.borderWidth = 3.0
        return view
    }()

    private let myTabBarController = UITabBarController()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .black

        setupHostView()
        setupTabs()
        
        embedTabBar()
    }

    private func setupHostView() {
        view.addSubview(hostView)
        hostView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            hostView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 50),
            hostView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50),
            hostView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50),
            hostView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -50)
        ])
    }

    private func embedTabBar() {
      myTabBarController.tabBarMinimizeBehavior = .onScrollDown
        addChild(myTabBarController)
        hostView.addSubview(myTabBarController.view)
        
        myTabBarController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            myTabBarController.view.topAnchor.constraint(equalTo: hostView.topAnchor),
            myTabBarController.view.bottomAnchor.constraint(equalTo: hostView.bottomAnchor),
            myTabBarController.view.leadingAnchor.constraint(equalTo: hostView.leadingAnchor),
            myTabBarController.view.trailingAnchor.constraint(equalTo: hostView.trailingAnchor)
        ])
        
        myTabBarController.didMove(toParent: self)
    }

    private func setupTabs() {
        let tab1 = ScrollableTabViewController(title: "Home", color: .systemBlue)
        let tab2 = ScrollableTabViewController(title: "Settings", color: .systemRed)
        
        myTabBarController.setViewControllers([tab1, tab2], animated: false)
    }
}

class ScrollableTabViewController: UIViewController {
    let scrollView = UIScrollView()
    let contentView = UIView()
    let color: UIColor
    
    init(title: String, color: UIColor) {
        self.color = color
        super.init(nibName: nil, bundle: nil)
        self.title = title
        self.tabBarItem = UITabBarItem(title: title, image: UIImage(systemName: "circle"), tag: 0)
    }
    
    required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        setupScrollView()
    }
    
    func setupScrollView() {
        view.addSubview(scrollView)
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(contentView)
        contentView.translatesAutoresizingMaskIntoConstraints = false
        
        contentView.backgroundColor = color.withAlphaComponent(0.3)
        
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.topAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            
            contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
            contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
            contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
            
            contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
            contentView.heightAnchor.constraint(equalToConstant: 1500)
        ])
        
        addLabel(text: "TOP", y: 10)
        addLabel(text: "BOTTOM", y: 1450)
    }
    
    func addLabel(text: String, y: CGFloat) {
        let label = UILabel()
        label.text = text
        label.font = .boldSystemFont(ofSize: 20)
        label.frame = CGRect(x: 20, y: y, width: 200, height: 30)
        contentView.addSubview(label)
    }
}

As for the implementation, I decided to use layout constraints instead of overriding layoutSubviews to use native layout mechanism instead of introducing simple yet custom logic.

@kligarski kligarski marked this pull request as ready for review November 27, 2025 11:43
Copy link
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@kligarski kligarski merged commit d582245 into main Nov 27, 2025
6 checks passed
@kligarski kligarski deleted the @kligarski/fix-ios-tabs-screen-incorrect-frame branch November 27, 2025 12:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants