skip to Main Content

I’ve got two overlapping UITableViews with only vertical scrolling. I want specialized coordinated scrolling across the two tables. I took the front table (in the view layering) and set isUserInteractionEnabled = false and added a scrollViewDidScroll() method to the back one. The front table ignore all gestures, so any pan gesture/scrolling would trigger on the back table. As scrolling occurred my scrollViewDidScroll() method would get called and I would check the contentOffset and scroll (set contentOffset) on the other table view as needed. It worked great.

enter image description here

Until…. the design added buttons to the front table view. I can no longer use isUserInteractionEnabled = false because this ignores taps and buttons won’t work. I need allow for taps, but not respond to pans(scrolling). I thought it would be fairly easily, but I’m either missing something simple or it’s more complicated than I would expect.

I’ve tried:

  • Disabling scrolling on the front table (pan gestures get ignored, but don’t fall through to the back table)
  • Overriding the pan gesture in the front table and applying the offset to the back table (momentum doesn’t work properly)
  • setting the UIPanGestureRecognizer from the back table on the front one (gesture recognizers can only belong to one view)
  • sub-classing the front table and doing an override of gestureRecognizerShouldBegin to return false for pan gestures (pan gestures don’t fall through to the back table)
  • sub-classing the front table and doing an overrider for touchesBegan (moved, ended, etc) and calling the methods on the back table (no effect)
  • and a few other things

I’m stumped on this. Help appreciated.

Edit: The image is from a simplified bit of code for a proof of concept. The real code has the top/front table changes contentOffset table height dependent on what’s visible in the bottom/back table.

2

Answers


  1. So, if u don’t wanna allow users to touch and drag the TOP table to scroll, but still allow them to tap on the button.

    Try using this:
    isScrollEnabled

    The default value is true, which indicates that scrolling is enabled. Setting the value to false disables scrolling.

    When scrolling is disabled, the scroll view doesn’t accept touch events; it forwards them up the responder chain.

    Login or Signup to reply.
  2. Here’s a quick example of two table views – not overlapping each other.

    We’ll use the same cell class for both "top" and "bottom" table views, just so we can confirm that we can tap the buttons in the cells in both tables.

    class CommonCell: UITableViewCell {
        
        static let identifier: String = "commonCell"
        
        // closure for button tap
        var gotTap: ((UITableViewCell) -> ())?
        
        var isTop: Bool = true { didSet {
            var cfg = btn.configuration
            cfg?.baseBackgroundColor = isTop ? .systemBlue : .systemRed
            btn.configuration = cfg
        }}
        
        var btn: UIButton!
        
        let theLabel: UILabel = UILabel()
        
        var myString: String = "" {
            didSet {
                theLabel.text = myString + " Count: (self.tapCount)"
            }
        }
        var tapCount: Int = 0 {
            didSet {
                theLabel.text = myString + " Count: (self.tapCount)"
            }
        }
        
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() -> Void {
            
            var cfg = UIButton.Configuration.filled()
            cfg.title = "Button"
            btn = UIButton(configuration: cfg)
            btn.addAction (
                UIAction { _ in
                    self.tapCount += 1
                    // call the closure so the controller can
                    //  update the data source
                    self.gotTap?(self)
                }, for: .touchUpInside
            )
            
            [theLabel, btn].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                contentView.addSubview(v)
            }
            
            let g = contentView.layoutMarginsGuide
            NSLayoutConstraint.activate([
                theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                theLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),
                
                btn.leadingAnchor.constraint(equalTo: theLabel.trailingAnchor, constant: 0.0),
                btn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                btn.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
                btn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            ])
            
        }
    }
    
    
    class SynchedTablesVC: UIViewController, UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate {
        
        let topTable: UITableView = UITableView()
        let bottomTable: UITableView = UITableView()
    
        let topHeight: CGFloat = 226.0
        
        var tapCounts: [[Int]] = []
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            tapCounts = Array(repeating: Array(repeating: 0, count: 25), count: 2)
            
            [topTable, bottomTable].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                view.addSubview(v)
            }
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                
                topTable.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
                topTable.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                topTable.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                topTable.heightAnchor.constraint(equalToConstant: topHeight),
                
                bottomTable.topAnchor.constraint(equalTo: topTable.bottomAnchor, constant: 0.0),
                bottomTable.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                bottomTable.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                bottomTable.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
                
            ])
    
            topTable.register(CommonCell.self, forCellReuseIdentifier: CommonCell.identifier)
            bottomTable.register(CommonCell.self, forCellReuseIdentifier: CommonCell.identifier)
    
            [topTable, bottomTable].forEach { v in
                v.dataSource = self
                v.delegate = self
            }
            
            // bottom table needs to be able to scroll up far enough
            //  to show bottom row of top table
            bottomTable.contentInset.bottom = topHeight
        }
        
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return tapCounts[section].count
        }
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let c = tableView.dequeueReusableCell(withIdentifier: CommonCell.identifier, for: indexPath) as! CommonCell
            if tableView == topTable {
                c.isTop = true
                c.myString = "Top Row: (indexPath.row)"
                c.tapCount = tapCounts[0][indexPath.row]
                // closure for button tap
                c.gotTap = { [weak self] c in
                    guard let self = self,
                          let cell = c as? CommonCell,
                          let iPath = self.topTable.indexPath(for: cell)
                    else { return }
                    tapCounts[0][iPath.row] = cell.tapCount
                }
                c.contentView.backgroundColor = .init(red: 1.0, green: 0.75, blue: 0.75, alpha: 1.0)
            } else {
                c.isTop = false
                c.myString = "Bottom Row: (indexPath.row)"
                c.tapCount = tapCounts[1][indexPath.row]
                // closure for button tap
                c.gotTap = { [weak self] c in
                    guard let self = self,
                          let cell = c as? CommonCell,
                          let iPath = self.bottomTable.indexPath(for: cell)
                    else { return }
                    tapCounts[1][iPath.row] = cell.tapCount
                }
                c.contentView.backgroundColor = .init(red: 0.75, green: 1.0, blue: 0.75, alpha: 1.0)
            }
            return c
        }
    
        // track which table view to use for content offset when scrolling
        var activeTableView: UITableView!
        var inActiveTableView: UITableView!
    
        func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
            if let tblView = scrollView as? UITableView {
                if tblView == topTable {
                    activeTableView = topTable
                    inActiveTableView = bottomTable
                } else {
                    activeTableView = bottomTable
                    inActiveTableView = topTable
                }
            }
        }
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            if let tblView = scrollView as? UITableView {
                if tblView == activeTableView {
                    inActiveTableView.contentOffset.y = tblView.contentOffset.y
                }
            }
        }
    }
    

    Looks like this:

    enter image description here

    enter image description here

    And, animation capture (too big to post here): https://imgur.com/a/V1yqj70

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search