Search code examples
iosswiftsnapkit

How to push another page in iOS programmatically?


I am working with one project on Xcode with SnapKit and without storyboard. I didn't touch AppDelegate. I have this code on SceneDelegate.

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        window = UIWindow(windowScene: windowScene)
        window?.makeKeyAndVisible()
        window?.rootViewController = TabBarController()
    }
}

Project's hierarchy like that:

  • HomePage(Group)
  1. TabBarController
  2. ViewController
  3. HomeCollectionViewCell
  • ShopPage(Group) ...

  • MessagePage(Group) ...

  • ProfilePage(Group)

  1. ProfileViewController
  2. SettingsViewController

I want to open SettingsViewController when settingsButton(on ProfileViewController) tapped.

This is my code in the TabBarController:

import UIKit

class TabBarController: UITabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        self.setupTabs()
        selectedIndex = 3
        tabBar.backgroundColor = .systemBackground
        tabBar.tintColor = UIColor(red: 0, green: 155/255, blue: 247/255, alpha: 1)
    }
    
    //MARK: - Tab Setup
    private func setupTabs(){
        let home = self.createNav(title: "Home", image: UIImage(systemName: "house"), vc: ViewController())
        let shop = self.createNav(title: "Shop", image: UIImage(systemName: "bag"), vc: ShopViewController())
        let message = self.createNav(title: "Message", image: UIImage(systemName: "ellipsis.message"), vc: MessageViewController())
        let profile = self.createNav(title: "Profile", image: UIImage(systemName: "person"), vc: ProfileViewController())
        self.setViewControllers([home, shop, message, profile], animated: true)
    }
    
    private func createNav(title: String, image: UIImage?, vc: UIViewController) -> UINavigationController{
        let nav = UINavigationController(rootViewController: vc)
        nav.tabBarItem.title = title
        nav.tabBarItem.image = image
        return nav
    }
}

This is my code in the ProfileViewController:

class ProfileViewController: UIViewController {
    private lazy var settingButton: UIButton = {
        let button = UIButton()
        button.setImage(UIImage(systemName: "gearshape"), for: .normal)
        button.addTarget(self, action: #selector(openSettings), for: .touchUpInside)
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor(red: 250/255, green: 250/255, blue: 250/255, alpha: 1)
        setupUI()
    }
    
    @objc private func openSettings() {
        let settingsViewController = SettingsViewController()
        navigationController?.pushViewController(settingsViewController, animated: true)
    }
}

private extension ProfileViewController {
    func setupUI() {
        setupViews()
        setupConstraints()
    }
    
    func setupViews() {
        view.addSubview(settingButton)
    }
    
    func setupConstraints() {
        settingButton.snp.makeConstraints { make in
            make.center.equalToSuperview()
        }
    }
}

But when I tap settingButton nothing happens.

I tried to set rootViewController like that on Scene Delegate: window?.rootViewController = UINavigationController(rootViewController: TabBarController())

Also I tried here set ProfileViewController as a rootViewController

Also I tried with present method:

@objc private func openSettings() {
    let settingsViewController = SettingsViewController()
    present(settingsViewController, animated: true)
}

Edit

Sorry, I thought that this part of code is not related to my problem and didn't provide you with this. My code was like that:

class ProfileViewController: UIViewController {
    private lazy var topView: UIView = {
        let view = UIView()
        return view
    }()
    
    private lazy var topTitle: UILabel = {
        let label = UILabel()
        label.text = "Profile"
        label.font = UIFont.boldSystemFont(ofSize: 19)
        return label
    }()
    
    private lazy var settingButton: UIButton = {
        let button = UIButton()
        button.setImage(UIImage(systemName: "gearshape"), for: .normal)
        button.addTarget(self, action: #selector(openSettings), for: .touchUpInside)
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    @objc private func openSettings() {
        print("button")
        let settingsViewController = SettingsViewController()
        navigationController?.pushViewController(settingsViewController, animated: true)
    }


}

// MARK: - setting iui methods
private extension ProfileViewController {
    func setupUI() {
        setupViews()
        setupConstraints()
        view.backgroundColor = .systemBackground
    }
    
    func setupViews() {
        view.addSubview(topView)
        topView.addSubview(topTitle)
        topView.addSubview(settingButton)
    }
    
    func setupConstraints() {
        topView.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(50)
            make.leading.trailing.equalToSuperview()
            make.height.equalToSuperview().multipliedBy(0.08)
        }
        
        topTitle.snp.makeConstraints { make in
            make.center.equalToSuperview()
        }
        
        settingButton.snp.makeConstraints { make in
            make.trailing.equalToSuperview().offset(-10)
            make.height.equalToSuperview().multipliedBy(0.4)
            make.width.equalToSuperview().multipliedBy(0.15)
            make.centerY.equalToSuperview()
        }
    }
}

But it turns out the problem was in the topview, but I don’t know exactly why. If my button is in the topview it is not pressed, but if outside then everything works fine.


Solution

  • Each of your tabs holds a UINavigationController ...

    So, your "Profile" tab has a UINavigationController with a root view controller of ProfileViewController ...

    You then add your topView as a subview of ProfileViewController's view, and you constrain it to the top of that view -- ignoring the safe-area, which includes the navigation bar.

    If I modify your code to add some background colors:

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        
        topView.backgroundColor = .yellow
        navigationController?.navigationBar.backgroundColor = .green.withAlphaComponent(0.5)
    }
    

    It looks like this:

    enter image description here

    You cannot tap a button (or any other UI element) that is covered by the navigation bar.

    If you want a "Title" and a right-side button on that navigation bar, manage that with the navigation bar...

    class ProfileViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemBackground
            
            // don't call anything in your current setupUI() func
            //setupUI()
            
            self.title = "Profile"
            let b = UIBarButtonItem(image: UIImage(systemName: "gearshape"), style: .plain, target: self, action: #selector(openSettings))
            navigationItem.rightBarButtonItem = b
        }
        
        @objc private func openSettings() {
            print("button")
            let settingsViewController = SettingsViewController()
            navigationController?.pushViewController(settingsViewController, animated: true)
        }
        
    }
    

    Now, it looks like this:

    enter image description here

    and tapping the Gear button will call openSettings()