Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using autolayout in UIScrollView subviews (programmatically)

First of all I've been reading all questions similar to this one, but with no success, so finally I'll try asking my specific case.

I have a UIScrollView that I fill with components programmatically this way:

- (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)];
}

I would like that theis constraints were:

H:|-10-[leftImg(20)]-10-[txtLb]-10-[rightImg(20)]-10-| And vertically each row has a space of 10px of vertical separation from the row above.

I've tried using constraintWithItem but I'm confused about how to use it in this case.

Any help would be very appreciated. Kind regards!

EDIT

Following Zhang's suggestion I've put the 3 components inside a UIView. This way I can use autolayout between them without problems and everything is in the correct position inside each UIView.

However I'm still having problems using autolayout between UIViews inside the loop. I'm doing this:

All blocks have no left/right margin with the 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];

Regarding to the vertical separation between blocks, when the UIView is the first block of the UIScrollView:

// 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];

And when the UIView has any block above it:

// 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];

This shows all blocks without vertical separation, all in the same Y position, and also give errors applying constraints. I'm sure I'm not using the right way constraintWithItem, but I can not find examples for this use.

like image 407
Wonton Avatar asked Mar 18 '23 03:03

Wonton


1 Answers

It seems like you're trying to reinvent the wheel mate. You probably should be using UICollectionView or UITableView instead of UIScrollView and manually adding your cells.

Anyhow, for your scrollView method, one way you can implement it is like this:

ViewController Header File

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

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

@end

ViewController Implementation File

#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

Cell Header File

#import <UIKit/UIKit.h>

@interface Cell : UIView

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

@end

Cell Implementation File

#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

You should see something like this:

screenshot1

Maybe you have your reason's for using a scrollView and manually adding each row, otherwise, if you're open to alternative, you can use UICollectionView or UITableView.

The method above will result in a lot of memory use as you can imagine if you had 1000 rows, the app needs to calculate, rendering and store 1000 rows in memory. Not scalable, and not feasible.

That's where UITableView or UICollectionView comes in, they reuse each cell when it goes offscreen that way, you'll only ever need to render and store the visible cells on screen.

UICollectionView Demo

So, if you want to see a UICollectionView approach, this is a demo of how you can do it:

ViewController Header File

#import <UIKit/UIKit.h>

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

@property (nonatomic, strong) UICollectionView *collectionView;

@property (nonatomic, strong) NSArray *items;

@end

ViewController Implementation File

#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 Header File

#import <UIKit/UIKit.h>

@interface CustomCell : UICollectionViewCell

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

@end

CustomCell Implementation File

#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

You end up with something like this:

screenshot2

Look the same but more efficient :D

No I didn't upload the same screenshot :P

Hope that helps?

like image 190
Zhang Avatar answered Apr 29 '23 00:04

Zhang