CollectionView FlowLayout 自定义单元格渲染问题

CollectionView FlowLayout custom cell rendering issues

我有一个带有自定义流程布局的集合视图和一个自定义集合视图单元格(没有故事板)。自定义单元格在背景视图上有一个 CAGradientLayer。当从暂停状态返回或更改 traitcollection 时,该层渲染不正确(参见图片:) )它应该是单元格的整个宽度。 另外,当滚动到下面的屏幕外项目时,渐变层根本不呈现?

旋转一次设备或滚动即可解决问题... 我不确定这是否可以在自定义单元格 class 或集合视图 viewcontroller 中解决。复用问题? 非常感谢任何帮助!

注意:通用应用程序,ipad 和 iphone,也兼容分屏。

单元格class

class NormalProjectCell: UICollectionViewCell, SelfConfiguringProjectCell {
    //MARK: - Properties
    let titleLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .title3), andColor: .label)
    let lastEditedLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .caption1), andColor: .secondaryLabel)
    let imageView = ProjectImageView(frame: .zero)
    var stackView = UIStackView()
    var backgroundMaskedView = UIView()
    
    //MARK: - Init
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.layer.cornerRadius = 35
        
        let seperator = Separator(frame: .zero)
        
        stackView = UIStackView(arrangedSubviews: [seperator, titleLabel, lastEditedLabel, imageView])
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .vertical
        stackView.distribution = .fillProportionally
        stackView.spacing = 5
        stackView.setCustomSpacing(10, after: lastEditedLabel)
        stackView.insertSubview(backgroundMaskedView, at: 0)
        contentView.addSubview(stackView)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    //MARK: - Layout
    override func layoutSubviews() {
        super.layoutSubviews()
        
        NSLayoutConstraint.activate([
            titleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 20),
            
            stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            stackView.topAnchor.constraint(equalTo: contentView.topAnchor),
            stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
        ])
        
        backgroundMaskedView.translatesAutoresizingMaskIntoConstraints = false
        backgroundMaskedView.backgroundColor = .tertiarySystemBackground
        backgroundMaskedView.pinToEdges(of: stackView)
        
        let gradientMaskLayer = CAGradientLayer()
        gradientMaskLayer.frame = backgroundMaskedView.bounds
        gradientMaskLayer.colors = [UIColor.systemPurple.cgColor, UIColor.clear.cgColor]
        gradientMaskLayer.locations = [0, 0.4]

        backgroundMaskedView.layer.mask = gradientMaskLayer
    }
    
    //MARK: - Configure
    func configure(with project: ProjectsController.Project) {
        titleLabel.text = project.title
        lastEditedLabel.text = project.lastEdited.customMediumToString
        
        imageView.image = Bundle.getProjectImage(project: project)
    }
}

和带有 collectionView 的 viewcontroller:

class ProjectsViewController: UIViewController {
    //MARK: - Types
    enum Section: CaseIterable {
        case normal
    }
    
    //MARK: - Properties
    let projectsController = ProjectsController()
    
    var collectionView: UICollectionView!
    var dataSource: UICollectionViewDiffableDataSource<Section, Project>!
    
    var lastScrollPosition: CGFloat = 0
    var isSearching = false
    
    let searchController = UISearchController()
    
    //MARK: - ViewController Methods
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureViewController()
        configureSearchController()
        configureCollectionView()
        createDataSource()
        updateData(on: projectsController.filteredProjects())
        
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        if isSearching {
            isSearching.toggle()
            searchController.searchBar.text = ""
            searchController.resignFirstResponder()
        }
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        searchController.searchBar.searchTextField.attributedPlaceholder = NSAttributedString(string: "Title or details text ...",
                                                                                              attributes: [NSAttributedString.Key.foregroundColor: UIColor.secondaryLabel])
    }
    
    
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        
        collectionView.collectionViewLayout = UICollectionView.createFlexibleFlowLayout(in: view)
    }
        
    //MARK: - DataSource
    func createDataSource() {
        dataSource = UICollectionViewDiffableDataSource<Section, Project>(collectionView: collectionView) { (collectionView, indexPath, project) in
                return self.configure(NormalProjectCell.self, with: project, for: indexPath)
        }
    }
    
    func updateData(on projects: [Project]) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Project>()
        snapshot.appendSections([Section.normal])
        snapshot.appendItems(projects)

        //apply() is safe to call from a background queue!
        self.dataSource.apply(snapshot, animatingDifferences: true)
    }
    
    ///Configure any type of cell that conforms to selfConfiguringProjectCell!
    func configure<T: SelfConfiguringProjectCell>(_ cellType: T.Type, with project: Project, for indexPath: IndexPath) -> T {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else {
            fatalError("Unable to dequeue \(cellType)")
        }
        
        cell.configure(with: project)
        return cell
    }
    
    //MARK: - Actions
    @objc func addButtonTapped() {
        let project = Project()
        let viewController = ProjectDetailsViewController(withProject: project)
        viewController.delegate = self
        navigationController?.pushViewController(viewController, animated: true)
    }
    
    @objc private func tapAndHoldCell(recognizer: UILongPressGestureRecognizer) {
        if recognizer.state == .ended {
            guard let indexPath = collectionView.indexPathForItem(at: recognizer.location(in: self.collectionView)),
                let project = dataSource?.itemIdentifier(for: indexPath) else {
                    return
            }
            
            let viewController = ProjectDetailsViewController(withProject: project)
            viewController.delegate = self
            navigationController?.pushViewController(viewController, animated: true)
        }
    }
    
    @objc private func swipeFromRightOnCell(recognizer: UISwipeGestureRecognizer) {
        if recognizer.state == .ended {
            guard let indexPath = collectionView.indexPathForItem(at: recognizer.location(in: self.collectionView)),
                let cell = collectionView.cellForItem(at: indexPath),
                let project = dataSource?.itemIdentifier(for: indexPath) else {
                    return
            }
            
            let overlay = ProjectCellDeletionOverlay(frame: CGRect(x: cell.bounds.width, y: 0, width: 0, height: cell.bounds.height))
            cell.addSubview(overlay)
            
            UIView.animate(withDuration: 0.70, animations: {
                overlay.backgroundColor = UIColor.red.withAlphaComponent(0.60)
                overlay.frame = CGRect(x: cell.bounds.width / 2, y: 0, width: cell.bounds.width / 2, height: cell.bounds.height)
            }) { _ in
                self.presentProjectAlertOnMainThread(withTitle: "Delete this Project?",
                                                     andMessage: "Are you sure?\nThis cannot be undone!\nAll associated notes will also be deleted!",
                                                     andDismissButtonTitle: "Cancel",
                                                     andConfirmButtonTitle: "Delete!",
                                                     completion: { success in
                                                        if success {
                                                            UIView.animate(withDuration: 1.40, animations: {
                                                                overlay.frame = CGRect(x: 0, y: 0, width: cell.bounds.width, height: cell.bounds.height)
                                                                cell.alpha = 0
                                                            }) { _ in
                                                                self.delete(project)
                                                                overlay.removeFromSuperview()
                                                            }
                                                        } else {
                                                            UIView.animate(withDuration: 1.5, animations: {
                                                                overlay.frame = CGRect(x: cell.bounds.width, y: 0, width: 0, height: cell.bounds.height)
                                                                overlay.alpha = 0
                                                            }) { _ in
                                                                overlay.removeFromSuperview()
                                                            }
                                                        }
                })
            }
        }
    }
    
    ///Will show an overlay view with help text on the app
    @objc private func showHelpView() {
        let helpViewController = AppHelpViewController(with: HelpViewDisplayTextFor.projects)
        helpViewController.modalTransitionStyle = .flipHorizontal
        helpViewController.modalPresentationStyle = .fullScreen
        present(helpViewController, animated: true)
    }
    
    ///Will show a menu with several options
    @objc private func showMenu() {
        
    }

    //MARK: - UI & Layout
    private func configureViewController() {
        view.backgroundColor = .systemPurple
        title = "Projects"
        
        navigationController?.navigationBar.prefersLargeTitles = false
        
        let menu =  UIBarButtonItem(image: ProjectImages.BarButton.menu, style: .plain, target: self, action: #selector(showMenu))
        let add =  UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped))
        navigationItem.leftBarButtonItems = [menu, add]
        let questionMark = UIBarButtonItem(image: ProjectImages.BarButton.questionmark, style: .plain, target: self, action: #selector(showHelpView))
        navigationItem.rightBarButtonItem = questionMark
    }

    private func configureCollectionView() {
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UICollectionView.createFlexibleFlowLayout(in: view))
        collectionView.delegate = self
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.backgroundColor = .clear
        view.addSubview(collectionView)
        
        collectionView.register(NormalProjectCell.self, forCellWithReuseIdentifier: NormalProjectCell.reuseIdentifier)
        
        let tapAndHold = UILongPressGestureRecognizer(target: self, action: #selector(tapAndHoldCell))
        tapAndHold.minimumPressDuration = 0.3
        collectionView.addGestureRecognizer(tapAndHold)
        
        let swipeFromRight = UISwipeGestureRecognizer(target: self, action: #selector(swipeFromRightOnCell) )
        swipeFromRight.direction = UISwipeGestureRecognizer.Direction.left
        collectionView.addGestureRecognizer(swipeFromRight)
    }
    
    private func configureSearchController() {
        searchController.searchResultsUpdater = self
        searchController.obscuresBackgroundDuringPresentation = false
        navigationItem.searchController = searchController
        
        //CollectionView under searchbar fix ???
        searchController.extendedLayoutIncludesOpaqueBars = true
//        searchController.edgesForExtendedLayout = .top
    }
    
}

//MARK: - Ext CollectionView Delegate
extension ProjectsViewController: UICollectionViewDelegate  {
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        guard let project = dataSource?.itemIdentifier(for: indexPath) else { return }
        ProjectsController.activeProject = project
        
        let loadingView = showLoadingView(for: project)

        let viewController = SplitOrFlipContainerController()
        UIView.animate(withDuration: 1.5, animations: {
            loadingView.alpha = 1
        }) { (complete) in
            self.dismiss(animated: false) {
                self.present(viewController, animated: false)
            }
        }
    }
}

//MARK: - Ext Search Results & Bar
extension ProjectsViewController: UISearchResultsUpdating {
    func updateSearchResults(for searchController: UISearchController) {
        guard let filter = searchController.searchBar.text, filter.isNotEmpty else {
            isSearching = false
            updateData(on: projectsController.filteredProjects())
            return
        }

        isSearching = true
        updateData(on: projectsController.filteredProjects(with: filter.lowercased()))
    }
    
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        lastScrollPosition = scrollView.contentOffset.y
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if lastScrollPosition < scrollView.contentOffset.y {
            navigationItem.hidesSearchBarWhenScrolling = true
        } else if lastScrollPosition > scrollView.contentOffset.y {
            navigationItem.hidesSearchBarWhenScrolling = false
        }
    }
}

//MARK: - ProjectHandler
extension ProjectsViewController: ProjectHandler {
    internal func save(_ project: Project, withImage image: UIImage?) {
        //call save and update the snapshot
        projectsController.save(project, withImage: image)
        updateData(on: projectsController.filteredProjects())
        collectionView.reloadData()
    }
    
    internal func delete(_ project: Project) {
        //call delete and update the snapshot
        projectsController.delete(project)
        updateData(on: projectsController.filteredProjects())
    }
}

以及流式布局:

extension UICollectionView {
    ///Flow layout with minimum 2 items across, with padding and spacing
    static func createFlexibleFlowLayout(in view: UIView) -> UICollectionViewFlowLayout {
        let width = view.bounds.width
        let padding: CGFloat
        let minimumItemSpacing: CGFloat
        let availableWidth: CGFloat
        let itemWidth: CGFloat
        
        if view.traitCollection.verticalSizeClass == .compact {
            print("//iPhones landscape")
            padding = 12
            minimumItemSpacing = 12
            availableWidth = width - (padding * 2) - (minimumItemSpacing * 3)
            itemWidth = availableWidth / 4
        } else if view.traitCollection.horizontalSizeClass == .compact && view.traitCollection.verticalSizeClass == .regular {
            print("//iPhones portrait")
            padding = 12
            minimumItemSpacing = 12
            availableWidth = width - (padding * 2) - (minimumItemSpacing)
            itemWidth = availableWidth / 2
        } else {
            print("//iPads")
            padding = 24
            minimumItemSpacing = 24
            availableWidth = width - (padding * 2) - (minimumItemSpacing * 3)
            itemWidth = availableWidth / 4
        }
        
        let flowLayout = UICollectionViewFlowLayout()
        flowLayout.sectionInset = UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding)
        flowLayout.itemSize = CGSize(width: itemWidth, height: itemWidth + 40)
        
        flowLayout.sectionHeadersPinToVisibleBounds = true
        
        return flowLayout
    }
}

感谢 HWS 论坛上@nemecek_filip 的一些帮助,我解决了这个问题! 使用自定义渐变视图的不同渐变方法!

特此更改和工作代码:

collectionView 单元格:

//
//  NormalProjectCell.swift
//

import UIKit

class NormalProjectCell: UICollectionViewCell, SelfConfiguringProjectCell {
    //MARK: - Properties
    let titleLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .title3), andColor: .label)
    let lastEditedLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .caption1), andColor: .secondaryLabel)
    let imageView = ProjectImageView(frame: .zero)
    var stackView = UIStackView()
    var backgroundMaskedView = GradientView()
    
    //MARK: - Init
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.layer.cornerRadius = 35
        
        let seperator = Separator(frame: .zero)
        
        stackView = UIStackView(arrangedSubviews: [seperator, titleLabel, lastEditedLabel, imageView])
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .vertical
        stackView.distribution = .fillProportionally
        stackView.spacing = 5
        stackView.setCustomSpacing(10, after: lastEditedLabel)
        stackView.insertSubview(backgroundMaskedView, at: 0)
        contentView.addSubview(stackView)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
        
    //MARK: - Layout
    override func layoutSubviews() {
        super.layoutSubviews()
        
        NSLayoutConstraint.activate([
            titleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 20),
            
            stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            stackView.topAnchor.constraint(equalTo: contentView.topAnchor),
            stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
        ])
        
        backgroundMaskedView.translatesAutoresizingMaskIntoConstraints = false
        backgroundMaskedView.pinToEdges(of: stackView)
    }
    
    //MARK: - Configure
    func configure(with project: ProjectsController.Project) {
        titleLabel.text = project.title
        lastEditedLabel.text = project.lastEdited.customMediumToString
        
        imageView.image = Bundle.getProjectImage(project: project)
    }
}

以及渐变视图:

//
//  GradientView.swift
//

import UIKit

class GradientView: UIView {
    var topColor: UIColor = UIColor.tertiarySystemBackground
    var bottomColor: UIColor = UIColor.systemPurple

    override class var layerClass: AnyClass {
        return CAGradientLayer.self
    }

    override func layoutSubviews() {
        (layer as! CAGradientLayer).colors = [topColor.cgColor, bottomColor.cgColor]
        (layer as! CAGradientLayer).locations = [0.0, 0.40]
    }
}