UITableViewCell图片自动布局约束失效原因及解决方法
问题:UITableViewCell中UIImageView约束在重载数据/单元格复用后失效
首次遇到该问题,此前均能自行解决。当前现象:重载数据或拖拽单元格至UITableView边界外后,UITableViewCell内的UIImageView出现自动布局约束失效、无法正常调整尺寸的问题。视图控制器初始加载时布局完全正常。
调试日志
022-07-31 16:57:49.849599+0200 gitHubRecrutationChallengeTry2[60468:8339844] [LayoutConstraints] Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. ( "<NSLayoutConstraint:0x600003198b40 'id12' UIImageView:0x14d611130.width == UIImageView:0x14d611130.height (active)>", "<NSLayoutConstraint:0x600003198b90 'id13' V:[UIImageView:0x14d611130]-(0)-| (active, names: '|':UITableViewCellContentView:0x14d6109d0 )>", "<NSLayoutConstraint:0x600003198be0 'id14' V:|-(0)-[UIImageView:0x14d611130] (active, names: '|':UITableViewCellContentView:0x14d6109d0 )>", "<NSLayoutConstraint:0x600003198c30 'id15' H:|-(20)-[UIImageView:0x14d611130] (active, names: '|':UITableViewCellContentView:0x14d6109d0 )>", "<NSLayoutConstraint:0x600003198d20 'id18' H:[UILabel:0x14d610b70]-(10)-| (active, names: '|':UITableViewCellContentView:0x14d6109d0 )>", "<NSLayoutConstraint:0x600003198d70 'id19' H:[UIImageView:0x14d611130]-(20)-[UILabel:0x14d610b70] (active)>", "<NSLayoutConstraint:0x6000031994a0 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x14d6109d0.height == 320.333 (active)>", "<NSLayoutConstraint:0x600003199450 'UIView-Encapsulated-Layout-Width' UITableViewCellContentView:0x14d6109d0.width == 370 (active)>" ) Will attempt to recover by breaking constraint <NSLayoutConstraint:0x600003198b40 'id12' UIImageView:0x14d611130.width == UIImageView:0x14d611130.height (active)> Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger. The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.
相关代码
SavedUsersTableViewController代码
import UIKit import RealmSwift class SavedUsersTableViewController: UIViewController, UserDetailViewControllerDelegate { func userRemovedFromFavorites(userModel: UserModel) { // retrivedUsers.removeAll(where: {$0.id == userModel.id}) // dataStore.removeUser(userModels: [userModel]) getSavedUsers() tableView.reloadData() } func userAddedToFavorites() { // getSavedUsers() // tableView.reloadData() } @IBOutlet weak var tableView: UITableView! let localRealm = try! Realm() private lazy var dataStore = ImageDataStore() private lazy var loadingQueue = OperationQueue() private lazy var loadingOperations = [IndexPath : DataLoadOperation]() private var retrivedUsers: [UserModel] = [] override func viewDidLoad() { title = "Saved Users" super.viewDidLoad() tableView.delegate = self tableView.dataSource = self tableView.prefetchDataSource = self getSavedUsers() tableView.reloadData() } func getSavedUsers() { loadingQueue.cancelAllOperations() loadingOperations.removeAll() retrivedUsers = [] let array = Array(localRealm.objects(RealmUserModel.self).sorted(byKeyPath: "userName", ascending: true)) for realmUser in array { let user = UserModel(realmUserModel: realmUser) retrivedUsers.append(user) } dataStore = ImageDataStore(userModels: retrivedUsers) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let destinationVC = segue.destination as? UserDetailViewController { if let sender = sender as? UserCell { // destinationVC.image = sender.userAvatarImage.image destinationVC.delegate = self if let user = sender.getUserModel() { destinationVC.assignUserModel(user: user) }else { fatalError("Failed at retrieving user model from the cell") } } } } } extension SavedUsersTableViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { guard let cell = cell as? UserCell else { return } let updateCellClosure: (UIImage?) -> () = { [unowned self] (image) in cell.configureCell(image) self.loadingOperations.removeValue(forKey: indexPath) } if let dataLoader = loadingOperations[indexPath] { if let image = dataLoader.image { cell.configureCell(image) loadingOperations.removeValue(forKey: indexPath) } else { dataLoader.loadingCompleteHandler = updateCellClosure } } else { if let dataLoader = dataStore.loadImage(at: indexPath.row) { dataLoader.loadingCompleteHandler = updateCellClosure loadingQueue.addOperation(dataLoader) loadingOperations[indexPath] = dataLoader } } } func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { if let dataLoader = loadingOperations[indexPath] { dataLoader.cancel() loadingOperations.removeValue(forKey: indexPath) } } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { // if let cell = tableView.cellForRow(at: indexPath) as? UserCell { // performSegue(withIdentifier: "ToSavedUserDetails", sender: cell) // } } } extension SavedUsersTableViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return retrivedUsers.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "SavedUserCell", for: indexPath) as! UserCell cell.configureCell(user: retrivedUsers[indexPath.row]) let updateCellClosure: (UIImage?) -> () = { [unowned self] (image) in cell.configureCell(image) self.loadingOperations.removeValue(forKey: indexPath) } if let dataLoader = loadingOperations[indexPath] { if let image = dataLoader.image { cell.configureCell(image) loadingOperations.removeValue(forKey: indexPath) } else { dataLoader.loadingCompleteHandler = updateCellClosure } } else { if let dataLoader = dataStore.loadImage(at: indexPath.row) { dataLoader.loadingCompleteHandler = updateCellClosure loadingQueue.addOperation(dataLoader) loadingOperations[indexPath] = dataLoader } } return cell } } extension SavedUsersTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { if let _ = loadingOperations[indexPath] { return } if let dataLoader = dataStore.loadImage(at: indexPath.row) { loadingQueue.addOperation(dataLoader) loadingOperations[indexPath] = dataLoader } } } func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { if let dataLoader = loadingOperations[indexPath] { dataLoader.cancel() loadingOperations.removeValue(forKey: indexPath) } } } }
UserCell代码
// // UserCell.swift // gitHubRecrutationChallengeTry2 // // Created by Tomasz Kubiak on 7/23/22. // import UIKit class UserCell: UITableViewCell { public static let identifier = "UserCell" @IBOutlet weak var userNameLabel: UILabel! @IBOutlet weak var userAvatarImage: UIImageView! @IBOutlet weak var loadingIndicator: UIActivityIndicatorView! @IBOutlet weak var tipLabel: UILabel! private var userModel: UserModel? override func awakeFromNib() { super.awakeFromNib() self.userAvatarImage.translatesAutoresizingMaskIntoConstraints = false } override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) } func configureCell(user: UserModel){ userModel = user userNameLabel.isHidden = false userNameLabel.text = user.userName tipLabel.isHidden = true } /// Configure cell image /// - Parameter image: UIimage to pass to the cell func configureCell(_ image: UIImage?) { DispatchQueue.main.async { [unowned self] in self.displayImage(image) } } /// Configure cell without username, image, or loading indicator - text to display is "Search of an user or check favorites" func configureCell() { tipLabel.text = "Search for an user or check favorites!" userNameLabel.isHidden = true tipLabel.isHidden = false loadingIndicator.stopAnimating() userAvatarImage.image = .none } private func displayImage(_ image: UIImage?) { if let _image = image { userAvatarImage.image = _image loadingIndicator.stopAnimating() } else { // where to put loading indicator? loadingIndicator.startAnimating() userAvatarImage.image = .none } } public func getUserModel() -> UserModel? { return userModel } }
解决方案
1. 修复translatesAutoresizingMaskIntoConstraints设置问题
UserCell的awakeFromNib方法中,手动设置userAvatarImage.translatesAutoresizingMaskIntoConstraints = false属于冗余操作——通过Storyboard/XIB创建并关联约束的视图,该属性默认就是false。重复设置会在单元格复用时引发约束冲突,直接删除该行代码:
override func awakeFromNib() { super.awakeFromNib() // 移除该行:self.userAvatarImage.translatesAutoresizingMaskIntoConstraints = false }
2. 降低UIImageView宽高比约束优先级
从日志可知,系统为满足单元格高度约束,被迫打破了UIImageView的宽高比约束。将宽高比约束优先级调低(如设为999),让其在与单元格封装约束冲突时,优先满足单元格布局需求:
- 打开Storyboard/XIB,找到UIImageView的
width == height约束 - 在属性检查器中,将
Priority从1000改为999
3. 完善单元格复用状态重置
单元格复用前需确保所有视图状态正确重置,避免旧布局影响新内容。在UserCell的configureCell(user:)方法中补充重置逻辑:
func configureCell(user: UserModel){ userModel = user userNameLabel.isHidden = false userNameLabel.text = user.userName tipLabel.isHidden = true // 重置图片和加载状态 userAvatarImage.image = nil loadingIndicator.startAnimating() }
4. 移除重复的图片加载逻辑
cellForRowAt和willDisplay中存在重复的图片加载逻辑,会导致重复操作引发布局异常。保留willDisplay中的加载逻辑,删除cellForRowAt内的对应代码:
修改后的cellForRowAt:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "SavedUserCell", for: indexPath) as! UserCell cell.configureCell(user: retrivedUsers[indexPath.row]) return cell }
内容的提问来源于stack exchange,提问作者tomtom
相关产品推荐
相关产品推荐

