Design UITableView's section header in Interfa

2019-01-13 06:09发布

问题:

I have a xib file with a UITableView for which I want to add a custom section header view using the delegate method tableView:viewForHeaderInSection:. Is there any possibility to design it in Interface Builder and then change some of it's subview's properties programmatically?

My UITableView has more section headers so creating one UIView in Interface Builder and returning it doesn't work, because I'd have to duplicate it, but there isn't any good method of doing it. Archiving and unarchiving it doesn't work for UIImages so UIImageViews would show up blank.

Also, I don't want to create them programmatically because they are too complex and the resulting code would be hard to read and maintain.

Edit 1: Here is my tableView:viewForHeaderInSection: method:

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {

    if ([tableView.dataSource tableView:tableView numberOfRowsInSection:section] == 0) {
        return nil;
    }

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

    /* wrapper */

    UIView *wrapperView = [UIView viewWithSize:headerSize];

    wrapperView.backgroundColor = [UIColor colorWithHexString:@"2670ce"];

    /* title */

    CGPoint titleMargin = CGPointMake(15, 8);

    UILabel *titleLabel = [UILabel labelWithText:self.categoriesNames[section] andFrame:CGEasyRectMake(titleMargin, CGSizeMake(headerSize.width - titleMargin.x * 2, 20))];

    titleLabel.textColor = [UIColor whiteColor];
    titleLabel.font = [UIFont fontWithStyle:FontStyleRegular andSize:14];

    [wrapperView addSubview:titleLabel];

    /* body wrapper */

    CGPoint bodyWrapperMargin = CGPointMake(10, 8);

    CGPoint bodyWrapperViewOrigin = CGPointMake(bodyWrapperMargin.x, CGRectGetMaxY(titleLabel.frame) + bodyWrapperMargin.y);
    CGSize bodyWrapperViewSize = CGSizeMake(headerSize.width - bodyWrapperMargin.x * 2, headerSize.height - bodyWrapperViewOrigin.y - bodyWrapperMargin.y);

    UIView *bodyWrapperView = [UIView viewWithFrame:CGEasyRectMake(bodyWrapperViewOrigin, bodyWrapperViewSize)];

    [wrapperView addSubview:bodyWrapperView];

    /* image */

    NSInteger imageSize = 56;
    NSString *imageName = [self getCategoryResourceItem:section + 1][@"image"];

    UIImageView *imageView = [UIImageView imageViewWithImage:[UIImage imageNamed:imageName] andFrame:CGEasyRectMake(CGPointZero, CGEqualSizeMake(imageSize))];

    imageView.layer.masksToBounds = YES;
    imageView.layer.cornerRadius = imageSize / 2;

    [bodyWrapperView addSubview:imageView];

    /* labels */

    NSInteger labelsWidth = 60;

    UILabel *firstLabel = [UILabel labelWithText:@"first" andFrame:CGRectMake(imageSize + bodyWrapperMargin.x, 0, labelsWidth, 16)];

    [bodyWrapperView addSubview:firstLabel];

    UILabel *secondLabel = [UILabel labelWithText:@"second" andFrame:CGRectMake(imageSize + bodyWrapperMargin.x, 20, labelsWidth, 16)];

    [bodyWrapperView addSubview:secondLabel];

    UILabel *thirdLabel = [UILabel labelWithText:@"third" andFrame:CGRectMake(imageSize + bodyWrapperMargin.x, 40, labelsWidth, 16)];

    [bodyWrapperView addSubview:thirdLabel];

    [@[ firstLabel, secondLabel, thirdLabel ] forEachView:^(UIView *view) {
        UILabel *label = (UILabel *)view;

        label.textColor = [UIColor whiteColor];
        label.font = [UIFont fontWithStyle:FontStyleLight andSize:11];
    }];

    /* line */

    UIView *lineView = [UIView viewWithFrame:CGRectMake(imageSize + labelsWidth + bodyWrapperMargin.x * 2, bodyWrapperMargin.y, 1, bodyWrapperView.frame.size.height - bodyWrapperMargin.y * 2)];

    lineView.backgroundColor = [UIColor whiteColorWithAlpha:0.2];

    [bodyWrapperView addSubview:lineView];

    /* progress */

    CGPoint progressSliderOrigin = CGPointMake(imageSize + labelsWidth + bodyWrapperMargin.x * 3 + 1, bodyWrapperView.frame.size.height / 2 - 15);
    CGSize progressSliderSize = CGSizeMake(bodyWrapperViewSize.width - bodyWrapperMargin.x - progressSliderOrigin.x, 30);

    UISlider *progressSlider = [UISlider viewWithFrame:CGEasyRectMake(progressSliderOrigin, progressSliderSize)];

    progressSlider.value = [self getCategoryProgress];

    [bodyWrapperView addSubview:progressSlider];

    return wrapperView;
}

and I would want it to look something like this:

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {

    if ([tableView.dataSource tableView:tableView numberOfRowsInSection:section] == 0) {
        return nil;
    }

    SectionView *sectionView = ... // get the view that is already designed in the Interface Builder

    sectionView.headerText = self.categoriesNames[section];
    sectionView.headerImage = [self getCategoryResourceItem:section + 1][@"image"];

    sectionView.firstLabelText = @"first";
    sectionView.secondLabelText = @"second";
    sectionView.thirdLabelText = @"third";

    sectionView.progress = [self getCategoryProgress];

    return wrapperView;
}

Edit 2: I'm not using a Storyboard, just .xib files. Also, I don't have an UITableViewController, just an UIViewController in which I added an UITableView.

回答1:

Storyboard or XIB

  1. Same Storyboard:

    return tableView.dequeueReusableCell(withIdentifier: "header")
    

  2. Separate XIB (Additional step: you must register that Nib first):

    tableView.register(UINib(nibName: "XIBSectionHeader", bundle:nil),
                       forCellReuseIdentifier: "xibheader")
    

To load from a Storyboard instead of a XIB, see this Stack Overflow answer.


Using UITableViewCell to create Section Header in IB

Take advantage of the fact that a section header is a regular UIView, and that UITableViewCell is, too, a UIView. In Interface Builder, drag & drop a Table View Cell from the Object Library onto your Table View Prototype Content.

Add an Identifier to the newly added Table View Cell, and customize its appearance to suit your needs. For this example, I used header.

Use dequeueReusableCell:withIdentifier to locate your section header, just like you would any table view cell. You will need to supply heightForHeaderInSection, which is hardcoded as 44 for clarity:

//MARK: UITableViewDelegate
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
{
    // This is where you would change section header content
    return tableView.dequeueReusableCell(withIdentifier: "header")
}

override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
{
    return 44
}

Swift 2 & earlier:

return tableView.dequeueReusableCellWithIdentifier("header") as? UIView
self.tableView.registerNib(UINib(nibName: "XIBSectionHeader", bundle:nil),
    forCellReuseIdentifier: "xibheader")

► Find this solution on GitHub and additional details on Swift Recipes.



回答2:

I finally solved it using this tutorial, which, largely consists of the following (adapted to my example):

  1. Create SectionHeaderView class that subclasses UIView.
  2. Create SectionHeaderView.xib file and set it's File's Owner's CustomClass to the SectionHeaderView class.
  3. Create an UIView property in the .m file like: @property (strong, nonatomic) IBOutlet UIView *viewContent;
  4. Connect the .xib's View to this viewContent outlet.
  5. Add an initializer method that looks like this:

    + (instancetype)header {
    
        SectionHeaderView *sectionHeaderView = [[SectionHeaderView alloc] init];
    
        if (sectionHeaderView) { // important part
            sectionHeaderView.viewContent = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:sectionHeaderView options:nil] firstObject];
    
            [sectionHeaderView addSubview:sectionHeaderView.viewContent];
    
            return sectionHeaderView;
        }
    
        return nil;
    }
    

Then, I added an UILabel inside the .xib file and connected it to the labelCategoryName outlet and implemented the setCategoryName: method inside the SectionHeaderView class like this:

- (void)setCategoryName:(NSString *)categoryName {

    self.labelCategoryName.text = categoryName;
}

I then implemented the tableView:viewForHeaderInSection: method like this:

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {

    SectionHeaderView *sectionHeaderView = [SectionHeaderView header];

    [sectionHeaderView setCategoryName:self.categoriesNames[section]];

    return sectionHeaderView;
}

And it finally worked. Every section has it's own name, and also UIImageViews show up properly.

Hope it helps others that stumble over the same wrong solutions over and over again, all over the web, like I did.



回答3:

Solution Is way simple

Create one xib, make UI according to your Documentation then in viewForHeaderInSection get xib

-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {

        NSArray *nibArray = [[NSBundle mainBundle] loadNibNamed:@"HeaderView" owner:self options:nil];

       HeaderView *headerView = [nibArray objectAtIndex:0];

    return headerView;
} 


回答4:

As far as I understand your problem, you want to have the same UIView duplicated multiple times for the multiple section headers you want to be able to display.

If this were my problem, here is how I would solve it.

ORIGINAL SOLUTION

1)

In my UIViewController that owns the table view, I'd also create a view that's a template for the header. Assign that to a IBOutlet. This will be the view you can edit via Interface Builder.

2)

In your ViewDidLoad or (maybe better) ViewWillAppear method, you'll want to make as many copies of that header template UIView as you'll need to display for section headers.

Making copies of UIViews in memory isn't trivial, but it isn't hard either. Here is an answer from a related question that shows you how to do it.

Add the copies to a NSMutableArray (where the index of each object will correspond to the sections... the view in index 0 of the array will be what you return for section 0, view 1 in the array for section 1, ec.).

3)

You will not be able to use IBOutlets for the elements of that section header (because your code only associates outlets with one particular view from the XIB file).

So for this, you'll probably want to use view tag properties for each of the UI elements in your header view that you'll want to modify/change for each different section. You can set these tags via Interface Builder and then refer to them programmatically in your code.

In your viewForHeaderInSection method, you'll do something like:

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {

    if ([tableView.dataSource tableView:tableView numberOfRowsInSection:section] == 0) {
        return nil;
    }

    SectionView *sectionView = [self.arrayOfDuplicatedHeaderViews objectAtIndex: section];
    // my title view has a tag of 10
    UILabel *titleToModify = [sectionView viewWithTag: 10]; 
    if(titleToModify)
    {
        titleToModify.text = [NSString stringWithFormat:@"section %d", section];
    }

    return sectionView;
}

Makes sense?

DIFFERENT SOLUTION

1)

You'd still need an array of UIViews (or "Section View" subclassed UIViews) but you could create each of those with successive calls to load the view from it's own XIB file.

Something like this:

@implementation SectionView

+ (SectionView*) getSectionView
{
  NSArray* array = [[NSBundle mainBundle] loadNibNamed:@"SectionView" owner:nil options:nil];
  return [array objectAtIndex:0]; // assume that SectionView is the only object in the xib
}

@end

(more detail found in the answer to this related question)

2)

You might be able to use IBOutlets on this (but I'm not 100% certain), but tag properties once again might work pretty well.