How to get the view which is centered(or visible to user) inside UIScrollview?

60 Views Asked by At

I have a UIScrollview with horizontal pagination, inside which there are five different view. These are scrolling perfectly as required. And now i also have five buttons on the top of my screen, on button action the scrollview will scroll to the required page(for example if user tap on button 3, the scrollview will scroll to third page). So now I want a small view to work as a selectorView(that is if user scrolling to next page the selector view should move to next button during scroll)just below the five buttons. This is also working fine to some extent but there is small issue specially in iPad devices. The issue is that my selectorView is not finishing in the center of required button. How can it be in the center of required button. I have used below code to move the selectorView.

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let newxPosition = scrollView.contentOffset.x
    if UIDevice().userInterfaceIdiom == .phone {
        UIView.animate(withDuration: 0.1) { [self] in
            self.movingSelectorView.frame.origin.x = newxPosition/5 + (movingViewXConstant ?? 25)
        }
    } else if UIDevice().userInterfaceIdiom == .pad {
        UIView.animate(withDuration: 0.1) { [self] in
            self.movingSelectorView.frame.origin.x = newxPosition/5 + (movingViewXConstant ?? 75)
        }
    }
}

enter image description here

Please see the image uploaded for better understanding. The five buttons are not in the scrollview as well as the movingSelectorView is also not inside the scrollView.

1

There are 1 best solutions below

6
DonMag On BEST ANSWER

Without getting details from you on your view hierarchy and current code, I'll guess at something that you may find useful.

Let's:

  • put the "Day" buttons in a horizontal stack view, using Fill Equally
  • add that stack view as a subview of the main view
  • add the "selector" view as subview of the main view
  • add a scroll view with 5 "full width" views and paging enabled

We'll set the initial frame of the selector view to a size of (20, 3), and position it under the title label of the "Day" buttons.

The center.x value of the selector view frame will start at one-half of the width of the first Day button.

When the scroll view scrolls - whether being dragged or by calling .scrollRectToVisible on a button tap - we'll get the percentage it has scrolled, and update the center.x of the selector view to the same percentage of the total buttons width, offset by the "one-half button width" value.

So, example code:

class ViewController: UIViewController, UIScrollViewDelegate {
    
    let scrollView = UIScrollView()
    let btnStack = UIStackView()
    let movingSelectorView = UIView()

    // this will be one-half of the width of a "Day" button
    var btnCenterOffset: CGFloat = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // stack view to hold the "Day" buttons
        btnStack.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(btnStack)
        
        btnStack.distribution = .fillEqually

        scrollView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(scrollView)

        scrollView.isPagingEnabled = true
        
        // stack view to hold the 5 "Day" views in the scroll view
        let contentStackView = UIStackView()
        contentStackView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(contentStackView)
        
        let g = view.safeAreaLayoutGuide
        let cg = scrollView.contentLayoutGuide
        let fg = scrollView.frameLayoutGuide
        
        NSLayoutConstraint.activate([

            btnStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            btnStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            btnStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            btnStack.heightAnchor.constraint(equalToConstant: 50.0),

            scrollView.topAnchor.constraint(equalTo: btnStack.bottomAnchor, constant: 0.0),
            scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),

            contentStackView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 0.0),
            contentStackView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 0.0),
            contentStackView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: 0.0),
            contentStackView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: 0.0),
            
            contentStackView.heightAnchor.constraint(equalTo: fg.heightAnchor),
            
        ])
        
        // create 5 "Day" buttons and add them to the btnStack view
        for i in 1...5 {
            let b = UIButton()
            b.setTitle("Day \(i)", for: [])
            b.setTitleColor(.darkGray, for: .normal)
            b.setTitleColor(.lightGray, for: .highlighted)
            b.titleLabel?.font = .systemFont(ofSize: 14.0, weight: .bold)
            b.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
            btnStack.addArrangedSubview(b)
        }
        
        // create 5 views for the scroll view
        let colors: [UIColor] = [
            .yellow, .green, .systemBlue, .cyan, .systemYellow,
        ]
        for (i, c) in colors.enumerated() {
            let v = UILabel()
            v.font = .systemFont(ofSize: 80.0, weight: .regular)
            v.text = "\(i + 1)"
            v.textAlignment = .center
            v.backgroundColor = c
            contentStackView.addArrangedSubview(v)
            v.widthAnchor.constraint(equalTo: fg.widthAnchor).isActive = true
        }

        // movingSelectorView will partially cover the "Day" buttons frames, so
        //  don't let it capture touches
        movingSelectorView.isUserInteractionEnabled = false
        movingSelectorView.backgroundColor = .darkGray
        view.addSubview(movingSelectorView)
        
        scrollView.delegate = self
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        if let b = btnStack.arrangedSubviews.first {
            // set the frame size and initial position of the movingSelectorView
            movingSelectorView.frame = .init(x: 0.0, y: btnStack.frame.maxY - 12.0, width: 20.0, height: 3.0)
            // set btnCenterOffset to one-half the width of a "Day" button
            btnCenterOffset = b.frame.width * 0.5
            // move the selector view to the center of the first "Day" button
            movingSelectorView.center.x = btnCenterOffset + btnStack.frame.origin.x
        }
    }
    
    @objc func btnTapped(_ sender: UIButton) {
        guard let idx = btnStack.arrangedSubviews.firstIndex(of: sender) else { return }
        // "Day" button was tapped, so scroll the scroll view to the associated view
        let w: CGFloat = scrollView.frame.width
        let h: CGFloat = scrollView.frame.height
        let r: CGRect = .init(x: CGFloat(idx) * w, y: 0, width: w, height: h)
        scrollView.scrollRectToVisible(r, animated: true)
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        // get the percentage that the scroll view has been scrolled
        //  based on its total contentSize.width
        let pct = scrollView.contentOffset.x / scrollView.contentSize.width
        // move the selector view based on that percentage
        movingSelectorView.center.x = btnStack.frame.width * pct + btnCenterOffset + btnStack.frame.origin.x
    }
    
}

and how it looks when running:

enter image description here

This will work - with Zero code changes - independent of device / view size:

enter image description here

Note: This is EXAMPLE CODE ONLY!!! -- it is meant to help you get started, and is not intended to be "Production Ready"