Search code examples
iosswiftuicontainerview

Swift: How to update data in Container View without storyboard


My project is created programmatically without using storyboard. And it is like Apple Music's miniPlayer, when clicking a row in tableView, will update the data of miniPlayer(which is in containerView).

I see some examples with storyboard and segue like below code: call child viewController's method in parent viewController to update data by using protocol & delegate.

But I don't use storyboard, so what is the alternative code to prepare()?

 protocol ContentDelegate {
    func updateContent(id: Int)
 }

 class ParentViewController: UIViewController {
    var delegate: ContentDelegate?

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

        if (segue.identifier == "containerController") {
            let containerVC = segue.destination  as!   ChildContainerViewController

            self.delegate = containerVC
        }        
    }
}

class ChildContainerViewController: UIViewController, ContentDelegate {
   func updateContent(id: Int) {
     // your code
   }
}

My Code: add container view in the root view controller(UITabViewController).

class ViewController: UITabBarController {
    
    // mini player
    var miniPlayer: MiniPlayerViewController?
    
    // container view
    var containerView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // set tabBar and other stuff
        ...
        configureContainer()   
    }
    
    func configureContainer() {
        
        // add container
        containerView = UIView()
        containerView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(containerView)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            containerView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            containerView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            containerView.bottomAnchor.constraint(equalTo: tabBar.topAnchor),
            containerView.heightAnchor.constraint(equalToConstant: 64.0)
        ])
        
        // add child view controller view to container
        miniPlayer = MiniPlayerViewController()
        guard let miniPlayer = miniPlayer else { return }
        addChild(miniPlayer)
        miniPlayer.view.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(miniPlayer.view)
        
        
        // Create and activate the constraints for the child’s view.
        guard let miniPlayerView = miniPlayer.view else { return }
        NSLayoutConstraint.activate([
            miniPlayerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            miniPlayerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            miniPlayerView.topAnchor.constraint(equalTo: containerView.topAnchor),
            miniPlayerView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
        ])
        
        miniPlayer.didMove(toParent: self)
    }
}

I want to trigger the update when clicking the row in parentView.

protocol ContentDelegate {
    func configure(songs: [Song]?, at index: Int)
}

class SongsListViewController: UIViewController {
    private var tableView: UITableView!
    var delegate: ContentDelegate?
    
    // MARK: - data source
    var songs = [Song]()
    . . .

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let index = indexPath.row
        let vc = MiniPlayerViewController()
        self.delegate = vc
        self.delegate?.configure(songs: songs, at: index)
//        present(vc, animated: true)
    }

The update method in child view.

extension MiniPlayerViewController {
    
    func configure(songs: [Song]?, at index: Int) {
        if let songs = songs {
            let song = songs[index]
            songTitle.text = song.title
            thumbImage.image = song.artwork?.image
        } else {
            // placeholder fake info
            songTitle.text = "你在终点等我"
            thumbImage.image = UIImage(named: "Wang Fei")
        }
    }
}

Solution

  • There is more than one approach to this...


    First approach - no custom delegate:

    Use the subclassed UITabBarController as an "intermediary". Give it a func such as:

    func configure(songs: [Song]?, at index: Int) -> Void {
        miniPlayer.configure(songs: songs, at: index)
    }
    

    then, in your "Select Song" view controller (one of the tabs):

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let tbc = self.tabBarController as? CustomTabBarController else {
            return
        }
        let index = indexPath.row
        tbc.configure(songs: songs, at: index)
    }
    

    Second approach - using a custom delegate:

    protocol ContentDelegate {
        func configure(songs: [Song]?, at index: Int)
    }
    

    Make sure your "mini player" controller conforms to the delegate:

    class MiniPlayerViewController: UIViewController, ContentDelegate {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // add UI elements, any other setup code
        }
        
    }
    
    extension MiniPlayerViewController {
        
        func configure(songs: [Song]?, at index: Int) {
            if let songs = songs {
                let song = songs[index % songs.count]
                songTitle.text = song.title
                thumbImage.image = song.artwork
            } else {
                // placeholder fake info
                songTitle.text = "你在终点等我"
                thumbImage.image = UIImage(named: "Wang Fei")
            }
        }
    
    }
    

    Give your "Select Song" view controller (and any other of the tab controllers) a delegate var:

    class SelectSongViewController: UIViewController {
    
        var delegate: ContentDelegate?
    
        // everything else
    }
    

    then, in your subclassed UITabBarController:

    override func viewDidLoad() {
        super.viewDidLoad()
    
        configureContainer()
    
        if let vc = viewControllers?.first as? SelectSongViewController {
            vc.delegate = miniPlayer
        }
        
    }
    

    now your "Select Song" view controller can call the delegate func:

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let tbc = self.tabBarController as? CustomTabBarController else {
            return
        }
        let index = indexPath.row
        delegate?.configure(songs: songs, at: index)
    }