UIScrollView缩放与自动布局不兼容

38

在使用纯自动布局的环境下,使用UIScrollView进行缩放似乎不起作用。

这尤其令人沮丧,因为iOS 6版本说明肯定让我相信它应该在这里的“纯自动布局方法”中起作用:http://developer.apple.com/library/ios/#releasenotes/General/RN-iOSSDK-6_0/_index.html

我查看了WWDC 2012年的演示文稿,包括202、228和232会话,但没有找到答案。

互联网上唯一与此问题相关的问题是UIScrollView zooming with Auto Layout,但它没有提供问题的代码,也没有答案。

这位用户https://stackoverflow.com/users/341994/matt对UIScrollView自动布局问题给出了很多很好的回答,甚至链接到了git hub上的代码,但我无法找到任何解决此问题的内容。

我已经试图将这个问题简化到绝对最小,以使它清晰明了。

我创建了一个带有storyboard的新单视图应用程序,在接口生成器中没有进行任何更改。

我向项目添加了一个大的图片文件“pic.jpg”。

SVFViewController.h

#import <UIKit/UIKit.h> 
@interface SVFViewController : UIViewController <UIScrollViewDelegate>
@property (nonatomic) UIImageView *imageViewPointer;
@end

SVFViewController.m

#import "SVFViewController.h"

@interface SVFViewController ()

@end

@implementation SVFViewController


- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    UIScrollView *scrollView = [[UIScrollView alloc] init];
    UIImageView *imageView = [[UIImageView alloc] init];
    [imageView setImage:[UIImage imageNamed:@"pic.jpg"]];
    [self.view addSubview:scrollView];
    [scrollView addSubview:imageView];

    scrollView.translatesAutoresizingMaskIntoConstraints = NO;
    imageView.translatesAutoresizingMaskIntoConstraints = NO;
    self.imageViewPointer = imageView;
    scrollView.maximumZoomScale = 2;
    scrollView.minimumZoomScale = .5;
    scrollView.delegate = self;

    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(scrollView,imageView);
    NSLog(@"Current views dictionary: %@", viewsDictionary);
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
    [scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[imageView]|" options:0 metrics: 0 views:viewsDictionary]];
    [scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[imageView]|" options:0 metrics: 0 views:viewsDictionary]];
}

-(UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView{
    return self.imageViewPointer;
}

@end

注意我特别努力让这段代码与iOS 6发布说明提供的示例代码尽可能相似,只是做了最低限度的实现缩放。

那么问题呢?

当您运行此应用程序并在滚动视图中移动时,一切都很好。但当您进行缩放时,问题就显而易见了,图像会来回闪烁,并且图像在滚动视图中的位置每次缩放时都会变得更加错误。

看起来imageView的内容偏移量正在发生冲突,似乎两个不同的事物每次“缩放”时都将其设置为不同的值。(imageView内容偏移属性的NSLog似乎证实了这一点)。

我在这里做错了什么?有人知道如何在纯自动布局环境中正确实现UIScrollView的缩放吗?是否有任何相关示例可供使用?

请帮忙解决。


我看到了这个链接(http://developer.apple.com/library/ios/#samplecode/PhotoScroller/Introduction/Intro.html),它让我感到不安,没有回答我的问题。是的,它使用autolayout实现了UIScrollView的缩放,但它通过一种我无法理解的复杂方式来子类化UIScrollView。一定有更简单的方法来做到这一点... - Corey
1
我也遇到了同样的问题。由于自动布局,将UIImageView嵌入UIScrollView不再像以前那么简单。 - Diego Allen
6个回答

14

再次阅读iOS SDK 6.0版本说明,我发现:

请注意,通过创建视图与滚动视图子树之外的视图(例如滚动视图的superview)之间的约束,您可以使滚动视图的子视图显示为悬浮(而不是滚动)在其他滚动内容上方。

解决方法

将您的子视图连接到外部视图,也就是嵌入滚动视图中的视图。

通过以下方式应用约束条件,我已经使它工作了:

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[imageView(width)]" options:0 metrics:@{@"width":@(self.imageViewPointer.image.size.width)} views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[imageView(height)]" options:0 metrics:@{@"height":@(self.imageViewPointer.image.size.height)} views:viewsDictionary]];

我尝试实现这个解决方案,但是在缩放时出现了“无法同时满足约束”的消息。我希望能够进一步调试,但不幸的是我不理解你的方法。 - Corey
感谢您的反馈。我已经更新了答案。确实,一个约束条件(“|”)是多余的。 - Mark Kryzhanouski
是的,这并不合乎逻辑且难以理解,但它确实有效。看起来这似乎是苹果的一个 bug。希望他们将来会改变 UIScrollView 的行为。 - Mark Kryzhanouski
2
这是一个简单的项目,它能够正常工作,我希望它能够符合您的期望。https://dl.dropbox.com/u/52101940/Test.zip - Mark Kryzhanouski
@MarkKryzhanouski 对于仅缩放效果,这个方法非常好用。但是我在对带有缩放和旋转的ImageView进行操作时遇到了问题。如果我使用手势旋转图像,然后尝试缩放,有时它会超出ScrollView的空间而消失。你能帮我解决这个问题吗? - Van
显示剩余4条评论

10

问题出在缩放过程中图片视图的位置发生了改变。在缩放期间,图片视图的原始位置将变为负值。我相信这就是产生不稳定运动的原因。此外,在缩放完成后,imageView仍处于错误的位置,这意味着滚动会显示偏移。

如果您实现-(void) scrollViewDidZoom:(UIScrollView *)scrollView并记录UIImageView的框架,您会看到其起点正在改变。

最终,我通过实现此策略来解决问题,并在缩放时改变了contentView的框架。

-(void) scrollViewDidZoom:(UIScrollView *)scrollView {
    CGRect cFrame = self.contentView.frame;
    cFrame.origin = CGPointZero;
    self.contentView.frame = cFrame;
}

非常感谢您的评论!不幸的是,我仍然无法根据您提供的策略使我上面所述的简单照片示例正常工作。 - Corey
3
知悉 - Github链接已损坏。 - powers

4
这些解决方案都算有效。以下是我采取的方法,无需使用任何hack或子类化,使用以下设置即可:
[view]
  [scrollView]
    [container]
      [imageView]
      [imageView2]
  1. In IB, hook up top, leading, bottom and trailing of scrollView to view.
  2. Hook up top, leading, bottom and trailing of container to scrollView.
  3. Hook up center-x and center-y of container to center-x and center-y of scrollView and mark it as remove on build time. This is only needed to silence the warnings in IB.
  4. Implement viewForZoomingInScrollView: in the view controller, which should be scrollView's delegate, and from that method return container.
  5. When setting the imageView's image, determine minimum zoom scale (as right now it will be displayed at the native size) and set it:

    CGSize mySize = self.view.bounds.size;
    CGSize imgSize = _imageView.image.size;
    CGFloat scale = fminf(mySize.width / imgSize.width,
                          mySize.height / imgSize.height);
    _scrollView.minimumZoomScale = scale;
    _scrollView.zoomScale = scale;
    _scrollView.maximumZoomScale = 4 * scale;
    

这对我很有效,在设置图像时,它会将滚动视图缩放以显示整个图像,并允许将其放大到初始大小的4倍。


嗯...我喜欢你的方法。但是告诉我,为什么容器里有两个imageView?另外,你不用将它们连接到某个地方吗?它们的约束条件是什么? - Mihai Fratu
这只是一个例子,你可以添加任何东西到 container(因为那是被缩放的视图)。那些图像视图上的约束条件应该是你需要的,以便所有边都连接在一起。 - Pascal
是的,我无法在iOS7或iOS8上使其正常工作。某些奇怪的事情发生了,图像没有居中在UIScrollView中。而且当缩放时,一切都变得完全错误...你能用吗? - Mihai Fratu
我必须保持中心约束,以使其正常工作(iOS 11)。 - d4Rk
太好了!谢谢,我原本认为在四个边缘上已经使用约束锚定了,将中心点固定会变得多余,但是在缩放的情况下,它可以完美地优化行为! - Laser

1
假设您在故事板中有一个 "UIScrollView" 内部有一个 "UIImageView",并且将 "UIScrollView" 中的所有约束与视图控制器链接,并将 UIView 中的两个约束 (Horizontal Space - Scroll View - ViewHorizontal Space - Scroll View - View) 也连接起来。将视图控制器设置为 "UIScrollView" 的委托。然后实现以下代码:
@interface VC () <UIScrollViewDelegate>
@property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray* constraints;
@end

@implementation FamilyTreeImageVC

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.scrollView removeConstraints:self.constraints];
}


- (UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView {
    return self.imageView;
}

-(void) scrollViewDidZoom:(UIScrollView *)scrollView {
    CGRect cFrame = self.imageView.frame;
    cFrame.origin = CGPointZero;
    self.imageView.frame = cFrame;
}

在我的情况下,我不需要链接UIView中的水平空间,但其他部分都解决了! - Jesse
@Hizabr 为什么你要在viewDidLoad中移除约束? - AlexanderN

0

当我尝试使用仅scrollView从storyboard项目实现缩放时,我遇到了同样的问题。

我通过添加单独的捏合手势识别器来解决它。我只是从工具箱中将其拖到我的场景上。然后我将它连接到一个名为“doPinch”的操作上,该操作实现了缩放。我将它连接到了一个名为“pinchRecognizer”的输出口,以便我可以访问它的比例属性。这似乎覆盖了scrollView的内置缩放,跳动消失了。也许它不会犯同样的起源错误,或者更优雅地处理它们。这在IB布局之上非常少的工作。

一旦您将捏合手势识别器引入场景,您确实需要action和viewForZoomingInScrollView方法。如果省略其中任何一个,缩放就会停止工作。

我的视图控制器中的代码如下:

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.zoomableImage;
}

- (IBAction)doPinch:(id)sender

{
NSLog(@"In the pinch action now with scale: %f", self.pinchRecognizer.scale);
[scrollView setZoomScale:self.pinchRecognizer.scale animated:NO];
}

这个非常基本的实现有一个副作用:当您返回到缩放的图像并进一步缩放时,比例尺的值为1.0f,因此它会跳回原始比例。
您可以通过引入一个名为“currentScale”的属性来解决这个问题,以跟踪比例尺,并在再次开始缩放时设置捏合手势识别器比例尺。您需要使用手势识别器的state属性。
- (IBAction)doPinch:(id)sender
{
NSLog(@"In the pinch action now with scale: %f", self.pinchRecognizer.scale);
NSLog(@"Gesture recognizer state is: %d", self.pinchRecognizer.state);

switch (self.pinchRecognizer.state)
{
    case 1:
        NSLog(@"Zoom begins, with current scale set to: %f", self.currentScale);
        [self.pinchRecognizer setScale:self.currentScale];
        break;
    case 3:
        NSLog(@"Zoom ends, with pinch recognizer scale set to: %f", self.pinchRecognizer.scale);
        self.currentScale = self.pinchRecognizer.scale;
    default:
        break;
}

[scrollView setZoomScale:self.pinchRecognizer.scale animated:NO];
}

这个pinch具体附着在哪里?ScrollView上?ImageView上?还是整个View上? - Matt Mc
抱歉回复晚了 - 没有注意到收件箱!捏合手势识别器已添加到storyboard场景中。它似乎位于顶层并且可以检测该场景中任何可见视图上的捏合手势。 - Tim

0

这是我设法解决的问题。

以下是原始内容和我的更改:

@interface ScrollViewZoomTestViewController () <UIScrollViewDelegate>

@property (nonatomic, strong) UIImageView* imageViewPointer;

// These properties are new
@property (nonatomic, strong) NSMutableArray* imageViewConstraints;
@property (nonatomic) BOOL imageViewConstraintsNeedUpdating;
@property (nonatomic, strong) UIScrollView* scrollViewPointer;

@end

@implementation ScrollViewZoomTestViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIScrollView *scrollView = [[UIScrollView alloc] init];
    UIImageView *imageView = [[UIImageView alloc] init];
    [imageView setImage:[UIImage imageNamed:@"pic.jpg"]];
    [self.view addSubview:scrollView];
    [scrollView addSubview:imageView];

    scrollView.translatesAutoresizingMaskIntoConstraints = NO;
    imageView.translatesAutoresizingMaskIntoConstraints = NO;
    self.imageViewPointer = imageView;
    // New
    self.scrollViewPointer = scrollView;
    scrollView.maximumZoomScale = 2;
    scrollView.minimumZoomScale = .5;
    scrollView.delegate = self;

    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(scrollView, imageView);
    NSLog(@"Current views dictionary: %@", viewsDictionary);
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];

    // Saving the image view width & height constraints
    self.imageViewConstraints = [[NSMutableArray alloc] init];
    // Constrain the image view to be the same width & height of the scroll view
    [_imageViewConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[imageView(scrollView)]|" options:0 metrics: 0 views:viewsDictionary]];
    [_imageViewConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[imageView(scrollView)]|" options:0 metrics: 0 views:viewsDictionary]];

    // Add the image view constraints to the VIEW, not the scroll view
    [self.view addConstraints:_imageViewConstraints];

    // Flag
    self.imageViewConstraintsNeedUpdating = YES;
}

所以在这里,我将所有的约束条件添加到self.view中,在一个NSMutableArray属性中保存UIImageView上设置的约束,并设置一个标志,表示需要更新UIImageView的约束。
这些初始的UIImageView约束用于设置其起始状态。它的宽度和高度与UIScrollView相同。然而,这不允许我们缩放图像视图。它将保持与滚动视图相同的宽度/高度。这不是我们想要的。这就是为什么我保存约束并设置标志的原因。我们一会儿会处理它。
现在,设置视图进行缩放:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
    return self.imageViewPointer;
}

好的,现在我们需要实际允许缩放。我正在删除初始的UIImageView约束,并添加一些新的约束,这次是限制到UIScrollViewcontentSizewidthheight

- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view
{
    if(_imageViewConstraintsNeedUpdating)
    {
        // Remove the previous image view constraints
        [self.view removeConstraints:_imageViewConstraints];

        // Replace them with new ones, this time constraining against the `width` & `height` of the scroll view's content, not the scroll view itself
        NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_scrollViewPointer, _imageViewPointer);
        [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_imageViewPointer(width)]|" options:0 metrics:@{@"width" : @(_scrollViewPointer.contentSize.width)} views:viewsDictionary]];
        [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_imageViewPointer(height)]|" options:0 metrics:@{@"height" : @(_scrollViewPointer.contentSize.height)} views:viewsDictionary]];

        self.imageViewConstraintsNeedUpdating = NO;
    }
}

@end

我们不能在 -viewDidLoad 中这样设置约束,因为图片还没有被渲染到UIImageView中,所以UIScrollViewcontentSize将是{0,0}

这对我来说似乎很糟糕,但它确实有效,它使用了纯Auto Layout,并且我找不到更好的方法来做到这一点。我觉得苹果需要提供更好的方法来在UIScrollView中缩放内容并使用自动布局约束。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接