swiftuitableviewmultidimensional-arrayuiscrollviewuipagecontrol

Dynamically populating multiple TableViews using UIScrollView and UIPageControl (Swift 5)


I'm attempting to recreate something like in the iPhone's native weather app, where you can select multiple locations and they are appended as TableViews to a UIScrollView with UIPageControl

I'm starting simple with a hard-coded model in a 2D array. I want each array to go in each tableView, five tableViews total with one row in the first tableView, two in the second, etc:

var model: [[String]] = [
["TableView 1: row 1"],
["TableView 2: row 1", "TableView 2: row 2"],
["TableView 3: row 1", "TableView 3: row 2", "TableView 3: row 3"],
["TableView 4: row 1", "TableView 4: row 2", "TableView 4: row 3", "TableView 4: row 4"],
["TableView 5: row 1", "TableView 5: row 2", "TableView 5: row 3", "TableView 5: row 4", "TableView 5: row 5"]
]

Right now I can only wrap my head around something very simple, like what is shown below, so that the first tableView has 5 cells which say "TableView 1", then the second has five cells which say "TableView 2" and so on:

//number of rows
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 5
}

//cell for row
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    for i in 0...model.count-1{
        //grab the correct tableView from the array of tableViews
        if tableView == self.tableViews[i]{
        //dequeue the cell using a nib
            let cell = tableView.dequeueReusableCell(withIdentifier: ProtoTypeTableViewCell.identifier, for: indexPath) as! ProtoTypeTableViewCell
        //set text for the label in the nib file
            cell.label.text = "TableView \(i+1)"
            return cell
        }
    }
        return UITableViewCell()
}

Any help would be greatly appreciated; thank you.


Solution

  • This was quite an interesting question to me since I do like to recreate UIs of popular apps. I think there are a few ways you can achieve this, I will show you one way.

    I think the main challenges to solve:

    For the UI components, I have chosen to go this way:

    Here is how I went about doing this and added comments to explain

    Initial set up

    class PagingTableView: UIViewController
    {
        // Same model as yours
        var model: [[String]] = [
        ["TableView 1: row 1"],
            
        ["TableView 2: row 1",
         "TableView 2: row 2"],
            
        ["TableView 3: row 1",
         "TableView 3: row 2",
         "TableView 3: row 3"],
            
        ["TableView 4: row 1",
         "TableView 4: row 2",
         "TableView 4: row 3",
         "TableView 4: row 4"],
            
        ["TableView 5: row 1",
         "TableView 5: row 2",
         "TableView 5: row 3",
         "TableView 5: row 4",
         "TableView 5: row 5"]
        ]
        
        let scrollView: UIScrollView =
        {
            let scrollView = UIScrollView()
            scrollView.isPagingEnabled = true
            scrollView.translatesAutoresizingMaskIntoConstraints = false
            return scrollView
        }()
        
        let stackView: UIStackView =
        {
            let stackView = UIStackView()
            stackView.axis = .horizontal
            stackView.distribution = .equalSpacing
            stackView.translatesAutoresizingMaskIntoConstraints = false
            return stackView
        }()
        
        let pageControl = UIPageControl()
        
        let padding: CGFloat = 20
        let pageWidth = UIScreen.main.bounds.width
        
        let tableViewCellIdentifier = "cell"
        
        // Use this factor to unique identify your table
        let tableViewUniqueIdFactor = 1000
        
        // https://stackoverflow.com/a/21130486/1619193
        // You can ignore this function, created for convenience
        private func randomColor() -> UIColor
        {
            let red = CGFloat(arc4random_uniform(256)) / 255.0
            let blue = CGFloat(arc4random_uniform(256)) / 255.0
            let green = CGFloat(arc4random_uniform(256)) / 255.0
            
            return UIColor(red: red, green: green, blue: blue, alpha: 1.0)
        }
    
        override func viewDidLoad()
        {
            super.viewDidLoad()
            
            title = "Paging TableView"
            view.backgroundColor = .white
            
            // Configure everything, functions come later
            configureScrollViewLayout()
            configureStackViewLayout()
            configurePageControl()
            addTableViewsToStackView()
        }
    

    Configure your scroll view layout

    private func configureScrollViewLayout()
    {
        scrollView.delegate = self
        
        view.addSubview(scrollView)
        
        // Auto layout
        scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
            .isActive = true
        
        scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,
                                        constant: padding).isActive = true
        
        scrollView.widthAnchor.constraint(equalToConstant: pageWidth).isActive = true
        
        scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,
                                        constant: -padding * 3).isActive = true
    }
    

    Configure your stack view layout

    private func configureStackViewLayout()
    {
        scrollView.addSubview(stackView)
        
        // Auto layout
        
        stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
            .isActive = true
        
        stackView.topAnchor.constraint(equalTo: scrollView.topAnchor)
            .isActive = true
        
        stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor)
            .isActive = true
        
        stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
            .isActive = true
        
        stackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor,
                                          multiplier: 1).isActive = true
    }
    

    Configure your page control layout

    private func configurePageControl()
    {
        pageControl.numberOfPages = model.count
        pageControl.currentPage = 0
        pageControl.tintColor = randomColor()
        pageControl.pageIndicatorTintColor = randomColor()
        pageControl.currentPageIndicatorTintColor = randomColor()
        
        view.addSubview(pageControl)
        
        // Auto layout
        pageControl.translatesAutoresizingMaskIntoConstraints = false
        
        pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor)
            .isActive = true
        
        pageControl.topAnchor.constraint(equalTo: scrollView.bottomAnchor)
            .isActive = true
        
        pageControl.widthAnchor.constraint(equalToConstant: 200)
            .isActive = true
        
        pageControl.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
            .isActive = true
    }
    

    Configure and add table views to the stack view

        private func addTableViewsToStackView()
        {
            for modelIndex in 0 ..< model.count
            {
                let tableView = UITableView()
                
                tableView.translatesAutoresizingMaskIntoConstraints = false
                
                // Uniquely identify each table which will come in handy
                // when figuring out which model should be loaded for a specific
                // table view
                tableView.tag = tableViewUniqueIdFactor + modelIndex
                
                // Register a default UITableView Cell
                tableView.register(UITableViewCell.self,
                                   forCellReuseIdentifier: tableViewCellIdentifier)
                
                tableView.dataSource = self
                
                tableView.backgroundColor = randomColor()
                
                // remove additional rows
                tableView.tableFooterView = UIView()
                
                stackView.addArrangedSubview(tableView)
                
                tableView.widthAnchor.constraint(equalToConstant: pageWidth)
                    .isActive = true
                
                // height is calculated automatically based on the height 
                // of the stack view
            }
        }
        
        // end of the class PagingTableView
    }
    

    At this stage, if you run this, you were to run the app, you will see the set up is like the weather app without the data in it and the paging control is not wired yet but we have the same number of pages as models (5):

    Paging UIScrollView Paging UIStackView iOS Weather App Swift

    Now what's left is to wire up the page control and the tableview to the model

    Update the page view with the scrollview delegate

    extension PagingTableView: UIScrollViewDelegate
    {
        func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
        {
            // Make sure you don't do anything with table view scrolls
            // This is only a worry if the view controller is the table view's
            // delegate also
            if !(scrollView is UITableView)
            {
                let pageNumber = round(scrollView.contentOffset.x / scrollView.frame.size.width)
                pageControl.currentPage = Int(pageNumber)
            }
        }
    }
    

    Implement the table view data source to map the table to the model

    extension PagingTableView: UITableViewDataSource
    {
        func tableView(_ tableView: UITableView,
                       numberOfRowsInSection section: Int) -> Int
        {
            // Retrieve the correct model using the unique identifier created earlier
            let modelIndex = tableView.tag - tableViewUniqueIdFactor
            
            // Get the correct array needed from model
            let modelForeCurrentTable = model[modelIndex]
            
            return modelForeCurrentTable.count
        }
        
        func tableView(_ tableView: UITableView,
                       cellForRowAt indexPath: IndexPath) -> UITableViewCell
        {
            let cell = tableView.dequeueReusableCell(withIdentifier: tableViewCellIdentifier)!
            
            // Retrieve the correct model using the unique identifier created earlier
            let modelIndex = tableView.tag - tableViewUniqueIdFactor
            
            // Get the correct array needed from model
            let modelForeCurrentTable = model[modelIndex]
            
            cell.textLabel?.text = modelForeCurrentTable[indexPath.row]
            
            cell.backgroundColor = .clear
            
            return cell
        }
    }
    

    Now everything is set up and you get something similar to the weather app with the right data showing in each table view and the page control wired up as well:

    Paging UITableView UIScrollView Paging UIStackView iOS Weather App Swift

    Final comments

    You can skip the layout parts if you create your UI using frames or in storyboard. I tried to give you something from start to finish that you would run in isolation and you could play around with it by tweaking things.