重新配置自定义内容配置

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

如果运行以下 UIKit 应用程序,您将看到一个具有组合布局(列表布局)和可比较数据源的集合视图。

当您点击右侧栏按钮项目时,付款方式会切换,此时我希望集合视图显示适当的付款按钮。

import UIKit
import PassKit

class ViewController: UIViewController {
    var collectionView: UICollectionView!
    
    var dataSource: UICollectionViewDiffableDataSource<String, String>!
    
    var paymentMethod: String = " Pay" {
        didSet {
            var snapshot = dataSource.snapshot()
            // re-apply all the cell registrations (which is just one in this case) to show the changed payment method
            snapshot.reconfigureItems(snapshot.itemIdentifiers)
            dataSource.apply(snapshot, animatingDifferences: true)
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureHierarchy()
        configureDataSource()
    }
    
    func configureHierarchy() {
        collectionView = .init(frame: .zero, collectionViewLayout: createLayout())
        view.addSubview(collectionView)
        collectionView.frame = view.bounds
        
        navigationItem.rightBarButtonItem = .init(title: "Toggle payment method", style: .plain, target: self, action: #selector(togglePaymentMethod))
    }
    
    func createLayout() -> UICollectionViewLayout {
        UICollectionViewCompositionalLayout { section, layoutEnvironment in
            let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
            return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment)
        }
    }
    
    @objc func togglePaymentMethod() {
        paymentMethod = paymentMethod == "Cash" ? " Pay" : "Cash"
    }
    
    func configureDataSource() {
        let payCashAtDeliveryCellRegistration = UICollectionView.CellRegistration<PayCashAtDeliveryCell, String> { [weak self] cell, indexPath, itemIdentifier in
            guard let self else { return }
            
            cell.button.addTarget(self, action: #selector(checkout), for: .touchUpInside)
        }
        let applePayButtonCellRegistration = UICollectionView.CellRegistration<PKPaymentButtonCell, String> { [weak self] cell, indexPath, itemIdentifier in
            guard let self else { return }
            
            cell.button.addTarget(self, action: #selector(checkout), for: .touchUpInside)
        }
        
        dataSource = .init(collectionView: collectionView) { [weak self] collectionView, indexPath, itemIdentifier in
            guard let self else { return nil }
            
            return switch paymentMethod {
            case " Pay":
                collectionView.dequeueConfiguredReusableCell(using: applePayButtonCellRegistration, for: indexPath, item: itemIdentifier)
            default:
                collectionView.dequeueConfiguredReusableCell(using: payCashAtDeliveryCellRegistration, for: indexPath, item: itemIdentifier)
            }
        }
        
        var snapshot = NSDiffableDataSourceSnapshot<String, String>()
        snapshot.appendSections(["main"])
        snapshot.appendItems(["demo"])
        dataSource.apply(snapshot, animatingDifferences: false)
    }
    
    @objc func checkout() {
        print(">> \(#function)")
    }
}

class PayCashAtDeliveryCell: UICollectionViewListCell {
    let button: UIButton = {
        var buttonConfiguration = UIButton.Configuration.plain()
        
        var attributeContainer = AttributeContainer()
        attributeContainer.font = .preferredFont(forTextStyle: .headline)
        attributeContainer.foregroundColor = .systemBlue
        
        buttonConfiguration.attributedTitle = .init("Ordina", attributes: attributeContainer)
        
        return UIButton(configuration: buttonConfiguration, primaryAction: nil)
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        contentView.addSubview(button)
        button.pinToSuperview()
        button.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).isActive = true
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class PKPaymentButtonCell: UICollectionViewListCell {
    let button = PKPaymentButton(paymentButtonType: .checkout, paymentButtonStyle: .automatic)
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        contentView.addSubview(button)
        button.pinToSuperview()
        button.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).isActive = true
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

extension UIView {
    func pin(
        to object: CanBePinnedTo,
        top: CGFloat = 0,
        bottom: CGFloat = 0,
        leading: CGFloat = 0,
        trailing: CGFloat = 0
    ) {
        self.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            self.topAnchor.constraint(equalTo: object.topAnchor, constant: top),
            self.bottomAnchor.constraint(equalTo: object.bottomAnchor, constant: bottom),
            self.leadingAnchor.constraint(equalTo: object.leadingAnchor, constant: leading),
            self.trailingAnchor.constraint(equalTo: object.trailingAnchor, constant: trailing),
        ])
    }
    
    func pinToSuperview(
        top: CGFloat = 0,
        bottom: CGFloat = 0,
        leading: CGFloat = 0,
        trailing: CGFloat = 0,
        file: StaticString = #file,
        line: UInt = #line
    ) {
        guard let superview = self.superview else {
            print(">> \(#function) failed in file: \(String.localFilePath(from: file)), at line: \(line): could not find \(Self.self).superView.")
            return
        }
        
        self.pin(to: superview, top: top, bottom: bottom, leading: leading, trailing: trailing)
    }
    
    func pinToSuperview(constant c: CGFloat = 0, file: StaticString = #file, line: UInt = #line) {
        self.pinToSuperview(top: c, bottom: -c, leading: c, trailing: -c, file: file, line: line)
    }
}

@MainActor
protocol CanBePinnedTo {
    var topAnchor: NSLayoutYAxisAnchor { get }
    var bottomAnchor: NSLayoutYAxisAnchor { get }
    var leadingAnchor: NSLayoutXAxisAnchor { get }
    var trailingAnchor: NSLayoutXAxisAnchor { get }
}

extension UIView: CanBePinnedTo { }
extension UILayoutGuide: CanBePinnedTo { }

extension String {
    static func localFilePath(from fullFilePath: StaticString = #file) -> Self {
        URL(fileURLWithPath: "\(fullFilePath)").lastPathComponent
    }
}

截至目前,当您点击右侧栏按钮项目时,应用程序会因错误而崩溃

Thread 1: "Attempted to dequeue a cell for a different registration or reuse identifier than the existing cell when reconfiguring an item, which is not allowed. You must dequeue a cell using the same registration or reuse identifier that was used to dequeue the cell originally to obtain the existing cell. Dequeued reuse identifier: 1FBF2362-FBFA-4B63-9E6C-F685BE6ECD5E; Original reuse identifier: 40FCBBD5-0D1B-4845-9D22-1C6288B5FE79; Existing cell: <ReconfigureCustomContentConfiguration.PKPaymentButtonCell: 0x105010430; baseClass = UICollectionViewListCell; frame = (20 35; 353 44); clipsToBounds = YES; layer = <CALayer: 0x600000262660>>"

可以通过在数据源的单元格提供者中捕获

paymentMethod
而不是 self 来避免崩溃:

dataSource = .init(collectionView: collectionView) { [paymentMethod] collectionView, indexPath, itemIdentifier in
    switch paymentMethod {
    case " Pay":
        collectionView.dequeueConfiguredReusableCell(using: applePayButtonCellRegistration, for: indexPath, item: itemIdentifier)
    default:
        collectionView.dequeueConfiguredReusableCell(using: payCashAtDeliveryCellRegistration, for: indexPath, item: itemIdentifier)
    }
}

此时,您可以将打印语句添加到单元格注册中,以查看当您更改付款方式时它们确实被调用,但集合视图仍然没有反映更改,无论如何,它更多的是一种解决方法,而不是正确的解决方案。

我本可以尝试只用一个类型为

UICollectionViewListCell
的单元格注册一个单元格(即没有子类化),但这在其他场景中给我带来了问题(动画应用快照时 UICollectionView 附件中的滞后,除非 .disclosureIndicator( ) 也是一个配件).

我也可以尝试只创建一个

UICollectionViewListCell
子类并重新配置它,但后来我想起,如果您使用
UIContentConfiguration
而不是单元格子类,您会得到很多好处(主要是您可以重复使用表视图单元格的配置,如好吧,并且单元格可以更好地自行调整大小(UICollectionViewListCell 不调整大小))。

这确实是我尝试过的:

// ...

func configureDataSource() {
    let checkoutButtonCell = UICollectionView.CellRegistration<UICollectionViewListCell, String> { [weak self] cell, indexPath, itemIdentifier in
        guard let self else { return }
        
        switch paymentMethod {
        case " Pay":
            var contentConfiguration = ApplePayButtonListContentConfiguration()
            contentConfiguration.target = (self, #selector(checkout), .touchUpInside)
            cell.contentConfiguration = contentConfiguration
        default:
            var contentConfiguration = PayCashAtDeliveryButtonListContentConfiguration()
            contentConfiguration.target = (self, #selector(checkout), .touchUpInside)
            cell.contentConfiguration = contentConfiguration
        }
    }
    
    dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
        collectionView.dequeueConfiguredReusableCell(using: checkoutButtonCell, for: indexPath, item: itemIdentifier)
    }
    
    var snapshot = NSDiffableDataSourceSnapshot<String, String>()
    snapshot.appendSections(["main"])
    snapshot.appendItems(["demo"])
    dataSource.apply(snapshot, animatingDifferences: false)
}

// ...

struct PayCashAtDeliveryButtonListContentConfiguration: UIContentConfiguration {
    var target: (Any?, Selector, UIControl.Event)?
    
    func makeContentView() -> UIView & UIContentView {
        PayCashAtDeliveryButtonListContentView(configuration: self)
    }
    
    func updated(for state: UIConfigurationState) -> Self {
        self
    }
}
class PayCashAtDeliveryButtonListContentView: UIView, UIContentView {
    var configuration: UIContentConfiguration
    
    init(configuration: UIContentConfiguration) {
        self.configuration = configuration
        super.init(frame: .zero)
        configureSubviews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configureSubviews() {
        guard configuration is PayCashAtDeliveryButtonListContentConfiguration else {
            print(">> Wrong config. File: \(String.localFilePath()), line: \(#line)")
            return
        }
        
        let button: UIButton = {
            var buttonConfiguration = UIButton.Configuration.plain()
            
            var attributeContainer = AttributeContainer()
            attributeContainer.font = .preferredFont(forTextStyle: .headline)
            attributeContainer.foregroundColor = .systemBlue
            
            buttonConfiguration.attributedTitle = .init("Ordina", attributes: attributeContainer)
            
            return UIButton(configuration: buttonConfiguration, primaryAction: nil)
        }()
        addSubview(button)
        button.pinToSuperview()
        button.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).isActive = true
    }
}

struct ApplePayButtonListContentConfiguration: UIContentConfiguration {
    var target: (Any?, Selector, UIControl.Event)?
    
    func makeContentView() -> UIView & UIContentView {
        ApplePayButtonListContentView(configuration: self)
    }
    
    func updated(for state: UIConfigurationState) -> Self {
        self
    }
}
class ApplePayButtonListContentView: UIView, UIContentView {
    var configuration: UIContentConfiguration
    
    init(configuration: UIContentConfiguration) {
        self.configuration = configuration
        super.init(frame: .zero)
        configureSubviews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configureSubviews() {
        guard configuration is ApplePayButtonListContentConfiguration else {
            print(">> Wrong config. File: \(String.localFilePath()), line: \(#line)")
            return
        }
        
        let button = PKPaymentButton(paymentButtonType: .checkout, paymentButtonStyle: .automatic)
        addSubview(button)
        button.pinToSuperview()
        button.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).isActive = true
    }
}

这有效,但也给了我警告

Warning: You are setting a new content configuration to a cell that has an existing content configuration, but the existing content view does not support the new configuration. This means the existing content view must be replaced with a new content view created from the new configuration, instead of updating the existing content view directly, which is expensive. Use separate registrations or reuse identifiers for different types of cells to avoid this. Make a symbolic breakpoint at UIContentConfigurationAlertForReplacedContentView to catch this in the debugger.

这让我觉得我需要只使用一种内容配置,我已经尝试过:

// ...

func configureDataSource() {
    let checkoutButtonCell = UICollectionView.CellRegistration<UICollectionViewListCell, String> { [weak self] cell, indexPath, itemIdentifier in
        guard let self else { return }
        
        var contentConfiguration = CheckoutButtonListContentConfiguration(
            paymentMethod: paymentMethod,
            buttonTarget: self,
            buttonAction: #selector(checkout)
        )
        cell.contentConfiguration = contentConfiguration
    }
    
    dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
        collectionView.dequeueConfiguredReusableCell(using: checkoutButtonCell, for: indexPath, item: itemIdentifier)
    }
    
    var snapshot = NSDiffableDataSourceSnapshot<String, String>()
    snapshot.appendSections(["main"])
    snapshot.appendItems(["demo"])
    dataSource.apply(snapshot, animatingDifferences: false)
}

// ...

struct CheckoutButtonListContentConfiguration: UIContentConfiguration {
    let paymentMethod: String
    let buttonTarget: Any
    let buttonAction: Selector
    
    init(paymentMethod: String, buttonTarget: Any, buttonAction: Selector) {
        self.paymentMethod = paymentMethod
        self.buttonTarget = buttonTarget
        self.buttonAction = buttonAction
    }
    
    func makeContentView() -> UIView & UIContentView {
        CheckoutButtonListContentView(configuration: self)
    }
    
    func updated(for state: UIConfigurationState) -> Self {
        self
    }
}
class CheckoutButtonListContentView: UIView, UIContentView {
    var configuration: UIContentConfiguration
    var button: UIButton!
    
    init(configuration: UIContentConfiguration) {
        self.configuration = configuration
        super.init(frame: .zero)
        configureSubviews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configureSubviews() {
        guard let config = configuration as? CheckoutButtonListContentConfiguration else {
            print(">> Wrong config. File: \(String.localFilePath()), line: \(#line)")
            return
        }
        
        subviews.forEach {
            $0.removeFromSuperview()
        }
        
        let button: UIButton = switch config.paymentMethod {
        case " Pay":
            PKPaymentButton(paymentButtonType: .checkout, paymentButtonStyle: .automatic)
        default:
            PayCashAtDeliveryButton()
        }
        button.addTarget(config.buttonTarget, action: config.buttonAction, for: .touchUpInside)
        addSubview(button)
        button.pinToSuperview()
        button.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).isActive = true
    }
}

class PayCashAtDeliveryButton: UIButton {
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        var configuration = UIButton.Configuration.plain()
        
        var attributeContainer = AttributeContainer()
        attributeContainer.font = .preferredFont(forTextStyle: .headline)
        attributeContainer.foregroundColor = .systemBlue
        
        configuration.attributedTitle = .init("Ordina", attributes: attributeContainer)
        
        self.configuration = configuration
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

但是集合视图不显示更新的付款方式,即使每次更改时都会调用单元格注册(您可以通过向其添加打印语句来验证)。

--

顺便说一句,我在闭包中捕获

weak self
,而不是
unowned self
,因为它对我来说似乎更安全,因为 UIKit 并不总是按我的预期工作,而且因为 Apple 在指导项目“现代集合视图”中更频繁地工作比不这样做也好。

uikit uicollectionviewcompositionallayout
1个回答
0
投票

改变

var configuration: UIContentConfiguration 

var configuration: UIContentConfiguration {
    didSet {
        configureSubviews()
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.