Refactor home to single collection view and add all-functions customization.
Replace scroll/stack layout with one UICollectionView, fix grid column math and Diffable item IDs so location report and quick actions render, and add the customizable all-functions page with persistence and tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
228
suixinkan/UI/Home/AllFunctionsViewController.swift
Normal file
228
suixinkan/UI/Home/AllFunctionsViewController.swift
Normal file
@ -0,0 +1,228 @@
|
||||
//
|
||||
// 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 let viewModel = AllFunctionsViewModel()
|
||||
private let collectionView: UICollectionView
|
||||
|
||||
private var dataSource: UICollectionViewDiffableDataSource<AllFunctionSection, HomeMenuItem>!
|
||||
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 = false
|
||||
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.edges.equalTo(view.safeAreaLayoutGuide)
|
||||
}
|
||||
}
|
||||
|
||||
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, HomeMenuItem>()
|
||||
snapshot.appendSections([.common, .more])
|
||||
snapshot.appendItems(viewModel.commonMenus, toSection: .common)
|
||||
snapshot.appendItems(viewModel.moreMenus, toSection: .more)
|
||||
dataSource.apply(snapshot, animatingDifferences: animated)
|
||||
hasAppliedInitialSnapshot = true
|
||||
}
|
||||
|
||||
private func makeDataSource() -> UICollectionViewDiffableDataSource<AllFunctionSection, HomeMenuItem> {
|
||||
let dataSource = UICollectionViewDiffableDataSource<AllFunctionSection, HomeMenuItem>(
|
||||
collectionView: collectionView
|
||||
) { [weak self] collectionView, indexPath, menu in
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: AllFunctionMenuCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as! AllFunctionMenuCell
|
||||
guard let section = AllFunctionSection(rawValue: indexPath.section) else { return cell }
|
||||
cell.apply(menu: menu, actionStyle: section.actionStyle)
|
||||
cell.onActionTap = {
|
||||
switch section {
|
||||
case .common:
|
||||
self?.viewModel.removeFromCommon(menu)
|
||||
case .more:
|
||||
self?.viewModel.addToCommon(menu)
|
||||
}
|
||||
}
|
||||
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 = 12
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 全部功能页分区标题。
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user