对齐 Android 无权限强制弹窗、角色下拉与申请页交互,补充 UIButton configuration 适配,并增强景区排队 TTS 与相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
253 lines
8.5 KiB
Swift
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
|
|
}
|
|
}
|