You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 18:16:37