Files
suixinkan_uikit/suixinkan/UI/Home/AllFunctionsViewController.swift
lujiuyinandCursor efb3925257 完善首页权限申请流程与队列播报体验。
对齐 Android 无权限强制弹窗、角色下拉与申请页交互,补充 UIButton configuration 适配,并增强景区排队 TTS 与相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 13:46:45 +08:00

253 lines
8.5 KiB
Swift

//
// 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<AllFunctionSection, AllFunctionListItem>!
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<AllFunctionSection, AllFunctionListItem>()
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<AllFunctionSection, AllFunctionListItem> {
let dataSource = UICollectionViewDiffableDataSource<AllFunctionSection, AllFunctionListItem>(
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
}
}