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:

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

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