-1

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.

Abhishek K
  • 15
  • 7
  • Do **not** use `UIDevice().userInterfaceIdiom` ... position your `movingSelectorView` relative to the frame of the selected "Day" button. – DonMag Aug 02 '23 at 19:40
  • Hi thanks for your reply. I've done this from other way. But i want to know that how this can be done by using frame of selected day. – Abhishek K Aug 03 '23 at 10:08
  • I think you need to add some detail showing what you are doing. It looks like you want to move the "selector" bar when you tap on a day button... but the code you posted is in `scrollViewDidScroll`? What is the view hierarchy? How are the buttons and the `movingSelectorView` related to each other? – DonMag Aug 03 '23 at 12:53
  • Actually what i am doing here in the app is, there are both options either user can scroll horizontally or user can tap on any button to scroll. So on the button tap i've already done code which is working as i wanted. But i was facing problem when user wants to scroll the screen instead of tapping any button to scroll. There are five different view for each button. Functionality is very similar to "Page menu" or like custom segment controller which supports swipes to move in left or right direction. – Abhishek K Aug 03 '23 at 18:02
  • That doesn't explain why you can't use the button frames to position the selector view. – DonMag Aug 03 '23 at 19:39
  • ok on the button click, earlier i was moving the movingSelectorView using below code `func moveSelectorViewToDayOne() { UIView.animate(withDuration: 0.4, delay: 0, options: UIView.AnimationOptions.curveEaseIn, animations: { // let newxPosition = self.scrollView.contentOffset.x // self.movingSelectorView.frame.origin.x = newxPosition/5 self.movingSelectorView.frame.origin.x = self.dayOneHolderView.frame.midX }) { animationComplete in print("Animation Complete")} }` – Abhishek K Aug 03 '23 at 20:02
  • above code works fine but the problem was that i was not able to move the movingSelectorView when user wants to move to next using scroll instead of tapping button. So for that here i used the scrollView delegate method "scrollViewDidScroll" and in that method i was not getting the idea to move the view with the scroll direction. – Abhishek K Aug 03 '23 at 20:06
  • You still haven't told us anything about your view hierarchy. The animated image you posted "jumps" at the start... are those 5 buttons *also* in a scroll view? Is the `movingSelectorView` in the same scroll view? Are you trying to move the `movingSelectorView` *while* you are scrolling, so if you've scrolled only part of the way the selctor view should be between two of the buttons? We need ***details*** ... preferably a [mre]. Please review [ask]. – DonMag Aug 03 '23 at 20:18

1 Answers1

1

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"

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Hi thanks for your answer. It is the perfect way of doing this. But I have done this by using different approach. – Abhishek K Aug 06 '23 at 19:07
  • I was also searching if there is a way to highlight the selected day or keep the selected day(button) alpha to 1 and changing other buttons alpha to 0.8(like dim as compared to selected button). I have tried it using `func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)` method but it is not working properly when user scrolls quickly 3-4 times. – Abhishek K Aug 06 '23 at 19:21
  • @AbhishekK - you say *"I have done this by using different approach"* ... are you asking about highlighting/dimming the "Day" buttons using **my approach** or a **different approach**? – DonMag Aug 07 '23 at 12:23
  • I am asking _about highlighting/dimming the "Day" buttons_ using your approach. So that i can get an idea to implement the same. – Abhishek K Aug 07 '23 at 13:01
  • @AbhishekK - in `scrollViewDidScroll()` I'm already calculating the *percentage* of the scroll view. Loop through the buttons, setting alpha or title color based on the same percentage. – DonMag Aug 07 '23 at 13:44
  • Thanks for the answer. I'll try to loop through the buttons. :) – Abhishek K Aug 07 '23 at 18:29