如何在收集视图中修复动画挂钩?

问题描述 投票:0回答:1

我有收集视图。当我触摸单元格时,收集单元类的动画

CarouselCell
播放。当我按单元格时,下载文件的过程以方法
didSelectItemAt
开始。为了更新过程,我从这里使用代码
reloadItem(indexPath: IndexPath)
问题是,如果下载文件时,我尝试触摸单元格,动画将播放一次,例如跳跃或挂钩,然后它根本无法使用触摸直到下载文件为止。另外,在下载过程中,再次单击单元格后,该方法
didSelectItemAt
在下载文件之前不会调用。在文件下载过程中未调用
didSelectItemAt
方法的事实很好。这样可以防止同一文件多次下载。但是问题在于,我不明白为什么会发生这种情况,因为我没有在任何地方写这样的代码。如何修复动画,为什么在文件下载过程中未调用
didSelectItemAt

代码:

i删除了所有相关的代码以节省相关的空间并用计时器替换为模拟文件下载进度

收集:

struct DownloadItem {
    var item: Int
    var state: String = ".none"
    var progress: Float = 0.0
    init(item: Int) { self.item = item }
}

class CarouselController: UIViewController, UICollectionViewDelegate {
        
    var progress = 0.0
    var collectionView: UICollectionView!
    var dataSource: UICollectionViewDiffableDataSource<Section, Item>?
    let sections = Bundle.main.decode([Section].self, from: "carouselData.json")
    var items = [DownloadItem]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupCollectionView()
        setupScrollView()
        setupDownloadItems()
    }
    
    func setupScrollView() {
        collectionView.scrollToItem(at: IndexPath(item: 0, section: 0), at: .centeredHorizontally, animated: false)
    }

    func setupDownloadItems() {
        let count = dataSource!.snapshot().numberOfItems
        for index in 0...count { items.append(DownloadItem(item: index)) }
    }
        
    func createDataSource() {
        dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
            switch self.sections[indexPath.section].identifier {
            case "carouselCell":
                let cell = self.configure(CarouselCell.self, with: item, for: indexPath)
                if indexPath.row < self.items.count {
                    let item = self.items[indexPath.row]
                    cell.title.text = "\(String(format: "%.f%%", item.progress * 100))"
                }
                return cell
            default: return self.configure(CarouselCell.self, with: item, for: indexPath)
            }
        }
    }
    
    func configure<T: SelfConfiguringCell>(_ cellType: T.Type, with item: Item, for indexPath: IndexPath) -> T {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else { fatalError("\(cellType)") }
        cell.configure(with: item)
        return cell
    }
    
    func reloadData() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections(sections)
        for section in sections { snapshot.appendItems(section.item, toSection: section) }
        dataSource?.apply(snapshot)
    }
        
    func setupCollectionView() {
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCompositionalLayout())
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.isScrollEnabled = false
        collectionView.delegate = self
        collectionView.contentInsetAdjustmentBehavior = .never
        view.addSubview(collectionView)
        collectionView.register(CarouselCell.self, forCellWithReuseIdentifier: CarouselCell.reuseIdentifier)
        createDataSource()
        reloadData()
    }
    
    func createCompositionalLayout() -> UICollectionViewLayout {
        UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
            
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                  heightDimension: .fractionalHeight(1))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
                        
            let groupWidth = (layoutEnvironment.container.contentSize.width * 1.05)/3
            let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(groupWidth),
                                                   heightDimension: .absolute(groupWidth))
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
                        
            let section = NSCollectionLayoutSection(group: group)
            section.contentInsets = NSDirectionalEdgeInsets(
                top: (layoutEnvironment.container.contentSize.height/2) - (groupWidth/2),
                leading: 0,
                bottom: 0,
                trailing: 0)
            section.interGroupSpacing = 64
            section.orthogonalScrollingBehavior = .groupPagingCentered
            section.contentInsetsReference = .none
            section.visibleItemsInvalidationHandler = { (items, offset, environment) in
                
                items.forEach { item in
                    let distanceFromCenter = abs((item.frame.midX - offset.x) - environment.container.contentSize.width / 2.0)
                    let minScale: CGFloat = 0.7
                    let maxScale: CGFloat = 1.1
                    let scale = max(maxScale - (distanceFromCenter / environment.container.contentSize.width), minScale)
                    item.transform = CGAffineTransform(scaleX: scale, y: scale)
                }
            }
            
            return section
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        
        print("-")
        
        _ = Timer.scheduledTimer(withTimeInterval: 0.10, repeats: true) { timer in
            guard self.progress <= 1.0 else {
                timer.invalidate()
                self.progress = 0.0
                return
            }
            self.progress += 0.01
            self.items[indexPath.row].progress = Float(self.progress)
            self.reloadItem(indexPath: .init(row: indexPath.row, section: 0))
        }
                
    }
    
    func reloadItem(indexPath: IndexPath) {
        guard let needReloadItem = dataSource!.itemIdentifier(for: indexPath) else { return }
        guard var snapshot = dataSource?.snapshot() else { return }
        snapshot.reloadItems([needReloadItem])
        dataSource?.apply(snapshot, animatingDifferences: false)
    }
    
}

CELL

class CarouselCell: UICollectionViewCell, SelfConfiguringCell {
        
    static let reuseIdentifier: String = "carouselCell"
    
    var topSpaceView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    var imageView: UIImageView = {
        let image = UIImageView()
        image.contentMode = .scaleAspectFill
        image.translatesAutoresizingMaskIntoConstraints = false
        return image
    }()
    
    var titleImageView: UIImageView = {
        let image = UIImageView()
        image.contentMode = .scaleAspectFill
        image.translatesAutoresizingMaskIntoConstraints = false
        return image
    }()
    
    var textView: UIView = {
        let view = UIView()
        view.backgroundColor = .blue
        view.layer.borderColor = UIColor.red.cgColor
        view.layer.borderWidth = 3
        view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    var title: UILabel = {
        let label = UILabel()
        label.textColor = .white
        label.textAlignment = .center
        label.sizeToFit()
        label.font = UIFont(name: "Marker Felt", size: 24)
        label.numberOfLines = 0
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    var progressView: UIProgressView = {
        let view = UIProgressView(progressViewStyle: .bar)
        view.center = view.center
        view.setProgress(0.0, animated: true)
        view.trackTintColor = .clear
        view.tintColor = .white.withAlphaComponent(0.15)
        view.clipsToBounds = true
        view.layer.borderColor = UIColor.red.cgColor
        view.layer.borderWidth = 3
        view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    var bottomSpaceView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    var stackView: UIView = {
        let stack = UIView()
        stack.layer.shadowColor = UIColor.black.cgColor
        stack.layer.shadowOffset = CGSize.zero
        stack.layer.shadowOpacity = 0.6
        stack.layer.shadowRadius = 3
        stack.translatesAutoresizingMaskIntoConstraints = false
        return stack
    }()
    
    override var isHighlighted: Bool {
        didSet { if oldValue == false && isHighlighted { highlight() }
            else if oldValue == true && !isHighlighted { unHighlight() }
        }
    }
    
    var imageTask: AnyCancellable?
    var titleImageTask: AnyCancellable?
    
    override func prepareForReuse() {
        super.prepareForReuse()
        imageView.image = nil
        titleImageView.image = nil
        imageTask?.cancel()
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
        setupConstraints()
    }
    
    required init?(coder: NSCoder) {
        fatalError("error")
    }
    
    func setupViews() {
        stackView.addSubview(topSpaceView)
        stackView.addSubview(imageView)
        stackView.addSubview(titleImageView)
        stackView.addSubview(textView)
        stackView.addSubview(bottomSpaceView)
        
        contentView.addSubview(stackView)
        
        textView.addSubview(progressView)
        textView.addSubview(title)
        
        stackView.sendSubview(toBack: textView)
        textView.bringSubview(toFront: title)
    }
        
    func setupConstraints() {

        stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
        stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
        stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0).isActive = true
        stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0).isActive = true

        topSpaceView.topAnchor.constraint(equalTo: stackView.topAnchor, constant: 0).isActive = true
        topSpaceView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.07).isActive = true
        topSpaceView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
        topSpaceView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true

        imageView.topAnchor.constraint(equalTo: topSpaceView.bottomAnchor, constant: 0).isActive = true
        imageView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.7).isActive = true
        imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0).isActive = true
        imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0).isActive = true

        titleImageView.topAnchor.constraint(equalTo: topSpaceView.bottomAnchor, constant: 0).isActive = true
        titleImageView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.7).isActive = true
        titleImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0).isActive = true
        titleImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0).isActive = true

        textView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.16).isActive = true
        textView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24).isActive = true
        textView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24).isActive = true
        textView.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: -6).isActive = true
        textView.layer.cornerRadius = contentView.frame.size.height*0.16/2

        title.leadingAnchor.constraint(equalTo: textView.leadingAnchor, constant: 0).isActive = true
        title.trailingAnchor.constraint(equalTo: textView.trailingAnchor, constant: 0).isActive = true
        title.topAnchor.constraint(equalTo: textView.topAnchor, constant: 0).isActive = true
        title.bottomAnchor.constraint(equalTo: textView.bottomAnchor, constant: 0).isActive = true

        progressView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.16).isActive = true
        progressView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 24).isActive = true
        progressView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -24).isActive = true
        progressView.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: -6).isActive = true
        progressView.layer.cornerRadius = contentView.frame.size.height*0.16/2

        bottomSpaceView.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: 0).isActive = true
        bottomSpaceView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.07).isActive = true
        bottomSpaceView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
        bottomSpaceView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
    }
        
    private func animateScale(to scale: CGFloat, duration: TimeInterval) {
        UIView.animate( withDuration: duration, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.5, options: [.beginFromCurrentState], animations: {
            self.stackView.transform = .init(scaleX: scale, y: scale)
            self.textView.transform = .init(scaleX: scale, y: scale)
        }, completion: nil)
    }
    func highlight() {
        animateScale(to: 0.9, duration: 0.4)
    }
    func unHighlight() {
        animateScale(to: 1, duration: 0.38)
    }
        
    func configure(with item: Item) {
        
        title.text = item.title
        textView.backgroundColor = .green
        textView.layer.borderColor = UIColor.green.cgColor
        progressView.layer.borderColor = UIColor.green.cgColor
        
        titleImageTask = Future<UIImage?, Never>() { promise in
            UIImage(named: item.titleImage)?.prepareForDisplay(completionHandler: { loadedImage in
                promise(Result.success(loadedImage))
            })
        }
        .receive(on: DispatchQueue.main)
        .sink { image in
            self.titleImageView.image = image
        }
        
        imageTask = Future<UIImage?, Never>() { promise in
            UIImage(named: item.image)?.prepareForDisplay(completionHandler: { loadedImage in
                promise(Result.success(loadedImage))
            })
        }
        .receive(on: DispatchQueue.main)
        .sink { image in
            self.imageView.image = image
        }
    }
}

也尝试使用

touchesBegan
/
touchesEnded
而不是iShighlighthighted,但结果是相同的:

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        highlight()
    }
        
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        unHighlight()
    }
        
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesCancelled(touches, with: event)
        unHighlight()
    }
Additional:

extension Bundle { func decode<T: Decodable>(_ type: T.Type, from file: String) -> T { guard let url = self.url(forResource: file, withExtension: nil) else { fatalError("Failed to locate \(file) in bundle.") } guard let data = try? Data(contentsOf: url) else { fatalError("Failed to load \(file) from bundle.") } let decoder = JSONDecoder() guard let loaded = try? decoder.decode(T.self, from: data) else { fatalError("Failed to decode \(file) from bundle.") } return loaded } } protocol SelfConfiguringCell { static var reuseIdentifier: String { get } func configure(with item: Item) } public struct Section: Decodable, Hashable { let index: Int let identifier: String let title: String let subtitle: String let item: [Item] } public struct Item: Decodable, Hashable { let index: Int let title: String let image: String let titleImage: String let backgroundColor: String let borderColor: String }
json

[ { "index": 1, "identifier": "carouselCell", "title": "", "subtitle": "", "item": [ { "index": 1, "title": "0", "image": "cover1", "titleImage": "title1", "backgroundColor": "5896CC", "borderColor": "3274AF", }, { "index": 2, "title": "0", "image": "cover2", "titleImage": "title2", "backgroundColor": "895138", "borderColor": "703A22", }, { "index": 3, "title": "0", "image": "cover3", "titleImage": "title3", "backgroundColor": "804136", "borderColor": "62322A", }, { "index": 4, "title": "0", "image": "cover4", "titleImage": "title4", "backgroundColor": "3E78A3", "borderColor": "2A597D", }, ] }, ]
    
ios swift uicollectionview uicollectionviewcompositionallayout uicollectionviewflowlayout
1个回答
0
投票
有几个问题:

  1. 细胞的重新加载虽然

    isHighlighted

    true
    isHighlighted
    可能会使单元的状态管理混淆。
    
    我可能建议您不要重新加载整个单元格,而是要使用单元格使用不同的机制来根据下载的状态进行自我更新。例如,也许添加一个the的属性,使该单元观察

    progress

    的属性。或使用通知中心随着进度的通知,并让细胞观察这些通知。

    这可能是次要的问题,但是我发现当所有单元子视图指定
    Item

    的所有单元格时,小区的选择更为可靠(当然,除非您需要这些子视图的用户互动,否则至少在这一点上不是这种情况)。解决先前的问题可能会消除这一问题,而只是一个选择。
  2. 一些无关的观察:
    我建议不要使用
    isUserInteractionEnabled

    确定正在更新的单元格。您要确保您的异步过程将不受收集视图中单元格的插入或删除的影响。 不必说,一旦将进度更新移至单元本身,您还将确保正确处理要重新使用的单元格以进行不同的下载(因为有问题的单元可能会滚动滚动并重复使用)。

你说:

    下载,当再次单击单元单元时,在下载文件之前,将不会调用该方法didSelectItemat。在文件下载过程中未调用
  1. false

    方法的事实很好。

    我建议您确实希望在选择单元格时被调用。现在,显然,您将需要跟踪下载是否已经开始,以免您启动重复下载。但是,不要依靠没有调用的当前行为,因为这是一个错误。
    

    注意,您的视图控制器具有
  2. IndexPath
  3. 状态变量。您可能想支持多个并发下载。此

    didSelectItemAt

    确实属于您的模型,而不是视图控制器级别。 (我怀疑您是为了MRE介绍的,但只是说……)

        

最新问题
© www.soinside.com 2019 - 2024. All rights reserved.