Files
suixinkan_uikit/suixinkan/UI/Home/AllFunctionsViewController.swift
汉秋 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
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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