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

时间:2020-11-02 12:41:46

标签: uicollectionview uikit uicollectionviewcell swift5 flowlayout

我有一个带有自定义流程图和一个自定义collectionview单元(没有情节提要)的collectionview。自定义单元在背景视图上具有CAGradientLayer。从暂停状态或特征收集更改返回时,此层的渲染不正确(请参见图像:enter image description here),它应该是单元的完整宽度。 另外,当滚动到下面的屏幕外项目时,渐变层根本不会渲染吗?

旋转设备一次,或滚动即可解决问题... 我不确定这是否可以在自定义单元格类或collectionview viewcontroller中解决。重用问题? 任何帮助,不胜感激!

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

单元格类

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
    }
}

1 个答案:

答案 0 :(得分:0)

感谢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:

//
//  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]
    }
}