244 lines
8.2 KiB
Swift
244 lines
8.2 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 = 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, 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
|
||
) { [weak self] collectionView, indexPath, item in
|
||
let cell = collectionView.dequeueReusableCell(
|
||
withReuseIdentifier: AllFunctionMenuCell.reuseIdentifier,
|
||
for: indexPath
|
||
) as! AllFunctionMenuCell
|
||
cell.apply(menu: item.menu, actionStyle: item.actionStyle)
|
||
cell.onActionTap = {
|
||
switch item.section {
|
||
case .common:
|
||
self?.viewModel.removeFromCommon(item.menu)
|
||
case .more:
|
||
self?.viewModel.addToCommon(item.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 = 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
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 全部功能页分区标题。
|
||
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
|
||
}
|
||
}
|