How to make UIScrollView zoom in only one directio

2019-03-18 20:38发布

问题:

I'm attempting to make a UIScrollView only allow zooming in the horizontal direction. The scroll view is setup with a pure autolayout approach. The usual approach is as suggested in a number of Stack Overflow answers (e.g. this one) and other resources. I have successfully used this in apps before autolayout existed. The approach is as follows: in the scroll view's content view, I override setTransform(), and modify the passed in transform to only scale in the x direction:

override var transform: CGAffineTransform {
    get { return super.transform }
    set {
        var t = newValue
        t.d = 1.0
        super.transform = t
    }
}

I also reset the scroll view's content offset so that it doesn't scroll vertically during the zoom:

func scrollViewDidZoom(scrollView: UIScrollView) {
    scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: scrollView.frame.height)
    scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0.0)
}

This works very nicely when not using autolayout:

However, when using autolayout, the content offset ends up wrong during zooming. While the content view only scales in the horizontal direction, it moves vertically:

I've put an example project on GitHub (used to make the videos in this question). It contains two storyboards, Main.storyboard, and NoAutolayout.storyboard, which are identical except that Main.storyboard has autolayout turned on while NoAutolayout does not. Switch between them by changing the Main Interface project setting and you can see behavior in both cases.

I'd really rather not switch off autolayout as it solves a number of problems with implementing my UI in a much nicer way than is required with manual layout. Is there a way to keep the vertical content offset correct (that is, zero) during zooming with autolayout?

EDIT: I've added a third storyboard, AutolayoutVariableColumns.storyboard, to the sample project. This adds a text field to change the number of columns in the GridView, which changes its intrinsic content size. This more closely shows the behavior I need in the real app that prompted this question.

回答1:

Think I figured it out. You need to apply a translation in the transform to offset the centering UIScrollView tries to do while zooming.

Add this to your GridView:

var unzoomedViewHeight: CGFloat?
override func layoutSubviews() {
    super.layoutSubviews()
    unzoomedViewHeight = frame.size.height
}

override var transform: CGAffineTransform {
    get { return super.transform }
    set {
        if let unzoomedViewHeight = unzoomedViewHeight {
            var t = newValue
            t.d = 1.0
            t.ty = (1.0 - t.a) * unzoomedViewHeight/2
            super.transform = t
        }
    }
}

To compute the transform, we need to know the unzoomed height of the view. Here, I'm just grabbing the frame size during layoutSubviews() and assuming it contains the unzoomed height. There are probably some edge cases where that's not correct; might be a better place in the view update cycle to calculate the height, but this is the basic idea.



回答2:

Try setting translatesAutoresizingMaskIntoConstraints

func scrollViewDidZoom(scrollView: UIScrollView) {
    gridView.translatesAutoresizingMaskIntoConstraints = true
}

In the sample project it works. If this is not sufficient, try creating new constraints programmatically in scrollViewDidEndZooming: might help.

Also, if this does not help, please update the sample project so we can reproduce the problem with variable intrinsicContentSize()

This article by Ole Begemann helped me a lot
How I Learned to Stop Worrying and Love Cocoa Auto Layout

WWDC 2015 video
Mysteries of Auto Layout, Part 1
Mysteries of Auto Layout, Part 2

And so luckily, there's a flag for that. It's called translatesAutoResizingMask IntoConstraints [without space].

It's a bit of a mouthful, but it pretty much does what it says. It makes views behave the way that they did under the Legacy Layout system but in an Auto Layout world.



回答3:

Although @Anna's solution works, UIScrollView provides a way of working with Auto Layout. But because scroll views works a little differently from other views, constraints are interpreted differently too:

  • Constraints between the edges/margins of scroll view and its contents attaches to the scroll view's content area.
  • Constraints between the height, width, or centers attach to the scroll view’s frame.
  • Constraints between scroll view and views outside scroll view works like an ordinary view.

So, when you add a subview to the scroll view pinned to its edges/margins, that subview becomes the scroll view's content area or content view.

Apple suggests the following approach in Working with Scroll Views:

  1. Add the scroll view to the scene.
  2. Draw constraints to define the scroll view’s size and position, as normal.
  3. Add a view to the scroll view. Set the view’s Xcode specific label to Content View.
  4. Pin the content view’s top, bottom, leading, and trailing edges to the scroll view’s corresponding edges. The content view now defines the scroll view’s content area.
  5. (...)
  6. (...)
  7. Lay out the scroll view’s content inside the content view. Use constraints to position the content inside the content view as normal.

In your case, the GridView must be inside the content view:

You can keep the grid view constraints that you have been using but, now, attached to content view. And for the horizontal-only zooming, keep the code exactly as it is. Your transform overriding handle it very well.



回答4:

I'm not sure if this satisfies your requirement of 100% autolayout, but here's one solution, where the UIScrollView is set with autolayout and gridView added as a subview to it.

Your ViewController.swift should look like this:

// Add an outlet for the scrollView in interface builder.
@IBOutlet weak var scrollView: UIScrollView!

// Remove the outlet and view for gridView.
//@IBOutlet var gridView: GridView!

// Create the gridView here:
var gridView: GridView!

// Setup some views
override func viewDidLoad() {
    super.viewDidLoad()
    // The width for the gridView is set to 1000 here, change if needed.
    gridView = GridView(frame: CGRect(x: 0, y: 0, width: 1000, height: self.view.bounds.height))
    scrollView.addSubview(gridView)
    scrollView.contentSize = CGSize(width: gridView.frame.width, height: scrollView.frame.height)
    gridView.contentMode = .ScaleAspectFill
}

func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
    return gridView
}

func scrollViewDidZoom(scrollView: UIScrollView) {
    scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0.0)
    scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: scrollView.frame.height)
}