在 UIScrollView 子视图中使用自动布局(以编程方式)

Posted

技术标签:

【中文标题】在 UIScrollView 子视图中使用自动布局(以编程方式)【英文标题】:Using autolayout in UIScrollView subviews (programmatically) 【发布时间】:2014-10-22 09:25:07 【问题描述】:

首先,我一直在阅读与此类似的所有问题,但没有成功,所以最后我会尝试询问我的具体情况。

我有一个 UIScrollView,我以这种方式以编程方式填充组件:

- (void) fillScrollView(int numItems)

    float posY = 10.0;
    for(int i = 0; i < numItems; i++)
    
        UIImageView *leftImg = [[UIImageView alloc] init];
        [leftImg setTranslatesAutoresizingMaskIntoConstraints:NO];
        [leftImg setFrame:CGRectMake(10.0, posY, 20.0, 20.0)];
        [leftImg setImage:[UIImage imageNamed:@"img1"]];

        UILabel *txtLb = [[UILabel alloc] init];
        [txtLb setTranslatesAutoresizingMaskIntoConstraints:NO];
        [txtLb setFont:[UIFont systemFontOfSize:15.0]];
        [txtLb setNumberOfLines:0];
        [txtLb setLineBreakMode:NSLineBreakByWordWrapping];
        [txtLb setFrame:CGRectMake(40.0, posY, 240.0, 20.0)];
        NSString *data = @"This is my example text, it could be longer.";
        [txtLb setText:data];
        CGRect paragraphRect = [dato boundingRectWithSize:CGSizeMake(txtLb.frame.size.width, 9999.0)
                                              options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
                                           attributes:@NSFontAttributeName: txtLb.font
                                              context:nil];
        float height = paragraphRect.size.height;
        CGRect frame = txtLb.frame;
        frame.size.height = ceil(height);
        [txtLb setFrame:frame];

        UIImageView *rightImg = [[UIImageView alloc] init];
        [rightImg setTranslatesAutoresizingMaskIntoConstraints:NO];
        [rightImg setFrame:CGRectMake(290.0, posY, 20.0, 20.0)];
        [rightImg setImage:[UIImage imageNamed:@"img2"]];

        [_scrollV addSubview:leftImg];
        [_scrollV addSubview:txtLb];
        [_scrollV addSubview:rightImg];

        height = height + 20.0;
        if(height < 40.0) height = 40.0;
        posY = posY + height;
    

    [_scrollV setContentSize:CGSizeMake(_scrollV.frame.size.width, posY)];

我希望这些约束是:

H:|-10-[leftImg(20)]-10-[txtLb]-10-[rightImg(20)]-10-| 并且在垂直方向上,每一行与上一行垂直间隔为 10px。

我尝试过使用 constraintWithItem,但我对在这种情况下如何使用它感到困惑。

任何帮助将不胜感激。亲切的问候!

编辑

按照张的建议,我将 3 个组件放在 UIView 中。这样我就可以毫无问题地在它们之间使用自动布局,并且每个 UIView 中的一切都在正确的位置。

但是,在循环内使用 UIView 之间的自动布局时,我仍然遇到问题。我正在这样做:

UIScrollView 所有块都没有左/右边距。

// 0px to the left of the UIScrollView
NSLayoutConstraint *constraintLeft = [NSLayoutConstraint constraintWithItem:block
               attribute:NSLayoutAttributeLeftMargin
               relatedBy:NSLayoutRelationEqual
                  toItem:_scrollV
               attribute:NSLayoutAttributeRightMargin
              multiplier:1.0
                constant:0.0];
// 0px to the right of the UIScrollView
NSLayoutConstraint *constraintRight = [NSLayoutConstraint constraintWithItem:block
               attribute:NSLayoutAttributeRightMargin
               relatedBy:NSLayoutRelationEqual
                  toItem:_scrollV
               attribute:NSLayoutAttributeLeftMargin
              multiplier:1.0
                constant:0.0];
[_scrollV addConstraint:constraintLeft];
[_scrollV addConstraint:constraintRight];

关于block之间的垂直分隔,当UIView是UIScrollView的第一个block时:

// 10px below UIScrollView top
NSLayoutConstraint *constraintTop = [NSLayoutConstraint constraintWithItem:block
               attribute:NSLayoutAttributeTopMargin
               relatedBy:NSLayoutRelationEqual
                  toItem:_scrollV
               attribute:NSLayoutAttributeBottomMargin
              multiplier:1.0
                constant:10.0];
[_scrollV addConstraint:constraintTop];

当 UIView 上面有任何块时:

// 10px below previous block
NSLayoutConstraint *constraintTop = [NSLayoutConstraint constraintWithItem:block
               attribute:NSLayoutAttributeTopMargin
               relatedBy:NSLayoutRelationEqual
                  toItem:previousBlock
               attribute:NSLayoutAttributeBottomMargin
              multiplier:1.0
                constant:10.0];
[_scrollV addConstraint:constraintTop];

这显示了所有没有垂直分隔的块,都在相同的 Y 位置,并且还给出了应用约束的错误。 我确定我没有使用正确的方法 constraintWithItem,但我找不到这种用法的示例。

【问题讨论】:

我建议您将这些子视图放在 for 循环中并将它们放入自定义 UIView 类中,在该类中正确设置自动布局,然后只需要在 for 循环中管理单个自定义视图。 谢谢张!但也许我遗漏了一些东西:如果我将所有这些子视图放入一个唯一的 UIView(以及 UIScrollView 中的那个 UIView),我会遇到与子视图的自动布局相同的问题,不是吗?由于它们是动态的,并且直到运行时我才知道它们的数量,所以我也必须在运行时设置自动布局,不是吗? 抱歉再次发表评论。我已经重读了你的评论,我想我现在理解你了。您不是要我将所有组件放在一个大的 UIView 中,而是将每个块(每个循环)放在 UIView 中,这样我可以更轻松地设置块之间的垂直间距。我会试试这个。 检查我的新答案,我已经给了你两个解决方案,一个使用你的 UIScrollView 并手动添加行 vs UICollectionView。 我刚刚阅读了您的新答案,我会尝试并让您知道。非常感谢!! 【参考方案1】:

您似乎正在尝试重新发明***伴侣。您可能应该使用 UICollectionView 或 UITableView 而不是 UIScrollView 并手动添加您的单元格。

无论如何,对于你的 scrollView 方法,你可以实现它的一种方式是这样的:

ViewController 头文件

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UIView *contentView;

@end

ViewController 实现文件

#import "ViewController.h"
#import "Cell.h"


@interface ViewController ()

@end

@implementation ViewController

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


    [self initViews];
    [self initConstraints];


    [self fillScrollView:15];


- (void)didReceiveMemoryWarning 
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.


-(BOOL)prefersStatusBarHidden

    return YES;


-(void)initViews

    self.scrollView = [[UIScrollView alloc] init];

    // ------------------------------------------------------------------
    // This content view will be the only child view of scrollview
    // ------------------------------------------------------------------
    self.contentView = [[UIView alloc] init];

    // add content view to scrollview now
    [self.scrollView addSubview:self.contentView];

    // add scrollview to main view
    [self.view addSubview:self.scrollView];


-(void)initConstraints

    self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
    self.contentView.translatesAutoresizingMaskIntoConstraints = NO;

    id views = @
                 @"scrollView": self.scrollView,
                 @"contentView": self.contentView
                 ;

    // setup scrollview constraints
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics:nil views:views]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView]|" options:0 metrics:nil views:views]];


    // ---------------------------------------
    // setup content view constraint
    //
    // note: need to pin all four side of
    // contentView to scrollView
    // ---------------------------------------
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|" options:0 metrics:nil views:views]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[contentView]|" options:0 metrics:nil views:views]];


-(void)fillScrollView:(int) numItems

    // clear any previous cells before adding new ones
    [self.contentView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];





    // Need to construct the layout visual format string

    NSMutableString *strVerticalConstraint = [[NSMutableString alloc] init];

    [strVerticalConstraint appendString:@"V:|"];


    // this dictionary will hold all the key-value pair that identifies all the subviews
    NSMutableDictionary *subviews = [[NSMutableDictionary alloc] init];


    for(int i = 0; i < numItems; i++)
    
        Cell *cell = [[Cell alloc] init];

        // customize the cell's appearance here
        cell.leftImage.image = [UIImage imageNamed:@"leftImage.png"];
        cell.textLabel.text = @"This is my example text, it could be longer.";
        cell.rightImage.image = [UIImage imageNamed:@"rightImage.png"];



        [self.contentView addSubview:cell];

        cell.translatesAutoresizingMaskIntoConstraints = NO;

        id views = @
                     @"cell": cell
                     ;

        [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[cell]|" options:0 metrics:nil views:views]];

        // prevent cell's width to extend beyond screen width
        [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:cell attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeWidth multiplier:1.0 constant:self.view.bounds.size.width]];


        // cell name
        NSString *cellName = [[NSString alloc] initWithFormat:@"cell%d", i];

        // construct each cell's vertical constraint to add it strVerticalConstraint
        NSString *viewName = nil;

        if(i < numItems - 1)
        
            viewName = [[NSString alloc] initWithFormat:@"[%@(50)]-10-", cellName];
        
        else
        
            viewName = [[NSString alloc] initWithFormat:@"[%@(50)]", cellName];
        


        [strVerticalConstraint appendString:viewName];

        // add cell name to dictionary
        [subviews setValue:cell forKey:cellName];
    

    [strVerticalConstraint appendString:@"|"];

    NSLog(@"strVerticalConstraint: \n%@", strVerticalConstraint);

    // Finally, use the long vertical constraint string
    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:strVerticalConstraint options:0 metrics:nil views:subviews]];






@end

单元格头文件

#import <UIKit/UIKit.h>

@interface Cell : UIView

@property (nonatomic, strong) UIImageView *leftImage;
@property (nonatomic, strong) UILabel *textLabel;
@property (nonatomic, strong) UIImageView *rightImage;

@end

单元实现文件

#import "Cell.h"

@implementation Cell

-(id)initWithFrame:(CGRect)frame

    self = [super initWithFrame:frame];

    if(self)
    
        [self initViews];
        [self initConstraints];
    

    return self;


-(void)initViews

    self.backgroundColor = [UIColor grayColor];

    self.leftImage = [[UIImageView alloc] init];
    self.leftImage.contentMode = UIViewContentModeScaleAspectFill;
    self.leftImage.clipsToBounds = YES;
    self.leftImage.layer.cornerRadius = 10.0;

    self.textLabel = [[UILabel alloc] init];
    self.textLabel.numberOfLines = 0;
    self.textLabel.lineBreakMode = NSLineBreakByWordWrapping;

    self.rightImage = [[UIImageView alloc] init];
    self.rightImage.contentMode = UIViewContentModeScaleAspectFill;
    self.rightImage.layer.cornerRadius = 10.0;
    self.rightImage.clipsToBounds = YES;

    [self addSubview:self.leftImage];
    [self addSubview:self.textLabel];
    [self addSubview:self.rightImage];


-(void)initConstraints

    self.leftImage.translatesAutoresizingMaskIntoConstraints = NO;
    self.textLabel.translatesAutoresizingMaskIntoConstraints = NO;
    self.rightImage.translatesAutoresizingMaskIntoConstraints = NO;

    id views = @
                 @"leftImage": self.leftImage,
                 @"textLabel": self.textLabel,
                 @"rightImage": self.rightImage
                 ;

    // horizontal constraints
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-10-[leftImage(20)]-10-[textLabel]-[rightImage(20)]-10-|" options:0 metrics:nil views:views]];

    // vertical constraints

    [self addConstraint:[NSLayoutConstraint constraintWithItem:self.leftImage attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0]];

    [self addConstraint:[NSLayoutConstraint constraintWithItem:self.textLabel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0]];

    [self addConstraint:[NSLayoutConstraint constraintWithItem:self.rightImage attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0]];

    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[leftImage(20)]" options:0 metrics:nil views:views]];
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[rightImage(20)]" options:0 metrics:nil views:views]];


@end

您应该会看到如下内容:

也许您有理由使用滚动视图并手动添加每一行,否则,如果您愿意替代,您可以使用 UICollectionView 或 UITableView。

上面的方法会导致大量的内存使用,你可以想象如果你有 1000 行,应用程序需要计算、渲染和存储 1000 行到内存中。不可扩展,也不可行。

这就是 UITableView 或 UICollectionView 的用武之地,它们会在每个单元格离开屏幕时重用它,您只需要在屏幕上渲染和存储可见单元格。

UICollectionView 演示

所以,如果您想查看 UICollectionView 方法,这是一个演示如何做到这一点:

ViewController 头文件

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout>

@property (nonatomic, strong) UICollectionView *collectionView;

@property (nonatomic, strong) NSArray *items;

@end

ViewController 实现文件

#import "ViewController.h"
#import "CustomCell.h"

@interface ViewController ()

@end

@implementation ViewController

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

    [self initViews];
    [self initConstraints];

    // --------------------------------------------------------
    // Hardcoding 15 sample items as the data source.
    // Your data might be from a JSON webservice REST API.
    // --------------------------------------------------------
    self.items = @[
                   @"This is my example text, it could be longer.",
                   @"This is my example text, it could be longer.",
                   @"This is my example text, it could be longer.",
                   @"This is my example text, it could be longer.",
                   @"This is my example text, it could be longer.",
                   @"This is my example text, it could be longer.",
                   @"This is my example text, it could be longer.",
                   @"This is my example text, it could be longer.",
                   @"This is my example text, it could be longer.",
                   @"This is my example text, it could be longer.",
                   @"This is my example text, it could be longer.",
                   @"This is my example text, it could be longer.",
                   @"This is my example text, it could be longer.",
                   @"This is my example text, it could be longer.",
                   @"This is my example text, it could be longer.",
                   @"This is my example text, it could be longer."
                   ];


- (void)didReceiveMemoryWarning 
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.


-(void)initViews

    UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
    flowLayout.minimumInteritemSpacing = 0;
    flowLayout.minimumLineSpacing = 10;

    self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:flowLayout];
    self.collectionView.backgroundColor = [UIColor whiteColor];

    // need to tell CollectionView beforehand the cell class you want to use
    [self.collectionView registerClass:[CustomCell class] forCellWithReuseIdentifier:@"cellID"];

    self.collectionView.delegate = self;
    self.collectionView.dataSource = self;

    [self.view addSubview:self.collectionView];


-(void)initConstraints

    self.collectionView.translatesAutoresizingMaskIntoConstraints = NO;

    id views = @
                 @"collectionView": self.collectionView
                 ;

    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[collectionView]|" options:0 metrics:nil views:views]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[collectionView]|" options:0 metrics:nil views:views]];


#pragma mark - UICollectionView Methods -

-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section

    return self.items.count;


-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath

    // note: reuse identifier must match what you specified in the register cell above
    CustomCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cellID" forIndexPath:indexPath];

    // ---------------------------------------------------------------
    // hardcoding images here, you might load your images from JSON
    // data using an image caching library like SDWebImage
    // ---------------------------------------------------------------
    cell.leftImage.image = [UIImage imageNamed:@"leftImage.png"];

    // getting text data from data source "self.items"
    cell.textLabel.text = self.items[indexPath.row];


    cell.rightImage.image = [UIImage imageNamed:@"rightImage.png"];


    return cell;


// ----------------------------------------------------------------
// Tells the collection view the width and height of each cell
// ----------------------------------------------------------------
-(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath

    CGSize size = CGSizeMake(self.view.frame.size.width, 50.0);

    return size;


@end

CustomCell 头文件

#import <UIKit/UIKit.h>

@interface CustomCell : UICollectionViewCell

@property (nonatomic, strong) UIImageView *leftImage;
@property (nonatomic, strong) UILabel *textLabel;
@property (nonatomic, strong) UIImageView *rightImage;

@end

CustomCell 实现文件

#import "CustomCell.h"

@implementation CustomCell

-(id)initWithFrame:(CGRect)frame

    self = [super initWithFrame:frame];

    if(self)
    
        [self initViews];
        [self initConstraints];
    

    return self;


-(void)initViews

    self.backgroundColor = [UIColor grayColor];

    self.leftImage = [[UIImageView alloc] init];
    self.leftImage.contentMode = UIViewContentModeScaleAspectFill;
    self.leftImage.clipsToBounds = YES;
    self.leftImage.layer.cornerRadius = 10.0;

    self.textLabel = [[UILabel alloc] init];
    self.textLabel.numberOfLines = 0;
    self.textLabel.lineBreakMode = NSLineBreakByWordWrapping;

    self.rightImage = [[UIImageView alloc] init];
    self.rightImage.contentMode = UIViewContentModeScaleAspectFill;
    self.rightImage.layer.cornerRadius = 10.0;
    self.rightImage.clipsToBounds = YES;

    [self.contentView addSubview:self.leftImage];
    [self.contentView addSubview:self.textLabel];
    [self.contentView addSubview:self.rightImage];


-(void)initConstraints

    self.leftImage.translatesAutoresizingMaskIntoConstraints = NO;
    self.textLabel.translatesAutoresizingMaskIntoConstraints = NO;
    self.rightImage.translatesAutoresizingMaskIntoConstraints = NO;

    id views = @
                 @"leftImage": self.leftImage,
                 @"textLabel": self.textLabel,
                 @"rightImage": self.rightImage
                 ;

    // horizontal constraints
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-10-[leftImage(20)]-10-[textLabel]-[rightImage(20)]-10-|" options:0 metrics:nil views:views]];

    // vertical constraints

    [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:self.leftImage attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0]];

    [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:self.textLabel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0]];

    [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:self.rightImage attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0]];

    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[leftImage(20)]" options:0 metrics:nil views:views]];
    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[rightImage(20)]" options:0 metrics:nil views:views]];


@end

你最终会得到这样的结果:

看起来一样但效率更高:D

不,我没有上传相同的屏幕截图:P

希望有帮助吗?

【讨论】:

哇!这是一个完美的答案!非常感谢你!只有一个问题,我被细胞的动态高度困住了。看到您的回答,似乎单元格的高度是固定的,80(使用 ScrollView)和 50(使用 Collections)。但是我的文本是可变的,所以它的高度可能会改变,我知道如何改变它,但我不知道如何相应地改变单元格的高度。 例如,使用 ScrollView,我使用 boundingRectWithSize 计算文本的高度,例如 40,然后我执行 viewName = [[NSString alloc] initWithFormat:@"[%@( %d)]-10-", 单元名称, alt];但单元格显示的比文本短。奇怪的是,我确定这个 40 高度比标签的“真实”高度短,因为我的文本有 3 行,但标签显示正确。我很困惑。 动态单元格高度令人头疼。如果您能够使您的应用程序使用最低 ios 版本 8,那么 iOS 8 UITableView 为您提供动态单元格高度,这样您就不必做太多事情,否则,它会变得更加困难,特别是如果您有一个复杂的细胞。

以上是关于在 UIScrollView 子视图中使用自动布局(以编程方式)的主要内容,如果未能解决你的问题,请参考以下文章

XCode - UIScrollView 分页不显示子视图(自动布局?)

自动布局 + UIScrollView 和单个子视图

具有动态视图数量和自动布局 iOS6 的 UIScrollView

自动布局和 UIScrollview 和子视图控制器问题

UIScrollView 作为 UIView 的子视图不适用于自动布局

以编程方式布局 UIScrollView,并为其子视图添加了自动布局,但它不滚动