// // AllFunctionsViewController.swift // suixinkan // import SnapKit import UIKit /// 全部功能页,管理常用应用与更多功能的增删。 final class AllFunctionsViewController: BaseViewController { var onDidCustomize: (() -> Void)? private enum AllFunctionSection: Int, CaseIterable, Hashable { case common case more var title: String { switch self { case .common: return "常用应用" case .more: return "更多功能" } } var actionStyle: AllFunctionMenuCell.ActionStyle { switch self { case .common: return .remove case .more: return .add } } } /// 全部功能列表中的展示项,包含分区信息以确保移动后重新配置操作图标。 private struct AllFunctionListItem: Hashable { let section: AllFunctionSection let menu: HomeMenuItem var actionStyle: AllFunctionMenuCell.ActionStyle { section.actionStyle } } private let viewModel = AllFunctionsViewModel() private let collectionView: UICollectionView private var dataSource: UICollectionViewDiffableDataSource! private var hasAppliedInitialSnapshot = false init() { collectionView = UICollectionView(frame: .zero, collectionViewLayout: Self.makeLayout()) super.init(nibName: nil, bundle: nil) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func setupNavigationBar() { title = "全部功能" } override func setupUI() { view.backgroundColor = AppColor.pageBackground collectionView.backgroundColor = .clear collectionView.alwaysBounceVertical = true collectionView.allowsSelection = true collectionView.delegate = self collectionView.contentInset = UIEdgeInsets( top: AppSpacing.xs, left: 0, bottom: AppSpacing.md, right: 0 ) collectionView.register( AllFunctionMenuCell.self, forCellWithReuseIdentifier: AllFunctionMenuCell.reuseIdentifier ) collectionView.register( AllFunctionSectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: AllFunctionSectionHeaderView.reuseIdentifier ) dataSource = makeDataSource() view.addSubview(collectionView) } override func setupConstraints() { collectionView.snp.makeConstraints { make in make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide) make.bottom.equalToSuperview() } } override func bindActions() { viewModel.onStateChange = { [weak self] in Task { @MainActor in self?.applyViewModel(animated: self?.hasAppliedInitialSnapshot == true) } } } override func viewDidLoad() { super.viewDidLoad() viewModel.loadFunctions() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) if isMovingFromParent, viewModel.didCustomize { onDidCustomize?() } } @MainActor private func applyViewModel(animated: Bool) { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.common, .more]) snapshot.appendItems( viewModel.commonMenus.map { AllFunctionListItem(section: .common, menu: $0) }, toSection: .common ) snapshot.appendItems( viewModel.moreMenus.map { AllFunctionListItem(section: .more, menu: $0) }, toSection: .more ) dataSource.apply(snapshot, animatingDifferences: animated) hasAppliedInitialSnapshot = true } private func makeDataSource() -> UICollectionViewDiffableDataSource { let dataSource = UICollectionViewDiffableDataSource( collectionView: collectionView ) { collectionView, indexPath, item in let cell = collectionView.dequeueReusableCell( withReuseIdentifier: AllFunctionMenuCell.reuseIdentifier, for: indexPath ) as! AllFunctionMenuCell cell.apply(menu: item.menu, actionStyle: item.actionStyle) return cell } dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in guard kind == UICollectionView.elementKindSectionHeader, let section = AllFunctionSection(rawValue: indexPath.section) else { return nil } let header = collectionView.dequeueReusableSupplementaryView( ofKind: kind, withReuseIdentifier: AllFunctionSectionHeaderView.reuseIdentifier, for: indexPath ) as! AllFunctionSectionHeaderView header.apply(title: section.title) return header } return dataSource } private static func makeLayout() -> UICollectionViewCompositionalLayout { UICollectionViewCompositionalLayout { sectionIndex, environment in let spacing: CGFloat = 15 let columns = 3 let horizontalInset = AppSpacing.md let availableWidth = environment.container.effectiveContentSize.width - horizontalInset * 2 let totalSpacing = spacing * CGFloat(columns - 1) let itemWidth = max(0, (availableWidth - totalSpacing) / CGFloat(columns)) let itemSize = NSCollectionLayoutSize( widthDimension: .absolute(itemWidth), heightDimension: .absolute(itemWidth) ) let item = NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), heightDimension: .absolute(itemWidth) ) let group = NSCollectionLayoutGroup.horizontal( layoutSize: groupSize, repeatingSubitem: item, count: columns ) group.interItemSpacing = .fixed(spacing) let section = NSCollectionLayoutSection(group: group) section.interGroupSpacing = spacing section.contentInsets = NSDirectionalEdgeInsets( top: 0, leading: horizontalInset, bottom: sectionIndex == AllFunctionSection.common.rawValue ? AppSpacing.lg : AppSpacing.md, trailing: horizontalInset ) let headerSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), heightDimension: .absolute(36) ) let header = NSCollectionLayoutBoundarySupplementaryItem( layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top ) section.boundarySupplementaryItems = [header] return section } } } extension AllFunctionsViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard let item = dataSource.itemIdentifier(for: indexPath) else { return } collectionView.deselectItem(at: indexPath, animated: true) switch item.section { case .common: viewModel.removeFromCommon(item.menu) case .more: viewModel.addToCommon(item.menu) } } } /// 全部功能页分区标题。 private final class AllFunctionSectionHeaderView: UICollectionReusableView { static let reuseIdentifier = "AllFunctionSectionHeaderView" private let titleLabel = UILabel() override init(frame: CGRect) { super.init(frame: frame) titleLabel.font = .app(.title) titleLabel.textColor = AppColor.textPrimary addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(AppSpacing.md) make.bottom.equalToSuperview().inset(AppSpacing.xs) } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func apply(title: String) { titleLabel.text = title } }