Dynamic Table View Cell Height and Auto Layout

本文介绍如何使用Auto Layout实现UITableView Cell的高度动态调整,通过实例演示如何创建自定义Cell,并利用Auto Layout来适配不同内容的大小。

Dynamic Table View Cell Height and Auto Layout

  Joshua Greene 
Learn how to create dynamic height table view cells using auto layout.

Learn how to create dynamic height table view cells using auto layout.

If you wanted to create a customized table view complete with dynamic table view cell height in the past, you had to write a lot of sizing code. You had to calculate the height of every label, image view, text field, and everything else within the cell–manually.

Frankly, this was arduous, drool-inducing and error-prone.

In this dynamic table view cell height tutorial, you’ll learn how to create custom cells and dynamically size them to fit their contents. If you’ve worked with custom cells before, you’re probably thinking, “That’s going to take a lot of sizing code.”

However, what if we told you that you’re not going to write any sizing code at all?

“Preposterous!” you might say. Oh, but you can. Furthermore, you will!

By the time you’re at the end of this tutorial, you’ll know how to save yourself from hundreds of lines of code by leveraging auto layout.

Note: This tutorial works with existing iOS 7 apps and Xcode 5. It does not cover the iOS 8 Beta features for auto-sizing table view cells.

This tutorial assumes you have basic familiarity with using both auto layout and UITableView (including its data source and delegate methods). If you need to brush up on these topics before continuing, we’ve got you covered!

Getting Started

iOS 6 introduced a wonderful new technology: auto layout. Developers rejoiced; parties commenced in the streets; bands performed songs to recognize its greatness…

Okay, so those might be overstatements, but it was a big deal.

While it inspired hope for countless developers, initially, auto layout was cumbersome to use. Manually writing auto layout code was, and still is, a great example of the verbosity of Objective-C, and Interface Builder was often counter-productive in terms of its helpfulness with constraints.

With the introduction of Xcode 5.1 and iOS 7, auto layout support in Interface Builder took a great leap forward. In addition to that, iOS 7 introduced a very important delegate method in UITableViewDelegate:

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath;

This method allows a table view to calculate the actual height of each cell lazily, even deferring until the moment that table’s needed. At long last, auto layout can do the heavy lifting in dynamically calculating cell height. (Cue the bands!)

But, you probably don’t want to get bogged down in theory right now, do you? You’re probably ready to get down to coding, so let’s get down to business with the tutorial app.

Tutorial App Overview

Imagine that your top client has come to you and said, “Our users are clamoring for a way to view their favorite Deviant Artists’ submissions.”

Did you mention that you’ve not heard of Deviant Art?

“Well, it’s a popular social networking platform where artists can share their art creations, called deviations and blog posts too.” your client explains. “You really should check out the Deviant Art website.”

Fortunately, Deviant Art offers a Media RSS endpoint, which you can use to access deviations and posts by artist.

“We started making the app, but we’re stumped at how to display the content in a table view,” your client admits. “Can you make it work?”

Suddenly you feel the urge to slide into the nearest phone booth and change into your cape. Now, you just need the tools to make it happen, so you can be your client’s hero.

First download the “client’s code” (the starter project for this tutorial) here.

This project uses CocoaPods, so open DeviantArtBrowser.xcworkspace (not the .xcodeproj file). The pods are included in the zip for you, so you don’t need to run pod install.

If you’d like to learn more about CocoaPods, you should go through our Introduction to CocoaPods tutorial.

The starter project successfully downloads content from the Deviant Art RSS feed, but doesn’t have a way of displaying any of that yet. That’s where you come in :]

Open Main.storyboard (under the Views group), and you’ll see four scenes:

Main.storyboard

From left to right, they are:

  • A top-level navigation controller
  • RWFeedViewController, titled Deviant Browser
  • Two scenes for RWDetailViewController (one displays only text content and the other displays both text and an image); their titles are Deviant Article and Deviant Media

Build and run. You’ll see the console log output, but nothing else works yet.

The log output should look like this:

2014-05-28 00:52:01.588 DeviantArtBrowser[1191:60b] GET 'http://backend.deviantart.com/rss.xml?q=boost%3Apopular'

2014-05-28 00:52:03.144 DeviantArtBrowser[1191:60b] 200 'http://backend.deviantart.com/rss.xml?q=boost%3Apopular' [1.5568 s]

The app is making a network request and getting a response, but does nothing with the response data.

Now, open RWFeedViewController.m. This is where most of the new code will go. Have a look at this snippet from parseForQuery::

  [self.parser parseRSSFeed:RWDeviantArtBaseURLString
                 parameters:[self parametersForQuery:query]
                    success:^(RSSChannel *channel) {
                      [weakSelf convertItemsPropertiesToPlainText:channel.items];
                      [weakSelf setFeedItems:channel.items];
 
                      [weakSelf reloadTableViewContent];
                      [weakSelf hideProgressHUD];
                    } failure:^(NSError *error) {
                      [weakSelf hideProgressHUD];
                      NSLog(@"Error: %@", error);
                    }];

self.parser is an instance of RSSParser, which is part of MediaRSSParser.

This method initiates a network request to Deviant Art to get the RSS feed, and then it returns an RSSChannel to the success block. After formatting , which is simply converting HTML to plain text, the success stores the channel.items as a local property called feedItems.

The channel.items array populates with RSSItem objects, each of which represents a single item element in an RSS feed. Great, you now know what you’ll need to display in the table view: the feedItems array!

Lastly, notice that there are three warnings in the project. What are these about?

It looks like someone put #warning statements showing what to implement. Well, that’s convenient, now isn’t it?

Well, that's convenient!

Create a Basic Custom Cell

After a quick dive into the source code, you’ve found the app is already fetching data, but it’s not displaying anything. To do that, you first need to create a custom table view cell to show the data.

Add a new class to the DeviantArtBrowser project and name it RWBasicCell. Make it a subclass of UITableViewCell; and make sure Also create xib file is not checked.

Open RWBasicCell.h, and add the following properties right after @interface RWBasicCell : UITableViewCell :

@property (nonatomic, weak) IBOutlet UILabel *titleLabel;
@property (nonatomic, weak) IBOutlet UILabel *subtitleLabel;

Next open Main.storyboard, and drag and drop a new UITableViewCell onto the table view of RWFeedViewController.

Set the Custom Class of the cell to RWBasicCell.

Set RWBasicCell Class

Set the Identifier (Reuse Identifier) to RWBasicCell.

Set RWBasicCell Identifier

Set the Row Height of the cell to 83.

Set RWBasicCell Row Height

Drag and drop a new UILabel onto the cell, and set its text to “Title”.

Add Title Label to RWBasicCell

Set the title label’s Lines (the number of lines the label can have at most) to 0, meaning “unlimited”.

Set RWBasicCell Title Label's Number Lines

Set the title label’s size and position to the values of the screenshot below.

Set RWBasicCell Title Label's Frame

Connect the titleLabel outlet of RWBasicCell to the title label on the cell.

Connect Outlet RWBasicCell Title Label's Outlet

Next, drag and drop a second UILabel onto the cell, right below the title label, and set its text to “Subtitle”.

Add Subtitle Label to RWBasicCell

As you did with the title label, change the values of the size and position to match the values of the screenshot below.

Set RWBasicCell Subtitle Label's Frame

Set the subtitle label’s Color to Light Gray Color; its Font to System 15.0; and its Lines to 0.

Set RWBasicCell Subtitle Label's Properties

Connect the subtitleLabel outlet of RWBasicCell to the subtitle label on the cell.

Connect RWBasicCell Subtitle Label Outlet

Awesome, you’ve laid out and configured RWBasicCell. Now you just need to add auto layout constraints. Select the title label and pin the toptrailing and leading edges to 20 points from the superview.

Set RWBasicCell Title Label's Pin Constraints

This ensures that no matter how big or small the cell may be, the title label is always:

  • 20 points from the top
  • Spans the entire width of the cell, minus 20 points of padding on the left and right

Now select the subtitle label and this time, pin the leadingbottom, and trailing edges to 20 points from the superview.

Set RWBasicCell Subtitle Label's Pin Constraints

Similar to the title label, these constraints work together to ensure the subtitle label is always at 20 points from the bottom and spans the entire width of the cell, minus a little padding.

Pro tip: the trick to getting auto layout working on a UITableViewCell is to ensure there are constraints that pin each subview to all the sides – that is, each subview should have leading, top, trailing and bottom constraints.

Further, there should be a clear line of constraints going from the top to the bottom of the contentView. This way, auto layout will correctly determine the height of the contentView, based on its subviews.

The tricky part is that Interface Builder often doesn’t warn you if you’re missing some of these constraints…

Auto layout will simply not return the correct heights when you try to run the project. For example, it may always return 0for the cell height, which is a strong clue that indicates your constraints are probably wrong.

If you run into issues in your own projects, try adjusting your constraints until the above criteria are met.

Select the subtitle label, hold down ⌃ control and drag to the title label. Choose Vertical Spacing to pin the top of the subtitle label to the bottom of the title label.

You probably have some warnings related to auto layout, but you’re about to fix those.

On the title and the subtitle labels, set the Horizontal and Vertical constraints for Content Hugging Priority and Content Compression Resistance Priority to 1000.

Set RWBasicCell Labels' Content Hugging and Compression Resistance Priorities

By setting these priorities to 1000, you’re forcing these constraints to be prioritized over all others. This tells auto layout that you want the labels to fit to show all of their text.

Lastly, select the title label and change its Intrinsic Size from Default (System Defined) to Placeholder. Do the same to the subtitle label’s Intrinsic Size.

Change Labels' Intrinsic Content Size to Placeholder

This tells Interface Builder to assume the current size of the labels is also their intrinsic content size. By doing that, the constraints are once again unambiguous and the warnings will disappear.

In the end, the auto layout constraints should look like this on RWBasicCell:

RWBasicCell's Final Constraints

Review: Does this satisfy the previous auto layout criteria?

  1. Does each subview have constraints that pin all of their sides? Yes.
  2. Are there constraints going from the top to the bottom of the contentView? Yes.

The titleLabel connects to the top by 20 points, it’s connected to the subtitleLabel by 0 points, and the subtitleLabel connects to the bottom by 20 points.

So auto layout can now determine the height of the cell!

Next, you need to create a segue from RWBasicCell to the scene titled Deviant Article:

Select RWBasicCell and control-drag to the Deviant Article scene. Select Push from the Selection Segue options.

Interface Builder will also automatically change the cell Accessory property to Disclosure Indicator, which doesn’t look great with the design of the app. Change the Accessory back to None.

Set RWBasicCell Accessory to None

Now, whenever a user taps on an RWBasicCell, the app will segue to RWDetailViewController.

Awesome, RWBasicCell is setup! If you build and run the app now, you’ll see that… nothing has changed. What the what?!

Say What...!?

Remember those #warning statements from before? Yep, those are your troublemakers. You need to implement the table view data source and delegate methods.

Implement UITableView Delegate and Data Source

Add the following to the top of RWFeedViewController.m, right below the #import statements:

#import "RWBasicCell.h"
static NSString * const RWBasicCellIdentifier = @"RWBasicCell";

You’ll be using RWBasicCell in both the data source and delegate methods and will need to be able to identify it by the Reuse Identifier you set to RWBasicCellIdentifier in the Storyboard.

Next, start by implementing the data source methods.

Replace tableView:numberOfRowsInSection: with the following:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
  return [self.feedItems count];
}

Then replace tableView:cellForRowAtIndexPath: with the following set of methods:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    return [self basicCellAtIndexPath:indexPath];
}
 
- (RWBasicCell *)basicCellAtIndexPath:(NSIndexPath *)indexPath {
  RWBasicCell *cell = [self.tableView dequeueReusableCellWithIdentifier:RWBasicCellIdentifier forIndexPath:indexPath];
  [self configureBasicCell:cell atIndexPath:indexPath];
  return cell;
}
 
- (void)configureBasicCell:(RWBasicCell *)cell atIndexPath:(NSIndexPath *)indexPath {
  RSSItem *item = self.feedItems[indexPath.row];
  [self setTitleForCell:cell item:item];
  [self setSubtitleForCell:cell item:item];
}
 
- (void)setTitleForCell:(RWBasicCell *)cell item:(RSSItem *)item {
  NSString *title = item.title ?: NSLocalizedString(@"[No Title]", nil);
  [cell.titleLabel setText:title];
}
 
- (void)setSubtitleForCell:(RWBasicCell *)cell item:(RSSItem *)item {
  NSString *subtitle = item.mediaText ?: item.mediaDescription;
 
  // Some subtitles can be really long, so only display the
  // first 200 characters
  if (subtitle.length > 200) {
    subtitle = [NSString stringWithFormat:@"%@...", [subtitle substringToIndex:200]];
  }
 
  [cell.subtitleLabel setText:subtitle];
}

Here’s what’s happening above:

  • In tableView:cellForRowAtIndexPath:, you call basicCellAtIndexPath: to get an RWBasicCell. As you’ll see later on, it’s easier to add additional types of custom cells if you create a helper method like this instead of returning a cell directing by the data source method.
  • In basicCellAtIndexPath:, you dequeue an RWBasicCell, configure it with configureBasicCell:atIndexPath:, and finally, return the configured cell.
  • In configureBasicCell:atIndexPath:, you get a reference to the item at the indexPath, which then gets and sets the titleLabeland subtitleLabel texts on the cell.

Now replace tableView:heightForRowAtIndexPath: with the following set of methods:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
  return [self heightForBasicCellAtIndexPath:indexPath];
}
 
- (CGFloat)heightForBasicCellAtIndexPath:(NSIndexPath *)indexPath {
  static RWBasicCell *sizingCell = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    sizingCell = [self.tableView dequeueReusableCellWithIdentifier:RWBasicCellIdentifier];
  });
 
  [self configureBasicCell:sizingCell atIndexPath:indexPath];
  return [self calculateHeightForConfiguredSizingCell:sizingCell];
}
 
- (CGFloat)calculateHeightForConfiguredSizingCell:(UITableViewCell *)sizingCell {
  [sizingCell setNeedsLayout];
  [sizingCell layoutIfNeeded];
 
  CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
  return size.height;
}

You’re finally using auto layout to calculate the cell height for you! Here’s what’s going on:

  1. In tableView:heightForRowAtIndexPath:, similar to the data source method, you simply return another method that does the actual calculation, heightForBasicCellAtIndexPath:. Once again, this makes it easier to add additional types of custom cells later on.
  2. You might have noticed that heightForBasicCellAtIndexPath: is pretty interesting.
    • This method instantiates a sizingCell using GCD to ensure it’s created only once
    • Calls configureBasicCell:atIndexPath: to configure the cell
    • Returns another method: calculateHeightForConfiguredSizingCell:. You probably already guessed it–this was also extracted to make it easier to add additional cells later on.
  3. Lastly, in calculateHeightForConfiguredSizingCell:, you:
    • First request the cell to lay out its content by calling setNeedsLayout and layoutIfNeeded.
    • Then you ask auto layout to calculate the systemLayoutSizeFittingSize:, passing in the parameter UILayoutFittingCompressedSize, and that means “use the smallest possible size” that fits the auto layout constraints.

Build and Run, and you should see a populated table view!

Populated Table View

This looks great! Try rotating to landscape mode, however, and you might notice something peculiar:

Table View Landscape Problem

“Why is there so much extra space around the subtitle label?” you might ask.

Well, there’s this preferredMaxLayoutWidth property on UILabel that affects its layout. Whenever the screen orientation changes, preferredMaxLayoutWidth does not update.

Fortunately, you can fix this by creating a subclass of UILabel. Add a new class to the project, name it RWLabel and make it a subclass of UILabel.

In RWLabel.m, replace everything within @implementation RWLabel with the following:

- (void)layoutSubviews {
  [super layoutSubviews];
 
  if (self.numberOfLines == 0) {
 
    // If this is a multiline label, need to make sure 
    // preferredMaxLayoutWidth always matches the frame width 
    // (i.e. orientation change can mess this up)
 
    if (self.preferredMaxLayoutWidth != self.frame.size.width) {
      self.preferredMaxLayoutWidth = self.frame.size.width;
      [self setNeedsUpdateConstraints];
    }
  }
}
 
- (CGSize)intrinsicContentSize {
  CGSize size = [super intrinsicContentSize];
 
  if (self.numberOfLines == 0) {
 
    // There's a bug where intrinsic content size 
    // may be 1 point too short
 
    size.height += 1;
  }
 
  return size;
}

As noted in the comments, RWLabel fixes two issues with UILabel:

  1. If the label has multiple lines, it makes sure that preferredMaxLayoutWidth always equals the frame width.
  2. Sometimes, the intrinsicContentSize can be 1 point too short. It adds 1 point to the intrinsicContentSize if the label has multiple lines.

Now you need to update RWBasicCell to use this custom subclass. In RWBasicCell.h, add the following right after the other#import statements:

#import "RWLabel.h"

Then replace both of the @property lines with the following:

@property (nonatomic, weak) IBOutlet RWLabel *titleLabel;
@property (nonatomic, weak) IBOutlet RWLabel *subtitleLabel;

In Main.storyboard, navigate to the Feed View Controller scene, select the title label and change its class to RWLabel.

Change RWBasicCell's labels class to RWLabel

Do the same for the subtitle label.

In RWFeedViewController.m, add the following inside calculateHeightForConfiguredSizingCell: (make it the first line in the method):

sizingCell.bounds = CGRectMake(0.0f, 0.0f, CGRectGetWidth(self.tableView.bounds), 0.0f);

This will make each RWLabel update its preferredMaxLayoutWidth property.

Build and Run; the labels should now be sized and positioned correctly in both portrait and landscape orientation.

Oh Yeah!

Where are the Images?

The app is looking great, but doesn’t it feel like there’s something missing? Oh snap, where are the images?

Deviant Art is all about images, but the app doesn’t show any of them. You need to fix that or your client will think you’ve lost your mind!

A simple approach you might take is to add an image view to RWBasicCell. While this might work for some apps, Deviant Art has both deviations (posts with images) and blog posts (without images), so in this instance, it’s actually better to create a new custom cell.

Add a new class to the project. Name it RWImageCell and make it a subclass of RWBasicCell. The reason it’s a subclass of RWBasicCell is because it too will need a titleLabel and subtitleLabel.

Open RWImageCell.h and add the following right after @interface RWImageCell : RWBasicCell:

@property (nonatomic, weak) IBOutlet UIImageView *customImageView;

This property’s name iscustomImageView rather than imageView, because there is already an imageView property on UITableViewCell.

Open Main.storyboard, select the basic cell you were working on before and copy it using ⌘C, or navigate to Edit > Copy.

Select the table view and press ⌘P or click Edit > Paste to create a new copy of the cell.

Select the new cell and change its Custom Class to RWImageCell. Likewise, change its Reuse Identifier to RWImageCell.

Select RWImageCell’s title label and change its x position to 128 and width to 172. Do the same to the subtitle label.

Interface Builder should give a warning about ambiguous auto layout constraints, because the constraints on those labels put them at a different position than what you just defined.

To correct this, first select RWImageCell’s title label to show its constraints, and then select its leading constraint and delete it. Delete the subtitle label’s leading constraint as well.

Now select RWImageCell’s title label and change its Intrinsic Size to Placeholder. Do the same to the subtitle label’s Intrinsic Size.

These tell Interface Builder to update the Placeholder to the current frame of the view. Check again, because the warnings should now be gone.

You need to add an image view to the cell next, but the height is currently a bit too small for it. So, select RWImageCell and change its Row Height to 141.

Now drag and drop an image view on RWImageCell. Set its size and position to the values shown in the screenshot below.

RWImageCell-ImageView-Rect

Select the image view and press pin the leadingtop and bottom to 20. Pin the width and height to 100, and press Add 5 constraints.

Set ImageView Pin Constraints

Select the image view to show its constraints and then select its bottom constraint to edit it. In the attributes editor, change itsRelation to Greater Than or Equal and its Priority to 999.

Set ImageView's Bottom Constraint

Similarly, select the subtitleLabel to show its constraints, and then select its bottom constraint. In the attributes editor, change its Relation to Greater Than or Equal, yet leave its Priority set to 1000.

This basically says to auto layout, “There should be at least 20 points below both imageView and subtitleLabel, but if you must, you may break the bottom constraint on imageView to show a longer subtitle.”

Next, select RWImageCell’s title label, and press pin leading to 8, and press Add 1 constraint. Do the same for the subtitle label.

In the end, the auto layout constraints should look like this on RWImageCell.

RWImageCell Constraints

You also need to select RWImageCell and actually connect the customImageView outlet to the image view.

Lastly, you need to create a segue from RWImageCell to the scene titled Deviant Media, so the app shows this screen when a user clicks on an RWImageCell.

Similar to how you setup the basic cell, select RWImageCell, control-drag to the scene titled Deviant Media, and then selectPush from the Selection Segue options.

Make sure you also change the Accessory to None.

Great, RWImageCell is setup! Now you just need to add the code to display it.

Show Me the Images!

Add the following to the top of RWFeedViewController.m, right below the #import statements:

#import "RWImageCell.h"
static NSString * const RWImageCellIdentifier = @"RWImageCell";

You’ll add RWImageCell to both the data source and delegate methods. This will need to be able to identify it by the previously specified Reuse Identifier, which is set to RWImageCellIdentifier.

Still in RWFeedViewController.m, replace tableView:cellForRowAtIndexPath: with the following:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  if ([self hasImageAtIndexPath:indexPath]) {
    return [self galleryCellAtIndexPath:indexPath];
  } else {
    return [self basicCellAtIndexPath:indexPath];
  }
}
 
- (BOOL)hasImageAtIndexPath:(NSIndexPath *)indexPath {
  RSSItem *item = self.feedItems[indexPath.row];
  RSSMediaThumbnail *mediaThumbnail = [item.mediaThumbnails firstObject];
  return mediaThumbnail.url != nil;
}
 
- (RWImageCell *)galleryCellAtIndexPath:(NSIndexPath *)indexPath {
  RWImageCell *cell = [self.tableView dequeueReusableCellWithIdentifier:RWImageCellIdentifier forIndexPath:indexPath];
  [self configureImageCell:cell atIndexPath:indexPath];
  return cell;
}
 
- (void)configureImageCell:(RWImageCell *)cell atIndexPath:(NSIndexPath *)indexPath {
  RSSItem *item = self.feedItems[indexPath.row];
  [self setTitleForCell:cell item:item];
  [self setSubtitleForCell:cell item:item];
  [self setImageForCell:(id)cell item:item];
}
 
- (void)setImageForCell:(RWImageCell *)cell item:(RSSItem *)item {
  RSSMediaThumbnail *mediaThumbnail = [item.mediaThumbnails firstObject];
 
  // mediaThumbnails are generally ordered by size,
  // so get the second mediaThumbnail, which is a
  // "medium" sized image
 
  if (item.mediaThumbnails.count >= 2) {
    mediaThumbnail = item.mediaThumbnails[1];
  } else {
    mediaThumbnail = [item.mediaThumbnails firstObject];
  }
 
  [cell.customImageView setImage:nil];
  [cell.customImageView setImageWithURL:mediaThumbnail.url];
}

Sweet! Because you kept your methods concise, you could reuse a lot of them.

Most of the above is similar to how you created and configured RWBasicCell earlier, but here’s a quick walkthrough of the new code:

  1. hasImageAtIndexPath checks if the item at the indexPath has a mediaThumbnail with a non-nil URL–Deviant Art generates thumbnails automatically for all uploaded deviations.
    • If so, it has an image you’ll need to display using an RWImageCell.
  2. configureImageCell:atIndexPath: is similar to the configure method for a basic cell, but it also sets an image via setImageForCell:item:.
  3. setImageForCell:item: attempts to get the second media thumbnail, which in general is a “medium” sized image and works wells for the given image view size.
    • The image is then set on customImageView by using a convenience method provided by AFNetworkingsetImageWithURL:.

Next, you need to update the delegate method for calculating the cell height.

Replace tableView:heightForRowAtIndexPath: with the following:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
  if ([self hasImageAtIndexPath:indexPath]) {
    return [self heightForImageCellAtIndexPath:indexPath];
  } else {
    return [self heightForBasicCellAtIndexPath:indexPath];
  }
}
 
- (CGFloat)heightForImageCellAtIndexPath:(NSIndexPath *)indexPath {
  static RWImageCell *sizingCell = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    sizingCell = [self.tableView dequeueReusableCellWithIdentifier:RWImageCellIdentifier];
  });
 
  [self configureImageCell:sizingCell atIndexPath:indexPath];
  return [self calculateHeightForConfiguredSizingCell:sizingCell];
}

This is very similar to what you already did with RWBasicCell; you’re just reusing methods like a boss. ‘Nuff said!

Build and run, and some pretty sweet art will greet you! By default, the app retrieves items from the “popular” category on Deviant Art, but you can search by artist too.

Try searching for the artist who goes by CheshireCatArt (case insensitive). This is a friend of mine, Devin Kraft, who’s an excellent graphic artist (check out his website).

He’s very active on Deviant Art and posts a good mix of deviations and blog posts. So, bias aside, his account is a good test case to show the app displays both cells with and without images.

CheshireCatArt Posts

Sweet!

If you select a cell, the app will push a new RWDetailViewController onto the navigation controller stack. You may have noticed, however, that when you rotate to landscape orientation, the label’s height is incorrect.

Hmmmm….this seems familiar. Is it deja vu?

Kind of. This is the same issue RWBasicCell had, caused by UILabel‘s preferredMaxLayoutWidth property not being updated on device rotation.

To fix this issue, open Main.storyboard. In each of the scenes for RWDetailViewController, select the label and change itsClass to RWLabel.

Awesome, the app is looking pretty sharp, but you can still improve on it to truly stun and amaze your client.

Optimizing the Table View

While you can use auto layout to calculate cell height starting in iOS 6, it’s most beneficial in iOS 7 because of a new delegate method:

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath;

If you don’t implement this method, the table view proactively calls tableView:heightForRowAtIndexPath: for every cell–even the cells that haven’t been displayed yet and may never be displayed.

If your table view has a lot of cells, this can be very expensive and result in poor initial loading, reloading or scrolling performance.

If you do implement this method, the table view will call it proactively.

Pro tip: You can greatly improve the performance of a table view that has many cells by implementing tableView:estimatedHeightForRowAtIndexPath:.

However, be warned that this delegate method comes with its own set of caveats.

If your cell estimates are inaccurate, then scrolling might be jumpy, the scroll indicator may be misleading, or the content offset may get messed up. For example, if the user rotates the device or taps the status bar to scroll to the top, the result could be a gap at the top/bottom of the table view.

If your table view has only a few cells, or if your cell heights are difficult to estimate in advance, you can opt to skip implementation of this method. However, you won’t have to worry about these issues.

If you notice poor loading, reloading, or scrolling, try implementing this method.

The key to success is finding a good balance between accurately estimating cell height and the performance cost of such calculation.

Ideally, you want to keep this method simple. Start with the simplest case: return a constant value. Add the following toRWFeedViewController.m:

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
  return 100.0f;
}

Build and run, and try scrolling the table view all the way to the bottom. You may notice the scrolling is jumpy, especially as you get towards the middle/end of the table view.

You might also notice that the scroll indicator isn’t very accurate. It seems to indicate there are fewer items are actually in the table view.

Now try rotating the simulator. You’ll likely see that the content offset is messed up e.g. white space at the top/bottom of the table view.

Inaccurate Cell Height Estimate

These are all symptoms of poor cell height estimation. Through better estimates, you greatly alleviate or even eliminate these issues.

Replace tableView:estimatedHeightForRowAtIndexPath: with the following:

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
  if ([self isLandscapeOrientation]) {
    if ([self hasImageAtIndexPath:indexPath]) {
      return 140.0f;
    } else {
      return 120.0f;
    }
  } else {
    if ([self hasImageAtIndexPath:indexPath]) {
      return 235.0f;
    } else {
      return 155.0f;
    }
  }
}
 
- (BOOL)isLandscapeOrientation {
  return UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation);
}

You’re still using magic constants such as 140 and 235 to estimate height, but now you’re picking the best estimate based on the cell contents and the device orientation. You could get even fancier with your estimation but remember: the point of this method is to be fast. You want to return an estimate as quickly as possible.

Build and run. And try scrolling through the list and rotating the device. This new estimation method adds a tiny bit of overhead, but improves the performance. Scrolling is smooth, the scroll indicator is accurate and content offset issues are very infrequent.

It’s hard to eliminate the content offset issue altogether because it’s directly caused by the table view not knowing how large its content area should be. But even with this current implementation, it’s unlikely users would ever, or at least infrequently, experience this issue.

Where to Go From Here

You can download the completed project from here.

Table views are perhaps the most fundamental of structured data views in iOS. As apps become more complex, table views are being used to show all kinds of content in all kinds of custom layouts. Getting your table view cells properly formatted with different kinds and amounts of content is now much easier with auto layout, and the performance can be much better with proper row height estimation.

Auto layout is becoming more important as we get more device sizes and interface builder becomes more powerful. And there’s less code too! There should be plenty of ways to apply the concepts in this tutorial to your own apps to make them more responsive, faster, and better looking.

If you have any comments or questions, please respond below! I’d love to hear from you.


转载:http://www.raywenderlich.com/73602/dynamic-table-view-cell-height-auto-layout

给出我devicelistview的完整内容:// MARK: - 更新后的 DeviceListView 支持传入数据并渲染 class DeviceListView: UIView { private let collectionView = UICollectionView( frame: .zero, collectionViewLayout: createLayout() ) var devices: [NewListDeviceModel] = [] { didSet { collectionView.reloadData() } } var onMoreButtonTap: ((NewListDeviceModel) -> Void)? override init(frame: CGRect) { super.init(frame: frame) setup() } required init?(coder: NSCoder) { super.init(coder: coder) setup() } private static func createLayout() -> UICollectionViewLayout { let layout = UICollectionViewFlowLayout() layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) layout.minimumLineSpacing = 10 return layout } private func setup() { collectionView.backgroundColor = .clear collectionView.showsVerticalScrollIndicator = false collectionView.register(DeviceListCell.self, forCellWithReuseIdentifier: "DeviceListCell") collectionView.delegate = self collectionView.dataSource = self addSubview(collectionView) collectionView.snp.makeConstraints { make in make.edges.equalToSuperview() } } } extension DeviceListView: UICollectionViewDataSource, UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return devices.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DeviceListCell", for: indexPath) as! DeviceListCell let device = devices[indexPath.item] cell.configure(with: device) // 注入点击事件 cell.moreButton.removeTarget(nil, action: nil, for: .allEvents) cell.moreButton.addTarget(self, action: #selector(moreButtonTapped(_:)), for: .touchUpInside) return cell } @objc private func moreButtonTapped(_ sender: UIButton) { let point = sender.convert(CGPoint.zero, to: collectionView) guard let indexPath = collectionView.indexPathForItem(at: point), indexPath.item < devices.count else { return } let device = devices[indexPath.item] onMoreButtonTap?(device) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let width = collectionView.bounds.width - 32 let height = width * 9 / 16 + 60 // 图片高度 + 下方文字 return CGSize(width: collectionView.bounds.width, height: height) } } // MARK: - 自动高度支持 extension DeviceListView { func updateHeightConstraint(in parentView: UIView) { // 移除旧高度约束 removeConstraints(constraints.filter { $0.firstAttribute == .height }) // 计算新高度 let height = calculateRequiredHeight() // 重新设置约束 snp.remakeConstraints { make in make.edges.equalToSuperview() make.height.equalTo(height) } // 触发父级 layout parentView.layoutIfNeeded() // 通知 tableView 刷新该 cell if let tableView = findParentTableView(in: parentView) { tableView.beginUpdates() tableView.endUpdates() } } private func calculateRequiredHeight() -> CGFloat { guard devices.count > 0 else { return 150 } var totalHeight: CGFloat = 0 for _ in 0..<devices.count { let width = collectionView.bounds.width - 32 let itemHeight = width * 9 / 16 + 60 // 图片比例 + 文字区域 totalHeight += itemHeight + 10 // 加间距 } totalHeight -= 10 // 最后一项不加 spacing return max(totalHeight, 150) } private func findParentTableView(in view: UIView) -> UITableView? { var parent = view.superview while parent != nil { if let tableView = parent as? UITableView { return tableView } parent = parent?.superview } return nil } }
12-06
<!-- *Author:jxx *Contact:283591387@qq.com *代码由框架生成,任何更改都可能导致被代码生成器覆盖 *业务请在@/extension/system/dynamic_general_ledger/dynamic_general_ledger.jsx此处编写 --> <template> <view-grid ref="grid" :columns="columns" :detail="detail" :editFormFields="editFormFields" :editFormOptions="editFormOptions" :searchFormFields="searchFormFields" :searchFormOptions="searchFormOptions" :table="table" :extend="extend"> </view-grid> <el-dialog v-model="isDialogVisible" title="文档权限信息" width="90%" :before-close="handleClose" style="max-width: 1400px" > <div class="dialog-content"> <!-- 文档基本信息 --> <!-- <el-descriptions title="文档基本信息" :column="2" border style="margin-bottom: 20px"> <el-descriptions-item label="文档编码">{{ currentDocument.code }}</el-descriptions-item> <el-descriptions-item label="文档名称">{{ currentDocument.name }}</el-descriptions-item> <el-descriptions-item label="文档所属工厂">{{ currentDocument.creatorFactory }}</el-descriptions-item> <el-descriptions-item label="文档安全等级">{{ currentDocument.level }}</el-descriptions-item> <el-descriptions-item label="文档状态">{{ currentDocument.state }}</el-descriptions-item> <el-descriptions-item label="文档类型">{{ currentDocument.type }}</el-descriptions-item> <el-descriptions-item label="项目编码">{{ currentDocument.projectCode }}</el-descriptions-item> <el-descriptions-item label="项目名称">{{ currentDocument.projectName }}</el-descriptions-item> <el-descriptions-item label="项目涉及工厂">{{ currentDocument.projectFactories }}</el-descriptions-item> <el-descriptions-item label="项目类型">{{ currentDocument.projectCgr }}</el-descriptions-item> </el-descriptions> <el-divider /> --> <h3>文档授权信息</h3> </div> <el-tabs v-model="activeAccessTab" class="demo-tabs" type="card"> <el-tab-pane label="用户" name="user"> <div class="user-controls" :style="{ marginBottom: '15px', marginLeft: '5px' }"> <div> <span><strong>搜索权限用户:</strong></span> <el-input v-model="searchUserInput" placeholder="请输入工号/姓名" style="width: 200px; margin-right: 10px" /> <el-button type="primary" @click="handleSearchAccessUser">搜索</el-button> </div> <div style="display: flex; justify-content: flex-end; margin-left: auto"> <!-- <el-button @click="clearUserFilters">重置筛选条件</el-button> --> <!-- <el-button type="primary" @click="addUser" style="margin-left: 10px"> 新增权限 </el-button> --> </div> </div> <el-table ref="accessUserTableRef" :data="userList" row-key="id" border height="500" stripe :header-cell-style="{ background: '#F8F8F9', color: '#333', textAlign: 'center' }" :cell-style="{ textAlign: 'center' }" default-expand-all :scrollbar-always-on="true" class="content-right-table" > <el-table-column label="操作" width="100"> <template #default="scope"> <el-button type="text" :style="{ color: scope.row.accessType === '特殊' ? '#409EFF' : '#909399' }" @click="deleteUser(scope.row)" >删除</el-button > <el-button type="text" :style="{ color: scope.row.accessType === '特殊' ? '#409EFF' : '#909399' }" @click="updateUser(scope.row)" >修改</el-button > </template> </el-table-column> <el-table-column label="查看权限" width="120"> <template #default="scope"> <el-checkbox :model-value="scope.row.checkAccess === 0" :style="{ color: scope.row.accessType === '特殊' ? '#409EFF' : '#909399' }" :disabled="true" /> </template> </el-table-column> <el-table-column prop="userName" label="工号" width="120" /> <el-table-column prop="userTrueName" label="姓名" width="120" /> <el-table-column prop="organization" label="事业部" width="150"> <template #default="scope">{{ scope.row.organization }}</template> </el-table-column> <el-table-column prop="department" label="工厂/部门" width="150"> <template #default="scope">{{ scope.row.department }}</template> </el-table-column> <el-table-column prop="position" label="职位" width="120"> <template #default="scope">{{ scope.row.position || '无' }}</template> </el-table-column> <el-table-column prop="projectRole" label="项目角色" width="120"> <template #default="scope">{{ scope.row.projectRole }}</template> </el-table-column> <el-table-column prop="systemRole" label="系统角色" width="120"> <template #default="scope">{{ scope.row.systemRole }}</template> </el-table-column> <el-table-column prop="accessType" label="权限类型" width="120"> <template #default="scope"> <el-tag :type="getTagType(scope.row.accessType)">{{ scope.row.accessType }}</el-tag> </template> </el-table-column> <el-table-column prop="modifyDate" label="授权日期" width="150"> <template #default="scope">{{ scope.row.modifyDate || '无' }}</template> </el-table-column> <el-table-column prop="modifierName" label="授权人工号" width="120"> <template #default="scope">{{ scope.row.modifierName || '无' }}</template> </el-table-column> <el-table-column prop="modifierTrueName" label="授权人姓名" width="120"> <template #default="scope">{{ scope.row.modifierTrueName || '无' }}</template> </el-table-column> </el-table> <el-pagination class="mt-3" style="display: flex; justify-content: flex-end; margin-top: 10px" background layout="total, sizes, prev, pager, next, jumper" :total="accessUsersTotal" :page-sizes="[10, 20, 30, 50]" :page-size="accessUsersPageSize" :current-page="accessUsersCurrentPage" @size-change="handleAccessUsersSizeChange" @current-change="handleAccessUsersCurrentChange" /> </el-tab-pane> <el-tab-pane label="角色" name="roles"> <div class="user-controls" :style="{ marginBottom: '15px', marginLeft: '5px' }"> <div> <span><strong>搜索权限角色:</strong></span> <el-input v-model="searchRoleInput" placeholder="请输入角色ID/名称" style="width: 200px; margin-right: 10px" /> <el-button type="primary" @click="handleSearchAccessRole">搜索</el-button> </div> <!-- <div style="display: flex; justify-content: flex-end; margin-left: auto"> <el-button type="primary" @click="addUser" style="margin-left: 10px"> 新增权限 </el-button> </div> --> </div> <el-table ref="accessRoleTableRef" :data="accessRolesList" row-key="id" border height="500" style="width: 100%" fit :header-cell-style="{ background: '#F8F8F9', color: '#333', textAlign: 'center' }" :cell-style="{ textAlign: 'center' }" default-expand-all :scrollbar-always-on="true" class="content-right-table" > <el-table-column label="操作"> <template #default="scope"> <el-button type="text" :style="{ color: '#409EFF' }" @click="deleteRole(scope.row)" >删除</el-button > <el-button type="text" :style="{ color: '#409EFF' }" @click="updateRole(scope.row)" >修改</el-button > </template> </el-table-column> <el-table-column label="查看权限"> <template #default="scope"> <el-checkbox :model-value="scope.row.check_access === 0" :disabled="true" /> </template> </el-table-column> <el-table-column prop="role_id" label="角色ID" /> <el-table-column prop="role_name" label="角色名称" /> <el-table-column prop="access_type" label="权限类型"> <template #default="scope"> <el-tag :type="getTagType(scope.row.access_type)">{{ scope.row.access_type }}</el-tag> </template> </el-table-column> <el-table-column prop="modify_date" label="授权日期"> <template #default="scope">{{ scope.row.modify_date || '无' }}</template> </el-table-column> <el-table-column prop="modifier_name" label="授权人工号"> <template #default="scope">{{ scope.row.modifier_name || '无' }}</template> </el-table-column> <el-table-column prop="modifier_truename" label="授权人姓名"> <template #default="scope">{{ scope.row.modifier_truename || '无' }}</template> </el-table-column> </el-table> <el-pagination class="mt-3" style="display: flex; justify-content: flex-end; margin-top: 10px" background layout="total, sizes, prev, pager, next, jumper" :total="accessRolesTotal" :page-sizes="[10, 20, 30, 50]" :page-size="accessRolesPageSize" :current-page="accessRolesCurrentPage" @size-change="handleAccessRolesSizeChange" @current-change="handleAccessRolesCurrentChange" /> </el-tab-pane> </el-tabs> </el-dialog> </template> <script> import extend from "@/extension/system/dynamic_general_ledger/dynamic_general_ledger.jsx"; import { ref, defineComponent,h } from "vue"; import axios from 'axios' import { ElDialog, ElRow, ElCol, ElButton, ElLink, ElMessage,ElIcon} from 'element-plus' import { useRouter } from 'vue-router' import Message from '@/views/index/Message.vue' import { View } from '@element-plus/icons-vue'; export default defineComponent({ setup() { const table = ref({ key: 'id', footer: "Foots", cnName: '动态一本账', name: 'dynamic_general_ledger/dynamic_general_ledger', url: "/dynamic_general_ledger/", sortName: "id" }); const isDialogVisible = ref(false) const dialogContent = ref('') const accessUsersCurrentPage = ref(1) const accessUsersPageSize = ref(10) const accessRolesCurrentPage = ref(1) const accessRolesPageSize = ref(10) const activeAccessTab = ref('user') const userList = ref([]) const accessUsersTotal = ref(0) const searchUserInput = ref('') const accessRolesList = ref([]) const searchRoleInput = ref('') const accessRolesTotal = ref(0) var document = { id: '', dr: '', code: '', name: '', area: '', typeCode: '', type: '', level: '', state: '', projectName: '', projectCgr: '', projectCode: '', projectFactories: '', creatorId: '', creatorName: '', creatorFactoryId: '', creatorFactory: '' } const { proxy } = getCurrentInstance() const openDialog = (params.row) => { debugger userList.value = [] accessRolesList.value = [] searchUserInput.value = '' searchRoleInput.value = '' activeAccessTab.value = 'user' isDialogVisible.value = true // 2. 获取文档信息 getDocument(params.row); //重置分页 accessUsersCurrentPage.value = 1 accessUsersPageSize.value = 10 accessRolesCurrentPage.value = 1 accessRolesPageSize.value = 10 userListUpdate(document, '') roleListUpdate(document, '') } // 搜索按钮点击事件 const handleSearchAccessUser = () => { accessUsersCurrentPage.value = 1 userListUpdate(document, searchUserInput.value) } const handleAccessUsersCurrentChange = (page) => { userList.value = [] accessUsersCurrentPage.value = page userListUpdate(document, searchUserInput.value) } const handleAccessUsersSizeChange = (size) => { accessUsersPageSize.value = size accessUsersCurrentPage.value = 1 userListUpdate(document, searchUserInput.value) } const getDocument = async (params) => { console.log(params) debugger try { const document1 = { code: params.row.file_code } const response1 = axios.post('/api/Sys_File_Info/getDocumentrefer', document1) console.log('===response1.data.value==:', response1.data) console.log('===response1.data.value==:', response1.data.id) console.log(response1) debugger document.id = response1.data.id document.dr = response1.data.dr document.code = response1.data.code document.name = response1.data.name document.area = response1.data.area document.type = response1.data.type document.level = response1.data.level document.state = response1.data.state document.projectName = response1.data.projectName document.projectCgr = response1.data.projectCgr document.projectCode = response1.data.projectCode document.projectFactories = response1.data.projectFactories document.creatorId = response1.data.creatorId document.creatorName = response1.data.creatorName document.creatorFactoryId = response1.data.creatorFactoryId document.creatorFactory = response1.data.creatorFactory console.log('========document:======', document) } catch (error) { // proxy.$message.error(`获取文件信息失败:${error.message}`) } } const userListUpdate = async (document, searchQuery) => { try { const params = { page: accessUsersCurrentPage.value, pageSize: accessUsersPageSize.value, file: document, searchQuery: searchQuery || '' // 传递搜索词 } const response = axios.post('/api/Access_File/getAccessUsersByFileWithPaging', params) userList.value = response.data.items.map((user) => ({ ...user, projectRole: user.projectRole ? user.projectRole : '无' })) accessUsersTotal.value = response.data.total console.log('userList.value:', userList.value) } catch (error) { proxy.$message.error(`获取权限用户失败:${error.message}`) } } const roleListUpdate = async (document, searchQuery) => { try { // const response1 = axios.post('/api/Sys_File_Info/getDocument', { // file: document // }) const params = { page: accessRolesCurrentPage.value, pageSize: accessRolesPageSize.value, file: document, searchQuery: searchQuery || '' // 传递搜索词 } const response = axios.post( '/api/Access_File_Role/getAccessRolesByFileWithPaging', params ) accessRolesList.value = response.data.items.map((role) => ({ ...role, access_type: '角色' })) accessRolesTotal.value = response.data.total } catch (error) { proxy.$message.error(`获取角色权限失败:${error.message}`) console.log(`获取角色权限失败:${error.message}`) } } const editFormFields = ref({}); const editFormOptions = ref([]); const searchFormFields = ref({}); const searchFormOptions = ref([]); const columns = ref([{field:'id',title:'序号',type:'long',width:120,require:true,align:'left',sort:true}, {field:'file_id',title:'文档ID',type:'string',width:110,align:'left'}, {field:'file_type',title:'文档类型',type:'string',width:110,align:'left'}, {field:'business_scenario',title:'所属业务场景',type:'string',width:110,align:'left'}, {field:'belong_stage',title:'所属阶段',type:'string',width:110,align:'left'}, {field:'model_project',title:'所属车型项目',type:'string',width:110,align:'left'}, {field:'project_type',title:'项目类型',type:'string',width:110,align:'left'}, {field:'product_model_number',title:'所属产品型号',type:'string',width:110,align:'left'}, {field:'file_security_level',title:'文档安全等级',type:'string',width:110,align:'left'}, {field:'file_code',title:'文件编号',type:'string',width:110,align:'left'}, {field:'file_version',title:'文件版本',type:'string',width:110,align:'left'}, {field:'file_state',title:'文件状态',type:'string',width:110,align:'left'}, {field:'development_tool',title:'编制工具',type:'string',width:110,align:'left'}, {field:'tool_code',title:'编制工具代码',type:'string',width:110,align:'left'}, {field:'method',title:'编制方式',type:'string',width:110,align:'left'}, {field:'collaboration_information',title:'协作信息',type:'string',width:110,align:'left'}, {field:'review_tool_platform',title:'评审工具/平台',type:'string',width:110,align:'left'}, {field:'review_method',title:'评审方式',type:'string',width:110,align:'left'}, {field:'review_scope',title:'评审范围',type:'string',width:110,align:'left'}, //评审角色需做对比 {field:'review_role',title:'评审角色',type:'string',width:110,align:'left'}, /*{ field: 'review_role', title: '评审角色', type: 'string', width: 110, align: 'left', render: async (value, params) => { console.log(value) debugger // 构造请求参数 const param = { FileType: params.row.file_type, }; try { const response = axios.post('/api/Review_Info/IsRole', param, { headers: { 'Content-Type': 'application/json' } }); const backendRoles = response.data.review_role.split(',').map(r => r.trim()).sort(); const frontendRoles = value.split(',').map(r => r.trim()).sort(); // 判断是否完全一致 if (JSON.stringify(frontendRoles) === JSON.stringify(backendRoles)) { return value; // 一致,直接返回原值 } // 不一致,高亮多余部分 const backendSet = new Set(backendRoles); const highlightRoles = frontendRoles.map(role => { return backendSet.has(role) ? role : `<span style="color: red;">${role}</span>`; }); return highlightRoles.join(', '); } catch (error) { console.error('接口调用失败', error); return value; // 出错返回原始值 } } },*/ {field:'transfer_tool',title:'传输工具',type:'string',width:110,align:'left'}, {field:'transfer_method',title:'传输方式',type:'string',width:110,align:'left'}, {field:'transfer_scope',title:'传输范围',type:'string',width:110,align:'left'}, {field:'transfer_format',title:'传输格式',type:'string',width:110,align:'left'}, {field:'publishing_platform',title:'发布平台',type:'string',width:110,align:'left'}, {field:'sharing_platform',title:'共享平台',type:'string',width:110,align:'left'}, {field:'sharing_method',title:'共享方式',type:'string',width:110,align:'left'}, {field:'sharing_scope',title:'共享范围',type:'string',width:110,align:'left'}, { title: '操作', width: 100, align: 'center', fixed: 'right', // 固定在表格最右侧 render: (h, params) => { return [ h('i', { class: 'el-icon-view preview-icon', // 使用 Element Plus 图标 style: 'cursor: pointer; margin-right: 10px;', // 添加样式 onClick: () => openDialog(params.row || '') // 点击事件处理函数 }), ] } }, {field:'integration_platform',title:'集成平台',type:'string',width:110,align:'left'}, {field:'storage_platform',title:'存储平台',type:'string',width:110,align:'left'}, {field:'retention_period',title:'保管期限',type:'string',width:110,align:'left'}, {field:'storage_strategy',title:'存储策略',type:'string',width:110,align:'left'}, {field:'backup_requirements',title:'备份要求',type:'string',width:110,align:'left'}]); const detail = ref({ cnName: "#detailCnName", table: "#detailTable", columns: [], sortName: "", key: "" }); return { table, extend, editFormFields, editFormOptions, searchFormFields, searchFormOptions, columns, detail, }; }, }); </script> 修改一下,并给我修改后的完整代码
09-18
请注意我发给你的代码中有之前自己写的测试代码,可能不对,你需要按照之前学习到的具体实现进行详细修改。 // // DeviceListOrganizationModel.swift // SurveillanceHome // // Created by MaCong on 2025/12/3. // Copyright © 2025 tplink. All rights reserved. // import Foundation class OrganizationModel { var name: String init(name: String = "默认组织") { self.name = name } } // // DeviceListView.swift // SurveillanceHome // // Created by MaCong on 2025/12/3. // Copyright © 2025 tplink. All rights reserved. // import UIKit class DeviceListView: UIView { private lazy var collectionView = UICollectionView( frame: .zero, collectionViewLayout: Self.createLayout() ) var devices: [TPSSDeviceForDeviceList] = [] { didSet { collectionView.reloadData() invalidateIntrinsicContentSize() } } var onMoreButtonTap: ((TPSSDeviceForDeviceList, TPSSChannelInfo?) -> Void)? override init(frame: CGRect) { super.init(frame: frame) setup() } required init?(coder: NSCoder) { super.init(coder: coder) setup() } private static func createLayout() -> UICollectionViewLayout { let layout = UICollectionViewFlowLayout() layout.sectionInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) layout.minimumLineSpacing = 10 return layout } private func setup() { collectionView.backgroundColor = .clear collectionView.showsVerticalScrollIndicator = false collectionView.register(DeviceListCell.self, forCellWithReuseIdentifier: "DeviceListCell") collectionView.delegate = self collectionView.dataSource = self addSubview(collectionView) collectionView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ collectionView.topAnchor.constraint(equalTo: topAnchor), collectionView.leadingAnchor.constraint(equalTo: leadingAnchor), collectionView.trailingAnchor.constraint(equalTo: trailingAnchor), collectionView.bottomAnchor.constraint(equalTo: bottomAnchor) ]) } } // MARK: - UICollectionView DataSource & Delegate extension DeviceListView: UICollectionViewDataSource, UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return devices.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DeviceListCell", for: indexPath) as! DeviceListCell let device = devices[indexPath.item] // 默认取第一个通道 let channel = device.channelsInfo.first cell.configure(with: device, channel: channel) cell.onMoreTap = { [weak self] dev, chn in self?.onMoreButtonTap?(dev, chn) } return cell } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let width = collectionView.bounds.width > 0 ? collectionView.bounds.width : UIScreen.main.bounds.width let itemWidth = width - 32 let itemHeight = itemWidth * 9 / 16 + 60 return CGSize(width: itemWidth, height: itemHeight) } } // MARK: - 自动高度支持 extension DeviceListView { override var intrinsicContentSize: CGSize { calculateRequiredHeight() } override func invalidateIntrinsicContentSize() { super.invalidateIntrinsicContentSize() DispatchQueue.main.async { self.findParentTableView()?.beginUpdates() self.findParentTableView()?.endUpdates() } } private func calculateRequiredHeight() -> CGSize { guard !devices.isEmpty else { return CGSize(width: UIView.noIntrinsicMetric, height: 150) } let width = bounds.width > 0 ? bounds.width : UIScreen.main.bounds.width let itemWidth = width - 32 let itemHeight = itemWidth * 9 / 16 + 60 let lineSpacing: CGFloat = 10 let totalHeight = CGFloat(devices.count) * itemHeight + lineSpacing * CGFloat(devices.count - 1) return CGSize(width: UIView.noIntrinsicMetric, height: max(totalHeight, 150)) } private func findParentTableView() -> UITableView? { var parent = superview while parent != nil { if let tableView = parent as? UITableView { return tableView } parent = parent?.superview } return nil } } // // DeviceListCell.swift // SurveillanceHome // // Created by MaCong on 2025/12/3. // Copyright © 2025 tplink. All rights reserved. // import UIKit class DeviceListCell: UICollectionViewCell { @IBOutlet weak var nameLabel: UILabel! @IBOutlet weak var thumbnailImageView: UIImageView! @IBOutlet weak var statusLabel: UILabel! @IBOutlet weak var moreButton: UIButton! private var device: TPSSDeviceForDeviceList! private var channel: TPSSChannelInfo? override func awakeFromNib() { super.awakeFromNib() setupUI() } private func setupUI() { layer.cornerRadius = 8 clipsToBounds = true backgroundColor = .tpbBackground } func configure(with device: TPSSDeviceForDeviceList, channel: TPSSChannelInfo? = nil) { self.device = device self.channel = channel // displayName 是 alias 或通道名 nameLabel.text = device.displayName // 在线状态:displayOnline 是关键字段 if device.displayOnline { statusLabel.text = "在线" statusLabel.textColor = .systemGreen } else { statusLabel.text = "离线" statusLabel.textColor = .systemRed } // 快照 URL(SDK 中未直接暴露,但底层使用 snapshotUrl 字段) if let urlStr = device.value(forKey: "snapshotUrl") as? String, let url = URL(string: urlStr) { #if canImport(Kingfisher) thumbnailImageView.kf.setImage(with: url) #else print(" loadImage: $url)") #endif } else { if #available(iOS 13.0, *) { let img = UIImage(systemName: "camera.fill") ?? UIImage() thumbnailImageView.image = img thumbnailImageView.backgroundColor = UIColor.tpbBackground } else { // Fallback on earlier versions } } moreButton.removeTarget(nil, action: nil, for: .allEvents) moreButton.addTarget(self, action: #selector(moreButtonTapped), for: .touchUpInside) } @objc private func moreButtonTapped() { onMoreTap?(device, channel) } var onMoreTap: ((TPSSDeviceForDeviceList, TPSSChannelInfo?) -> Void)? } // // DeviceListTitleView.swift // SurveillanceHome // // Created by MaCong on 2025/12/3. // Copyright © 2025 tplink. All rights reserved. // import UIKit import SnapKit class NewTitleView: UIView { private lazy var titleLabel: UILabel = { let label = UILabel() label.text = "" label.textColor = UIColor.tpbTextPrimary label.font = .boldSystemFont(ofSize: 18) label.lineBreakMode = .byTruncatingTail label.numberOfLines = 1 return label }() private lazy var arrowImageView: UIImageView = { let imageView = UIImageView(image: TPImageLiteral("devicelist_dropdown_arrow_nor")) imageView.contentMode = .scaleAspectFit return imageView }() lazy var titleLoadingOrgView: SimpleLoadingView = { let view = SimpleLoadingView(frame: CGRect(x: 0, y: 0, width: 24, height: 24)) view.isHidden = true return view }() internal var isInExpandStatus: Bool = false internal var isAnimating: Bool = false private var isExccedingWidth: Bool = false public var didClickCallback: ((Bool) -> Void)? var titleText: String { get { titleLabel.text ?? "" } set { titleLabel.text = newValue setNeedsLayout() } } override init(frame: CGRect) { super.init(frame: frame) setupViews() bindActions() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupViews() { addSubview(titleLabel) addSubview(arrowImageView) addSubview(titleLoadingOrgView) reloadView() } override func layoutSubviews() { super.layoutSubviews() let width = titleLabel.intrinsicContentSize.width if width > titleLabel.frame.width && titleLabel.frame.width > 0 { isExccedingWidth = true } else { isExccedingWidth = false } reloadView() } func reloadView() { // 布局更新... titleLabel.snp.updateConstraints { make in make.leading.equalToSuperview().offset(6) make.top.bottom.equalToSuperview() make.height.greaterThanOrEqualTo(20) } arrowImageView.snp.updateConstraints { make in make.centerY.equalToSuperview() make.leading.equalTo(titleLabel.snp.trailing).offset(4) make.size.equalTo(20) if isExccedingWidth { make.trailing.equalToSuperview().offset(-36) } else { make.trailing.equalToSuperview() } } titleLoadingOrgView.snp.updateConstraints { make in make.leading.equalTo(arrowImageView.snp.trailing).offset(12) make.centerY.equalToSuperview() make.size.equalTo(24) } } private func bindActions() { let tap = UITapGestureRecognizer(target: self, action: #selector(didClickTitleView)) self.isUserInteractionEnabled = true self.addGestureRecognizer(tap) } @objc private func didClickTitleView() { didClickCallback?(isInExpandStatus) } func changeToStatus(expand: Bool) { isInExpandStatus = expand rotateArrow(expand: expand) } private func rotateArrow(expand: Bool) { let rotation = CABasicAnimation(keyPath: "transform.rotation.z") rotation.duration = 0.3 rotation.fillMode = .forwards rotation.isRemovedOnCompletion = false rotation.fromValue = expand ? 0 : -Double.pi rotation.toValue = expand ? -Double.pi : 0 arrowImageView.layer.add(rotation, forKey: "rotateArrow") arrowImageView.transform = CGAffineTransform(rotationAngle: expand ? -CGFloat(Double.pi) : 0) } func showLoadingAnimation() { isAnimating = true titleLoadingOrgView.isHidden = false titleLabel.isHidden = true arrowImageView.isHidden = true } func hideLoadingAnimation() { isAnimating = false titleLoadingOrgView.isHidden = true titleLabel.isHidden = false arrowImageView.isHidden = false } } // // DeviceListNewViewController.swift // SurveillanceHome // // Created by MaCong on 2025/12/3. // Copyright © 2025 tplink. All rights reserved. // import UIKit import SnapKit // MARK: - DeviceListNewViewController class DeviceListNewViewController: SurveillanceCommonTableController, SelectOrganizationViewDelegate { // MARK: - UI Components private lazy var titleView: NewTitleView = { let view = NewTitleView() view.titleText = "设备列表" view.didClickCallback = { [weak self] _ in guard let self = self else { return } let selectOrgView = self.selectOrganizationView let titleView = self.titleView if titleView.isAnimating { return } if !titleView.isInExpandStatus { titleView.isAnimating = true self.view.addSubview(selectOrgView) selectOrgView.show(animated: true) { titleView.isAnimating = false } } else { titleView.isAnimating = true self.hideLoadingView() selectOrgView.dismiss(animated: true) { titleView.isAnimating = false } } titleView.changeToStatus(expand: !titleView.isInExpandStatus) } return view }() private lazy var multiScreenButton: UIButton = { let btn = UIButton(type: .custom) btn.setImage(TPImageLiteral("media_player_switch_multi_live"), for: .normal) btn.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) btn.addTarget(self, action: #selector(clickMultileLive), for: .touchUpInside) return btn }() // 当前筛选条件 var selectedTabType: DeviceListMasterSelectedType = .all { didSet { UserDefaults.standard.deviceListSelectedType = selectedTabType forceRefreshDeviceList() } } var selectedSiteInfo: TPSSVMSSubsiteInfo? { didSet { saveSelectedSiteInfo(selectedSiteInfo) forceRefreshDeviceList() } } private func saveSelectedSiteInfo(_ info: TPSSVMSSubsiteInfo?) { guard let siteInfo = info else { try? FileManager.default.removeItem(at: URL(fileURLWithPath: DeviceListMasterViewController.getDocumentsPath(path: DeviceListMasterViewController.kSelectedSiteFileName) ?? "")) return } do { let data = try NSKeyedArchiver.archivedData(withRootObject: siteInfo, requiringSecureCoding: true) try data.write(to: URL(fileURLWithPath: DeviceListMasterViewController.getDocumentsPath(path: DeviceListMasterViewController.kSelectedSiteFileName) ?? "")) } catch { print(error) } } private lazy var selectOrganizationView: SelectOrganizationView = { let view = SelectOrganizationView() view.rootViewController = self view.delegate = self view.frame = CGRect( x: 0, y: SelectOrganizationView.statusBarHeight + SelectOrganizationView.navigationBarHeight, width: screenWidth, height: screenHeight - SelectOrganizationView.statusBarHeight - SelectOrganizationView.navigationBarHeight ) return view }() // MARK: - Data & View private var devices: [TPSSDeviceForDeviceList] = [] { didSet { deviceListView.devices = devices } } private lazy var deviceListView: DeviceListView = { let view = DeviceListView() view.onMoreButtonTap = { [weak self] device, channel in self?.showDeviceMenu(for: device, channel: channel) } return view }() // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() setNavigation() tableView.contentInsetAdjustmentBehavior = .automatic reloadData() loadDevicesFromContext() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.setNavigationBarHidden(false, animated: false) } // MARK: - Layout Setup override func tpbSetupSubviews() { super.tpbSetupSubviews() } override func tpbMakeConstraint() { tableView.snp.remakeConstraints { make in make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top) make.leading.trailing.equalToSuperview() make.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom) } } // MARK: - Navigation Bar private func setNavigation() { navigationItem.titleView = titleView titleView.snp.makeConstraints { make in make.width.lessThanOrEqualTo(200) make.height.equalTo(44) } let backButtonItem = self.tpbCreateLeftBarButtonItem(with: TPImageLiteral("common_light_back_nor"), andTarget: self, andAction: #selector(jumpToXXX)) let centralButtonItem = self.tpbCreateLeftBarButtonItem(with: TPImageLiteral("central_surveillance_button"), andTarget: self, andAction: #selector(centralButtonClicked)) let messageButtonItem = self.tpbCreateLeftBarButtonItem(with: TPImageLiteral("tabbar_message_nor"), andTarget: self, andAction: #selector(onMessageButtonTapped)) navigationItem.leftBarButtonItems = [backButtonItem, centralButtonItem] navigationItem.rightBarButtonItem = messageButtonItem } // MARK: - Mock Sections (示例组件) private func createDeviceCountView() -> UIView { let containerView = UIView() containerView.backgroundColor = .tpbCard containerView.layer.cornerRadius = 4 containerView.clipsToBounds = true let labels = ["NVR", "4K", "2K", "HD"] var categoryViews: [UIView] = [] for text in labels { let categoryView = UIView() categoryView.backgroundColor = UIColor(white: 0.93, alpha: 1.0) categoryView.layer.cornerRadius = 8 categoryView.clipsToBounds = true let label = UILabel() label.text = text label.textColor = UIColor.tpbTextPrimary label.font = UIFont.systemFont(ofSize: 15, weight: .semibold) label.textAlignment = .center categoryView.addSubview(label) label.snp.makeConstraints { make in make.edges.equalToSuperview().inset(10) } containerView.addSubview(categoryView) categoryViews.append(categoryView) } for (index, view) in categoryViews.enumerated() { view.snp.makeConstraints { make in make.height.equalTo(60) make.centerY.equalTo(containerView) if index == 0 { make.leading.equalTo(containerView).offset(20) } else { make.leading.equalTo(categoryViews[index - 1].snp.trailing).offset(16) make.width.equalTo(categoryViews[0]) } if index == labels.count - 1 { make.trailing.equalTo(containerView).offset(-20) } } } return containerView } private func createStorageUsageView() -> UIView { let containerView = UIView() containerView.backgroundColor = .tpbCard containerView.layer.cornerRadius = 8 containerView.clipsToBounds = true let titleLabel = UILabel() titleLabel.text = "存储空间" titleLabel.font = UIFont.systemFont(ofSize: 14, weight: .medium) titleLabel.textColor = .tpbTextPrimary containerView.addSubview(titleLabel) let progressView = UIProgressView(progressViewStyle: .default) progressView.progressTintColor = UIColor.systemBlue progressView.trackTintColor = UIColor(white: 0.9, alpha: 1.0) containerView.addSubview(progressView) let detailLabel = UILabel() detailLabel.font = UIFont.systemFont(ofSize: 13, weight: .regular) detailLabel.textColor = .tpbTextPrimary detailLabel.textAlignment = .center containerView.addSubview(detailLabel) let used = 1.2, total = 5.0 let percent = Float(used / total) progressView.setProgress(percent, animated: false) let percentage = Int(percent * 100) detailLabel.text = String(format: "%.1f TB / %.1f TB (%d%%)", used, total, percentage) titleLabel.snp.makeConstraints { make in make.top.leading.equalTo(containerView).offset(16) } progressView.snp.makeConstraints { make in make.centerX.centerY.equalTo(containerView) make.width.equalTo(200) make.height.equalTo(6) } detailLabel.snp.makeConstraints { make in make.top.equalTo(progressView.snp.bottom).offset(8) make.centerX.equalTo(progressView) } return containerView } // MARK: - Table Cell Factory private func createDeviceListCell() -> TPBBaseTableCellModel { let cellModel = TPBBaseTableCellModel.customContent(with: deviceListView) cellModel.height = TPBTableElementHeight.automatic() return cellModel } private func createMultiScreenHeader() -> UIView { let headerView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 44)) headerView.backgroundColor = .clear let container = TPBBaseView() container.backgroundColor = .clear container.layer.cornerRadius = 3 container.clipsToBounds = true headerView.addSubview(container) container.snp.makeConstraints { make in make.trailing.equalTo(headerView).offset(-16) make.centerY.equalTo(headerView) make.size.equalTo(CGSize(width: 44, height: 44)) } multiScreenButton.removeFromSuperview() container.addSubview(multiScreenButton) multiScreenButton.snp.makeConstraints { make in make.edges.equalToSuperview() } return headerView } private func createMergedDeviceSection() -> TPBTableSectionModel { let section = TPBTableSectionModel() section.customHeaderView = createMultiScreenHeader() section.sectionHeaderHeight = TPBTableElementHeight.customHeight(44) section.cellModelArray = [createDeviceListCell()] return section } // MARK: - Reload Data private func reloadData() { var sections = [TPBTableSectionModel]() let section0 = TPBTableSectionModel() let deviceCountView = createDeviceCountView() let deviceCountCell = TPBBaseTableCellModel.customContent(with: deviceCountView) deviceCountCell.height = TPBTableElementHeight.customHeight(100) section0.cellModelArray = [deviceCountCell] sections.append(section0) let section1 = TPBTableSectionModel() let storageView = createStorageUsageView() let storageCell = TPBBaseTableCellModel.customContent(with: storageView) storageCell.height = TPBTableElementHeight.customHeight(100) section1.cellModelArray = [storageCell] sections.append(section1) let section2 = createMergedDeviceSection() sections.append(section2) sectionArray = sections tableView.reloadData() } // MARK: - Actions @objc private func clickMultileLive() { print("多屏播放按钮被点击") } @objc private func jumpToXXX() { print("返回上一页") } @objc private func centralButtonClicked() { let vc = CentralJumpViewController() let navi = BaseNavigationController(rootViewController: vc) navi.view.frame = self.view.frame vc.currentSiteId = selectedTabType == .all ? nil : selectedSiteInfo?.siteId vc.maskDidClickBlock = { navi.view.removeFromSuperview() } UIApplication.shared.keyWindow?.addSubview(navi.view) } @objc private func onMessageButtonTapped() { print("消息中心被点击") } // MARK: - Loading & Org Selection func hideLoadingView() { titleView.hideLoadingAnimation() } func didSelectOrganization(_ organization: OrganizationModel) { titleView.titleText = organization.name hideLoadingView() reloadData() } func didCancelSelectOrganization() {} // MARK: - Load Devices private func loadDevicesFromContext() { DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self else { return } let allDevices = TPAppContextFactory.shared().devices(inDeviceGroup: "default", includingHiddenChannels: false) let filtered = allDevices.filter { $0.listType == .remote && $0.displayOnline } DispatchQueue.main.async { self.devices = filtered } } } private func forceRefreshDeviceList() { loadDevicesFromContext() } // MARK: - Show Menu private func showDeviceMenu(for device: TPSSDeviceForDeviceList, channel: TPSSChannelInfo?) { let menu = DeviceListMenuView(frame: .zero) DeviceListMenuView.configure(device: device, channel: channel) // menu.action = { [weak self] (item, dev, chn) in // switch item { // case .setting: // self?.jumpToDeviceSetting(for: dev, channel: chn) // case .alarmMode: // self?.toggleNotification(for: dev, channel: chn) // case .upgrade: // self?.startFirmwareUpgrade(for: dev, channel: chn) // case .unbind: // self?.confirmUnbindDevice(for: dev, channel: chn) // case .collect: // self?.toggleFavoriteStatus(for: dev, channel: chn) // @unknown default: // break // } // } _ = presentGuideWith( viewToPresent: menu, size: CGSize(width: 200, height: menu.items.preferredheight), source: multiScreenButton ) } // // MARK: - Menu Actions // private func toggleNotification(for device: TPSSDeviceForDeviceList, channel: TPSSChannelInfo?) { // let enable = !(channel?.messageEnable ?? device.messageEnable) // let requestID = TPAppContextFactory.shared().requestSetMessagePushEnabled( // enable, // forDevice: device.identifier, // channel: channel?.channelId.intValue ?? -1, // of: device.listType // ) // if requestID < TPSS_EC_OK { // ToastView.showWarningToast(title: TPAppContextFactory.shared().errorMessage(for: requestID)) // } else { // ToastView.showLoadingToast(cirleWithMessage: nil) // } // } // // private func jumpToDeviceSetting(for device: TPSSDeviceForDeviceList, channel: TPSSChannelInfo?) { // let vc = DeviceSettingViewController(device: device, channel: channel) // navigationController?.pushViewController(vc, animated: true) // } // // private func startFirmwareUpgrade(for device: TPSSDeviceForDeviceList, channel: TPSSChannelInfo?) { // let vc = FirmwareUpgradeViewController(device: device, channel: channel) // navigationController?.pushViewController(vc, animated: true) // } // // private func confirmUnbindDevice(for device: TPSSDeviceForDeviceList, channel: TPSSChannelInfo?) { // let alert = UIAlertController(title: "解绑设备", message: "确定要解绑此设备吗?", preferredStyle: .alert) // alert.addAction(.init(title: "取消", style: .cancel)) // alert.addAction(.init(title: "确定", style: .destructive) { _ in // UnbindService.unbind(device: device, channel: channel) // }) // present(alert, animated: true) // } // // private func toggleFavoriteStatus(for device: TPSSDeviceForDeviceList, channel: TPSSChannelInfo?) { // let isVms = TPAppContextFactory.shared().isVmsLogin // let isCollected = isVms ? (channel?.isVMSFavorited ?? device.isVMSFavorited) : device.isCollected // // FavoriteService.updateFavoriteStatus(of: device, channel: channel, isFavorite: !isCollected) { [weak self] success in // if success { // DispatchQueue.main.async { // ToastView.showTopWarningWithLeftIconToast(title: !isCollected ? "已收藏" : "已取消收藏", iconName: "device_collect_success") // } // } // } // } } 其中DeviceListOrganizationModel、titleview部分可以不做修改,不影响设备展示
12-06
基于STM32 F4的永磁同步电机无位置传感器控制策略研究内容概要:本文围绕基于STM32 F4的永磁同步电机(PMSM)无位置传感器控制策略展开研究,重点探讨在不依赖物理位置传感器的情况下,如何通过算法实现对电机转子位置和速度的精确估计与控制。文中结合嵌入式开发平台STM32 F4,采用如滑模观测器、扩展卡尔曼滤波或高频注入法等先进观测技术,实现对电机反电动势或磁链的估算,进而完成无传感器矢量控制(FOC)。同时,研究涵盖系统建模、控制算法设计、仿真验证(可能使用Simulink)以及在STM32硬件平台上的代码实现与调试,旨在提高电机控制系统的可靠性、降低成本并增强环境适应性。; 适合人群:具备一定电力电子、自动控制理论基础和嵌入式开发经验的电气工程、自动化及相关专业的研究生、科研人员及从事电机驱动开发的工程师。; 使用场景及目标:①掌握永磁同步电机无位置传感器控制的核心原理与实现方法;②学习如何在STM32平台上进行电机控制算法的移植与优化;③为开发高性能、低成本的电机驱动系统提供技术参考与实践指导。; 阅读建议:建议读者结合文中提到的控制理论、仿真模型与实际代码实现进行系统学习,有条件者应在实验平台上进行验证,重点关注观测器设计、参数整定及系统稳定性分析等关键环节。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值