In daily development, we sometimes encounter interfaces with many content blocks and are changeable: Some content blocks in this interface are fixed, such as the product details picture, product name, price, etc. at the top. Some content blocks may not appear, such as promotions (obviously not every product has promotions), selected specifications (some products do not have specifications), store information (some products are self-operated and do not have stores), etc. Some content needs to change according to the situation, such as comments. A maximum of 4 comments are listed here. If there are no comments, "No comments yet" will be displayed and the "View all comments" button will not be displayed. For such an interface, I believe many people will feel that they will use TableView, because the comments need to be listed in the middle, and it is more appropriate to fill it with TableView cells. But how to deal with other content besides the comments? My previous approach was to use HeaderView above the comments and FooterView below. Although the function was finally realized, it was very troublesome to do. I used Auto Layout to do the layout. Due to the characteristics of Auto Layout itself, the code involves a lot of judgment and processing. For example, the "Selected Specifications" block initially has a top spacing constraint with the "Promotion" content block, but the "Promotion" content block may not have it, so the constraints of the "Selected Specifications" block itself must be adjusted according to the situation (for example, let its top spacing constraint point to the "Price" content block). Similarly, the "Promotion" content block itself also needs similar processing. If there is no "Promotion", it needs to hide itself. The easiest way to hide itself is to set its height constraint to 0 (because it also has a spacing constraint from the bottom to the "Selected Specifications", and it cannot be removed at will, otherwise the constraints related to the "Selected Specifications" must be adjusted again). In addition, there is another troublesome problem. When the interface first comes in, it needs to request network data. At this time, the interface should be displayed in an initial state. Obviously, some content blocks should not be displayed in the initial state. For example, promotions. Only after the data request is completed can we know whether there is a promotion, and if so, the promotion content will be displayed; for example, comments should be displayed as "No comments" at the beginning, and the corresponding content will be displayed after the data request is completed. In this way, we need to handle the display of each content block in the two states of initial entry and data request completion, which is very complicated and cumbersome. In summary, using TableView's HeaderView + comment content cell + FooterView + Auto Layout will bring the following problems: - The constraints themselves need to rely on other Views, and the Views they rely on are mutable content blocks, which will cause the constraints to require tedious judgment, modification, addition and deletion
- It is necessary to handle the interface display of two states: initial entry and data request completion, making the code more complicated and cumbersome
- The height of the corresponding content needs to be calculated additionally to update the height of HeaderView and FooterView
Obviously, this method is not an ideal solution. Some people may say that we should not use Auto Layout, but just operate the frame directly to layout. This may reduce some troubles, but it does not reduce the complexity overall. Some people also say that we should use ScrollView directly. In this case, all the content, including the cells of the comment content, have to be manually spliced. It can be imagined that this method is also quite troublesome. Therefore, we have to find another way and use other methods to achieve the goal. The following is a relatively simple method for everyone. This method was also shared with me by a former colleague, so I will borrow flowers to offer Buddha and share it with you. We still use TableView to make this interface. What is different from the previous one is that we make each variable content block into an independent cell. The granularity of the cell can be controlled by ourselves. For example, we can use one cell to include the product picture, title, subtitle, and price, or we can break it down to a smaller size, with the picture, title, subtitle, and price each corresponding to a cell. Here we choose the latter, because the picture content block needs to be stretched in proportion to the screen width; the text content of the title and subtitle may be one line or two lines, and the height is variable. It is simpler and clearer to control it with a separate cell. Let's define various types of cells: - //Basic cell, for the sake of simplicity, define this cell, and other cells inherit from this cell
-
- @interface MultipleVariantBasicTableViewCell : UITableViewCell
-
- @property (nonatomic, weak) UILabel *titleTextLabel;
-
- @ end
-
-
-
- //Scroll the picture
-
- @interface CycleImagesTableViewCell : MultipleVariantBasicTableViewCell
-
- @ end
-
-
-
- //Main title
-
- @interface MainTitleTableViewCell : MultipleVariantBasicTableViewCell
-
- @ end
-
-
-
- //subtitle
-
- @interface SubTitleTableViewCell : MultipleVariantBasicTableViewCell
-
- @ end
-
-
-
- //price
-
- @interface PriceTableViewCell : MultipleVariantBasicTableViewCell
-
- @ end
-
-
-
- // ...cell declarations for other content blocks
-
-
-
-
-
- // Implementation of various content block cells. For the sake of simplicity, only one Label is placed in the cell.
-
- @implementation MultipleVariantBasicTableViewCell
-
-
-
- - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
-
- self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
-
- if (self) {
-
- UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 320, 44)];
-
- label.numberOfLines = 0;
-
- [self.contentView addSubview:label];
-
- self.titleTextLabel = label;
-
- }
-
- return self;
-
- }
-
-
-
- @ end
-
-
-
- @implementation CycleImagesTableViewCell
-
- @ end
-
-
-
- @implementation MainTitleTableViewCell
-
- @ end
-
-
-
- // ...cell implementation of other content blocks
-
-
-
- // The comment content cell uses Auto Layout and the automatic height calculation of iOS 8 TableView to achieve content adaptation
-
- @implementation CommentContentTableViewCell
-
-
-
- - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
-
- self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
-
- if (self) {
-
- self.titleTextLabel.translatesAutoresizingMaskIntoConstraints = NO ;
-
- self.titleTextLabel.preferredMaxLayoutWidth = [UIScreen mainScreen].bounds. size .width - 8;
-
- NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:self.titleTextLabel attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeLeading multiplier:1.0f constant:4.0f];
-
- NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:self.titleTextLabel attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:-4.0f];
-
- NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:self.titleTextLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeTop multiplier:1.0f constant:4.0f];
-
- NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:self.titleTextLabel attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeBottom multiplier:1.0f constant:-4.0f];
-
- [self.contentView addConstraints:@[leftConstraint, rightConstraint, topConstraint, bottomConstraint]];
-
- }
-
- return self;
-
- }
-
-
-
- @ end
The next key point is how to control which cells are displayed and the number of cells displayed. If this step is not handled properly, it will also complicate development. For example, the following method: - // Loading data
-
- self.cellCount = 0;
-
- if (promotion exists) {
-
- self.cellCount++;
-
- }
-
- if (existsSpec) {
-
- self.cellCount++;
-
- }
-
- ......
If the number of cells is recorded in this way, the subsequent display of cells, click judgment, etc. will be very troublesome. Here we use a separate class (as a data structure) to save the cell information to be displayed. - // SKRow.h
-
- @interface SKRow : NSObject
-
-
-
- @property (nonatomic, copy) NSString *cellIdentifier;
-
- @property (nonatomic, strong) id data;
-
- @property (nonatomic, assign) float rowHeight;
-
-
-
- - (instancetype)initWithCellIdentifier:(NSString *)cellIdentifier
-
- data:(id)data
-
- rowHeight:( float )rowHeight;
-
-
-
- @ end
-
-
-
- // SKRow.m
-
- #import "SKRow.h"
-
-
-
- @implementation SKRow
-
-
-
- - (instancetype)initWithCellIdentifier:(NSString *)cellIdentifier data:(id)data rowHeight:( float )rowHeight {
-
- if (self = [super init]) {
-
- self.cellIdentifier = cellIdentifier;
-
- self.data = data;
-
- self.rowHeight = rowHeight;
-
- }
-
- return self;
-
- }
-
-
-
- @ end
SKRow is used to store the information required for each cell, including reuse identifier, data item, and height. Next, we start splicing cell information. - @interface ViewController ()
-
- @property (nonatomic, strong) NSMutableArray *> *tableSections;
-
- @ end
-
-
- self.tableSections = [NSMutableArray array];
-
-
-
- /* Initial load data
-
- * When initialized, only scrolling pictures, prices, comment headers, and no comments are displayed
-
- */
-
- // Scroll the image (keep the aspect ratio)
-
- SKRow *cycleImagesRow = [[SKRow alloc] initWithCellIdentifier:@ "CycleImagesCellIdentifier" data:@[@ "Scrolling Image Address" ] rowHeight:120*[UIScreen mainScreen].bounds. size .width / 320.f];
-
- // price
-
- SKRow *priceRow = [[SKRow alloc] initWithCellIdentifier:@ "PriceCellIdentifier" data:@ "0" rowHeight:44];
-
- [self.tableSections addObject:@[cycleImagesRow, priceRow]];
-
- // Comment header
-
- SKRow *commentSummaryRow = [[SKRow alloc] initWithCellIdentifier:@ "CommentSummaryCellIdentifier" data:@{@ "title" :@ "Product Evaluation" , @ "count" :@ "0" } rowHeight:44];
-
- // No comments
-
- SKRow *noCommentRow = [[SKRow alloc] initWithCellIdentifier:@ "NoCommentCellIdentifier" data:@ "No comments yet" rowHeight:44];
-
- [self.tableSections addObject:@[commentSummaryRow, noCommentRow]];
The above are the cells to be displayed in the initial state. We declare an array in ViewController to store the cell information to be displayed in each section of TableView. Here we divide the cells into different sections. In practice, you can decide whether to divide them or not and how many sections to divide them into. In the initial state, we have two sections. The first section is used to display basic information, and the second section is used to display comment information. This completes the splicing of cell information. The next step is to display: - - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
-
- // Here you can distinguish and process different cells by judging cellIdentifier, and get the data required by the cell from row.data
-
-
-
- SKRow *row = self.tableSections[indexPath. section ][indexPath.row];
-
- if ([row.cellIdentifier isEqualToString:@ "CycleImagesCellIdentifier" ]) {
-
- CycleImagesTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
-
- NSArray *urlStringArray = row.data;
-
- cell.titleTextLabel.text = [urlStringArray componentsJoinedByString:@ "\n" ];
-
- return cell;
-
- } else if ([row.cellIdentifier isEqualToString:@ "MainTitleCellIdentifier" ]) {
-
- MainTitleTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
-
- cell.titleTextLabel.text = row.data;
-
- return cell;
-
- } else if ([row.cellIdentifier isEqualToString:@ "PriceCellIdentifier" ]) {
-
- PriceTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
-
- cell.titleTextLabel.text = [NSString stringWithFormat:@ "¥%@" , row.data];
-
- return cell;
-
- } else if ([row.cellIdentifier isEqualToString:@ "SalePromotionCellIdentifier" ]) {
-
- SalePromotionTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
-
- NSArray *salePromotionStringArray = row.data;
-
- cell.titleTextLabel.text = [salePromotionStringArray componentsJoinedByString:@ "\n" ];
-
- return cell;
-
- } else if ([row.cellIdentifier isEqualToString:@ "SpecificationCellIdentifier" ]) {
-
- SpecificationTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
-
- cell.titleTextLabel.text = [NSString stringWithFormat:@ "Selected: %@" , row.data];
-
- return cell;
-
- } else if ([row.cellIdentifier isEqualToString:@ "CommentSummaryCellIdentifier" ]) {
-
- CommentSummaryTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
-
- NSDictionary *commentSummary = row.data;
-
- cell.titleTextLabel.text = [NSString stringWithFormat:@ "%@(%@)" , commentSummary[@ "title" ], commentSummary[@ "count" ]];
-
- return cell;
-
- } else if ([row.cellIdentifier isEqualToString:@ "CommentContentCellIdentifier" ]) {
-
- CommentContentTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
-
- cell.titleTextLabel.text = row.data;
-
- return cell;
-
- } else if ([row.cellIdentifier isEqualToString:@ "AllCommentCellIdentifier" ]) {
-
- AllCommentTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
-
- cell.titleTextLabel.text = row.data;
-
- return cell;
-
- } else if ([row.cellIdentifier isEqualToString:@ "NoCommentCellIdentifier" ]) {
-
- NoCommentTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
-
- cell.titleTextLabel.text = row.data;
-
- return cell;
-
- }
-
- return nil;
-
- }
The above code has been shortened and does not handle all types. Although it is a bit lengthy, the logic is very simple, which is to obtain cell information, distinguish different types of content blocks according to the reuse identifier, and display the processed data in the cell. For example, for product images, because they are scrolling images, there can be multiple scrolling images. The data we passed in earlier is the array data:@[@"scrolling image address"]. After getting the data, cell.titleTextLabel.text = [urlStringArray componentsJoinedByString:@"\n"];. For demonstration purposes, we only put one Label in the product image cell, so we simply display the address information in separate lines. In actual development, you can put in an image scrolling display control and pass the array data of the image address to the control for display. The processing of other types of cells is similar. For demonstration purposes, they are just simple data processing displays. Of course, don't forget to set the dataSource and delegate related to the TableView: - - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
-
- SKRow *row = self.tableSections[indexPath. section ][indexPath.row];
-
- return row.rowHeight;
-
- }
-
-
-
- - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
-
- return self.tableSections.count ;
-
- }
-
-
-
- - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger) section {
-
- return self.tableSections[ section ] .count ;
-
- }
In this way, we have completed the display of the interface in the initial state After completing the cell display processing, let's simulate how the interface displays the required cells after the network requests data. - self.tableSections = [NSMutableArray array];
-
-
-
- NSMutableArray *section1 = [NSMutableArray array];
-
- // Scroll the image (keep the aspect ratio)
-
- SKRow *cycleImagesRow = [[SKRow alloc] initWithCellIdentifier:@ "CycleImagesCellIdentifier" data:@[@ "Scrolling image address 1" , @ "Scrolling image address 2" , @ "Scrolling image address 3" ] rowHeight:120*[UIScreen mainScreen].bounds.size.width / 320.f];
-
- // Main title
-
- SKRow *mainTitleRow = [[SKRow alloc] initWithCellIdentifier:@ "MainTitleCellIdentifier" data:@ "Product Name" rowHeight:44];
-
- // Subtitle
-
- SKRow *subTitleRow = [[SKRow alloc] initWithCellIdentifier:@ "SubTitleCellIdentifier" data:@ "Holiday promotion, come and buy it" rowHeight:44];
-
- // price
-
- SKRow *priceRow = [[SKRow alloc] initWithCellIdentifier:@ "PriceCellIdentifier" data:@(arc4random()) rowHeight:44];
-
- [section1 addObjectsFromArray:@[cycleImagesRow, mainTitleRow, subTitleRow, priceRow]];
-
- // Promotion (randomly appear)
-
- if (arc4random() % 2 == 0) {
-
- SKRow *salePromotionRow = [[SKRow alloc] initWithCellIdentifier:@ "SalePromotionCellIdentifier" data:@[@ "Promotion information 1" , @ "Promotion information 2" , @ "Promotion information 3" ] rowHeight:44];
-
- [section1 addObject:salePromotionRow];
-
- }
-
- [self.tableSections addObject:section1];
-
-
-
- NSMutableArray *section2 = [NSMutableArray array];
-
- // Specifications (random)
-
- if (arc4random() % 2 == 0) {
-
- SKRow *specificationRow = [[SKRow alloc] initWithCellIdentifier:@ "SpecificationCellIdentifier" data:@ "Silver, 13.3 inches" rowHeight:44];
-
- [section2 addObject:specificationRow];
-
- }
-
- if (section2. count > 0) {
-
- [self.tableSections addObject:section2];
-
- }
-
-
-
- NSMutableArray *section3 = [NSMutableArray array];
-
- NSArray *commentArray = [NSMutableArray array];
-
- // Comment content data (randomly appear)
-
- if (arc4random() % 2 == 0) {
-
- commentArray = @[@ "Comment 1" , @ "Comment 2" , @ "In June 2016, iOS 10 was officially released, and Apple brought ten major updates to iOS 10. On June 13, 2016, the Apple Developer Conference WWDC was held in San Francisco. The conference announced that the beta version of iOS 10 would be launched in the summer of 2016, and the official version would be released in the fall. On September 7, 2016, Apple released iOS 10. The official version of iOS 10 was fully pushed on September 13 (1 a.m. on September 14, Beijing time). " , @ "Comment 4" ];
-
- }
-
- // Comment header
-
- SKRow *commentSummaryRow = [[SKRow alloc] initWithCellIdentifier:@ "CommentSummaryCellIdentifier" data:@{@ "title" :@ "Commodity Evaluation" , @ "count" :@(commentArray. count )} rowHeight:44];
-
- [section3 addObject:commentSummaryRow];
-
- if (commentArray. count > 0) {
-
- for (NSString *commentString in commentArray) {
-
- // The comment content needs to be adaptive in height, and the height value is specified as UITableViewAutomaticDimension
-
- SKRow *commentContentRow = [[SKRow alloc] initWithCellIdentifier:@ "CommentContentCellIdentifier" data:commentString rowHeight:UITableViewAutomaticDimension];
-
- [section3 addObject:commentContentRow];
-
- }
-
- // View all comments
-
- SKRow *allCommentRow = [[SKRow alloc] initWithCellIdentifier:@ "AllCommentCellIdentifier" data:@ "View all comments" rowHeight:44];
-
- [section3 addObject:allCommentRow];
-
- } else {
-
- // No comments
-
- SKRow *noCommentRow = [[SKRow alloc] initWithCellIdentifier:@ "NoCommentCellIdentifier" data:@ "No comments yet" rowHeight:44];
-
- [section3 addObject:noCommentRow];
-
- }
-
- [self.tableSections addObject:section3];
-
-
-
- [self.tableView reloadData];
The code above is also lengthy, but the logic is also very simple. The cell data is pieced together in the order of display. Some content blocks that may not be displayed, such as promotions, are randomly judged. If displayed, the data is added to the section array [section1 addObject:salePromotionRow];. Other types of cells are similar and will not be repeated here. It should be noted that the text of the comment content may have multiple lines, so we set its cell height to UITableViewAutomaticDimension: [[SKRow alloc] initWithCellIdentifier:@"CommentContentCellIdentifier" data:commentString rowHeight:UITableViewAutomaticDimension]; Since we use Auto Layout for the comment content cell, we can use the new features of iOS 8 TableView to automatically calculate the height of the cell. After splicing the data, just call [self.tableView reloadData]; to reload the TableView. Okay, that's it. Final result Although it is more verbose to write this kind of interface with variable content blocks using the above method, it has the following advantages: - The logic is clear and simple, easy to understand. There is no complex dependency between views like the previous HeaderView + Auto Layout + FooterView. It is very easy to handle whether the content block is displayed or not.
- Easy to adjust. For example, to change the order of content blocks, just move the order of the code that assembles the cell data.
- Easy to expand and add new content blocks. To add a new content block, just create a new cell, add the data code of the new cell type when splicing data, and also add the code to display the new cell type in the display area. There is almost no need to modify the original logic.
***, attached is the demo project code (https://github.com/kelystor/MultipleVariantCell). Note that this project was created with XCode 8, and lower versions of XCode may have problems running (XCode 8's storyboard seems to be incompatible with older versions by default). The example is based on iOS 8. If you want to be compatible with older versions, please modify it yourself (mainly involving the part of automatic cell height calculation). |