Ryan
Ryan

Reputation: 109

Swift Set Scrollview Subviews Frame Location based off content offset

Problem:

I am re-creating the iPhone App Switcher page where the app views collapse on top of each other to the left side of the screen. It looks like the x location of each app view's frame is set based off it's index and location on the visible screen.

I have an array of views within a scroll view. How would you set the frame of the views to replicate the iPhone app switching page?

Attempt:

`func scrollViewDidScroll(_ scrollView: UIScrollView) {
    items.enumerated().forEach { (index, tabView) in
        let screenWidth = UIScreen.main.bounds.width
        
        // This returns a value between 0 and 1 depending on the location of the tab view within the visible screen
        let xOffset = scrollView.convert(CGPoint(x: tabView.frame.minX, y: 0), to: view).x
        let percentViewMovedOnVisibleScreen: CGFloat = xOffset / screenWidth
        
        // Spacing - NOT CORRECT
        let someSpacingAmount: CGFloat = 80
        tabView.frame.origin.x = CGFloat(index) * percentViewMovedOnVisibleScreen * someSpacingAmount
    }
}`

I think this is close but it doesn't quite get you to what Apple has. Maybe something other than didScroll is needed to make it feel smooth?

enter image description here

GitHub Sample Project: https://github.com/Alexander-Frost/ViewContentOffset

Upvotes: 4

Views: 999

Answers (1)

DonMag
DonMag

Reputation: 77433

It's very possible (likely, I'd say) that this is not begin done in a scroll view.

Consider this example:

class TestSwitcherViewController: UIViewController {
    
    var leadConstraint: NSLayoutConstraint!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        let colors: [UIColor] = [
            // light red
            UIColor(red: 1.0, green: 0.5, blue: 0.5, alpha: 1.0),
            // light green
            UIColor(red: 0.5, green: 1.0, blue: 0.5, alpha: 1.0),
            // light blue
            UIColor(red: 0.0, green: 0.5, blue: 1.0, alpha: 1.0),
            // light orange
            UIColor(red: 0.9, green: 0.7, blue: 0.5, alpha: 1.0),
            // yellow
            UIColor(red: 1.0, green: 1.0, blue: 0.0, alpha: 1.0),
        ]
        
        var prevView: UIView!
        
        // create a view for each color (a label with the view number as its text)
        // for each of the views
        //  if it's the first one,
        //      constrain leading to view leading
        //  else
        //      constrain leading to previous view leading, constant 1.0, multiplier 2.0

        var i = 1
        colors.forEach { c in
            let v = UILabel()
            v.backgroundColor = c
            v.text = "\(i)"
            v.textAlignment = .center
            v.layer.cornerRadius = 20
            v.layer.masksToBounds = true
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
            v.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
            v.widthAnchor.constraint(equalToConstant: 200.0).isActive = true
            v.heightAnchor.constraint(equalToConstant: 400.0).isActive = true
            if i == 1 {
                leadConstraint = v.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0)
                leadConstraint.isActive = true
            } else {
                NSLayoutConstraint(item: v, attribute: .leading, relatedBy: .equal, toItem: prevView, attribute: .leading, multiplier: 2.0, constant: 1.0).isActive = true
            }
            prevView = v
            i += 1
        }
        
        // add a pan gesture recognizer to the view
        let pan = UIPanGestureRecognizer(target: self, action: #selector(self.didPan(_:)))
        view.addGestureRecognizer(pan)
        
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        updateScales()
    }
    
    @objc func didPan(_ gesture: UIPanGestureRecognizer) -> Void {
        
        let translation = gesture.translation(in: view)
        
        // increment or decrement the leading anchor constant
        let tmpX = leadConstraint.constant + (translation.x * 0.25)

        // don't let it go past either side
        leadConstraint.constant = min(max(tmpX, 0.0), view.frame.width - 200.0)
        
        gesture.setTranslation(.zero, in: view)

        updateScales()
        
    }
    
    func updateScales() -> Void {
        view.subviews.forEach { v in
            // percentage of distance from leading edge of label to 1/5th width of view
            let pct = min(v.frame.origin.x / (view.frame.width * 0.2), 1.0)
            let scale = 0.8 + 0.2 * pct
            v.transform = .identity
            v.transform = CGAffineTransform(scaleX: scale, y: scale)
        }
    }
    
}

It creates 5 colored views (numbered labels), and constrains them to each other using Leading anchors with both a Constant and a Multiplier. It adds a Pan Gesture to the view, so when you pan left or right it modifies the Constant of the Leading constraint of the "bottom" view to move it left / right. This in turn moves the other views... and since we're using a multiplier for the constraint, the movement "grows" as we slide to the right.

Here's how it looks on launch:

enter image description here

and here's how it looks after dragging a bit to the right:

enter image description here

Obviously, this would take a lot more work to replicate all the functionality of the App Switcher, but it might get you on your way.

Upvotes: 1

Related Questions