Reputation: 109
How can you find the view within an array of subviews that has the x coordinate closest to the center of the screen?
This is what I have:
extension UIScrollView {
func scrollToView(view:UIView, animated: Bool) {
if let origin = view.superview {
let childStartPoint = origin.convert(view.frame.origin, to: self)
self.scrollRectToVisible(CGRect(x: childStartPoint.x, y: 0, width: 1, height: self.frame.height), animated: animated)
}
}
}
Upvotes: 1
Views: 281
Reputation: 77433
You can find the "closest to center" subview by getting the "virtual center" of the scroll view - that is, the .contentOffset.x
plus 1/2 of the width of the frame:
// get the "virtual center" of the scroll view
let scrollCenterX = scrollView.contentOffset.x + scrollView.frame.width * 0.5
Then, find the "center" view by getting the minimum difference between the subviews centers and the "virtual center":
// find the subview with center.x closest to scrollCenterX
let closestToCenterView = viewsToTrack.min { a, b in abs(a.center.x - scrollCenterX) < abs(b.center.x - scrollCenterX) }
Here's a working example:
class CenterInScrollViewController: UIViewController {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .systemYellow
return v
}()
var viewsToTrack: [UIView] = []
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollView.heightAnchor.constraint(equalToConstant: 200.0),
scrollView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
let cg = scrollView.contentLayoutGuide
var prevView: UIView!
for i in 1...10 {
let v = UILabel()
v.text = "\(i)"
v.textAlignment = .center
v.backgroundColor = .cyan
v.layer.borderWidth = 1
v.layer.borderColor = UIColor.red.cgColor
v.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(v)
v.widthAnchor.constraint(equalToConstant: 120.0).isActive = true
v.heightAnchor.constraint(equalTo: scrollView.frameLayoutGuide.heightAnchor).isActive = true
if i == 1 {
v.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 10.0).isActive = true
} else {
v.leadingAnchor.constraint(equalTo: prevView.trailingAnchor, constant: 40.0).isActive = true
}
if i == 10 {
v.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -10.0).isActive = true
}
viewsToTrack.append(v)
prevView = v
}
scrollView.delegate = self
}
func centerMiddleView() -> Void {
// make sure we have views to find the centers
guard viewsToTrack.count > 0 else {
return
}
// reset view backgrounds to cyan
viewsToTrack.forEach { $0.backgroundColor = .cyan }
// get the "virtual center" of the scroll view
let scrollCenterX = scrollView.contentOffset.x + scrollView.frame.width * 0.5
// find the subview with center.x closest to scrollCenterX
let closestToCenterView = viewsToTrack.min { a, b in abs(a.center.x - scrollCenterX) < abs(b.center.x - scrollCenterX) }
// make sure we found one
if let v = closestToCenterView {
// set its background to yellow
v.backgroundColor = .yellow
// animate its center to the center of the scroll view via contentOffset
UIView.animate(withDuration: 0.3, animations: {
self.scrollView.contentOffset.x = v.center.x - self.scrollView.frame.width * 0.5
})
} else {
print("No center view? This shouldn't happen...")
}
}
}
extension CenterInScrollViewController: UIScrollViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
centerMiddleView()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// let DidEndDecelerating handle positioning,
// unless Dragging ended while "holding in-place"
if !decelerate {
centerMiddleView()
}
}
}
Note that unless you've added enough Leading and Trailing from the first and last subview, you won't be able to center those two views.
Upvotes: 1
Reputation: 30268
You could probably make this more efficient with zip
or something. But this is what I came up with:
func centerMiddleView() {
var closestDistance = CGFloat(9999)
var closestView = UIView()
let scrollViewCenterX = scrollView.bounds.width / 2
for colorView in colorViews { /// loop over views
if
let colorView = colorView,
let convertedCenter = colorView.superview?.convert(colorView.center, to: nil).x /// the view's center in terms of the screen
{
let distance = scrollViewCenterX - convertedCenter
if abs(distance) < closestDistance { /// this is closer than the previous view
closestDistance = distance
closestView = colorView
}
}
}
let closestViewCenter = closestView.center.x
let adjustedCenter = closestViewCenter - scrollViewCenterX
let offsetPoint = CGPoint(x: adjustedCenter, y: 0) /// ignore the Y coordinate
scrollView.setContentOffset(offsetPoint, animated: true)
}
You can call this when the user's finger lifts (you need both functions to handle when they are scrolling slowly):
extension ViewController: UIScrollViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
centerMiddleView()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
centerMiddleView()
}
}
colorViews
is an array of the views in the scrollview.
@IBOutlet weak var orangeView: UIView!
@IBOutlet weak var cherryView: UIView!
@IBOutlet weak var purpleView: UIView!
@IBOutlet weak var blueView: UIView!
@IBOutlet weak var greenView: UIView!
lazy var colorViews = [orangeView, cherryView, purpleView, blueView, greenView]
@IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self /// don't forget to set delegate
scrollView.contentInset = UIEdgeInsets(top: 0, left: 200, bottom: 0, right: 200) /// prevent glitch when centering an view at the edge
}
Result:
Upvotes: 1