Files
suixinkan_uikit/suixinkan/UI/Home/HomeViewController.swift

511 lines
19 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.

//
// HomeViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// Tab
final class HomeViewController: BaseViewController {
private let viewModel = HomeViewModel()
private let homeAPI = NetworkServices.shared.homeAPI
private let scenicHeaderView = HomeScenicHeaderView()
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<HomeCollectionSection, HomeCollectionItem>!
private var currentSections: [HomeCollectionSection] = []
private var hasInitialized = false
private var countdownTimer: Timer?
private var activeDialog: HomeDialogKind?
override func setupNavigationBar() {
navigationController?.setNavigationBarHidden(true, animated: false)
}
override func setupUI() {
view.backgroundColor = AppColor.pageBackground
collectionView = UICollectionView(
frame: .zero,
collectionViewLayout: HomeCollectionLayoutBuilder.makeLayout(sections: [.commonApps])
)
collectionView.backgroundColor = .clear
collectionView.alwaysBounceVertical = true
collectionView.alwaysBounceHorizontal = false
collectionView.showsHorizontalScrollIndicator = false
collectionView.isDirectionalLockEnabled = true
// section.contentInsets contentInset
collectionView.contentInset = UIEdgeInsets(
top: AppSpacing.sm,
left: 0,
bottom: AppSpacing.md,
right: 0
)
registerCollectionCells()
view.addSubview(scenicHeaderView)
view.addSubview(collectionView)
dataSource = makeDataSource()
}
override func setupConstraints() {
scenicHeaderView.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide)
make.leading.trailing.equalToSuperview()
make.height.equalTo(AppSpacing.homeHeaderHeight)
}
collectionView.snp.makeConstraints { make in
make.top.equalTo(scenicHeaderView.snp.bottom)
make.leading.trailing.bottom.equalToSuperview()
}
}
override func bindActions() {
viewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyViewModel() }
}
viewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
scenicHeaderView.onTap = { [weak self] in self?.presentScenicSelection() }
collectionView.delegate = self
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAccountDidSwitch),
name: NotificationName.accountDidSwitch,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleScenicDidChange),
name: NotificationName.scenicDidChange,
object: nil
)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !hasInitialized {
hasInitialized = true
Task { await initializeHome() }
} else {
Task { await viewModel.reloadIfNeeded(api: homeAPI) }
}
startCountdownTimer()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
countdownTimer?.invalidate()
countdownTimer = nil
}
deinit {
NotificationCenter.default.removeObserver(self)
}
private func registerCollectionCells() {
collectionView.register(HomeWorkStatusCell.self, forCellWithReuseIdentifier: HomeWorkStatusCell.reuseIdentifier)
collectionView.register(
HomeLocationReportCell.self,
forCellWithReuseIdentifier: HomeLocationReportCell.reuseIdentifier
)
collectionView.register(
HomeQuickActionsCell.self,
forCellWithReuseIdentifier: HomeQuickActionsCell.reuseIdentifier
)
collectionView.register(HomeStoreCell.self, forCellWithReuseIdentifier: HomeStoreCell.reuseIdentifier)
collectionView.register(HomeMenuCell.self, forCellWithReuseIdentifier: HomeMenuCell.reuseIdentifier)
collectionView.register(
HomeSectionHeaderView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: HomeSectionHeaderView.reuseIdentifier
)
}
private func makeDataSource() -> UICollectionViewDiffableDataSource<HomeCollectionSection, HomeCollectionItem> {
let dataSource = UICollectionViewDiffableDataSource<HomeCollectionSection, HomeCollectionItem>(
collectionView: collectionView
) { [weak self] collectionView, indexPath, item in
guard let self else { return UICollectionViewCell() }
let section = self.currentSections[indexPath.section]
switch (section, item) {
case (.workStatus, .workStatus):
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: HomeWorkStatusCell.reuseIdentifier,
for: indexPath
) as! HomeWorkStatusCell
cell.cardView.onOnlineTap = { [weak self] in
self?.viewModel.showOnlineStatusSwitchDialog()
}
cell.cardView.onReminderTap = { [weak self] in
self?.presentReminderPicker()
}
cell.apply(
isOnline: self.viewModel.isOnline,
countdownText: self.viewModel.countdownDisplayText,
reminderMinutes: self.viewModel.reminderMinutes
)
return cell
case (.locationReport, .locationReport):
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: HomeLocationReportCell.reuseIdentifier,
for: indexPath
) as! HomeLocationReportCell
cell.cardView.onReportTap = { [weak self] in
guard let self else { return }
Task { await self.viewModel.manualReportLocation(api: self.homeAPI) }
}
cell.apply(
address: self.viewModel.locationInfoText,
isReporting: self.viewModel.isLocationReporting
)
return cell
case (.quickActions, .quickActions):
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: HomeQuickActionsCell.reuseIdentifier,
for: indexPath
) as! HomeQuickActionsCell
cell.cardView.onCollectPayment = { [weak self] in
guard AppStore.shared.currentScenicId > 0 else {
self?.showToast("请先选择景区")
return
}
self?.navigationController?.pushViewController(
PaymentCollectionDetailsViewController(),
animated: true
)
}
cell.cardView.onSubmitTask = { [weak self] in
self?.navigationController?.pushViewController(TaskAddViewController(), animated: true)
}
cell.cardView.onToggleOnline = { [weak self] in
self?.viewModel.showOnlineStatusSwitchDialog()
}
cell.apply(isOnline: self.viewModel.isOnline)
return cell
case (.store, .store):
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: HomeStoreCell.reuseIdentifier,
for: indexPath
) as! HomeStoreCell
if let store = self.viewModel.storeItem {
cell.apply(store: store)
}
return cell
case (.commonApps, .menu(let menu)):
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: HomeMenuCell.reuseIdentifier,
for: indexPath
) as! HomeMenuCell
cell.apply(menu: menu)
return cell
default:
return UICollectionViewCell()
}
}
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
guard kind == UICollectionView.elementKindSectionHeader,
indexPath.section < self.currentSections.count,
self.currentSections[indexPath.section] == .commonApps else {
return nil
}
let header = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: HomeSectionHeaderView.reuseIdentifier,
for: indexPath
) as! HomeSectionHeaderView
header.apply(title: "常用应用")
return header
}
return dataSource
}
private func initializeHome() async {
showLoading()
await viewModel.initialize(api: homeAPI)
hideLoading()
applyViewModel()
await evaluateDialogsWithDelay()
await viewModel.refreshLocationInfo()
applyViewModel()
}
private func evaluateDialogsWithDelay() async {
try? await Task.sleep(nanoseconds: 100_000_000)
await viewModel.evaluateDialogs(api: homeAPI)
applyViewModel()
presentDialogsIfNeeded()
}
@MainActor
private func applyViewModel() {
scenicHeaderView.apply(scenicName: viewModel.currentScenicName)
let showWorkBlock = !viewModel.isMinimalTopRole
let showStore = viewModel.currentAppRole == .storeAdmin && viewModel.storeItem != nil
let sections = HomeCollectionLayoutBuilder.sectionOrder(
showWorkBlock: showWorkBlock,
showStore: showStore,
storeItem: viewModel.storeItem
)
if sections != currentSections {
currentSections = sections
collectionView.setCollectionViewLayout(
HomeCollectionLayoutBuilder.makeLayout(sections: sections),
animated: false
)
}
let snapshot = HomeCollectionLayoutBuilder.makeSnapshot(
showWorkBlock: showWorkBlock,
showStore: showStore,
storeItem: viewModel.storeItem,
commonMenus: viewModel.commonMenus
)
dataSource.apply(snapshot, animatingDifferences: false)
presentDialogsIfNeeded()
}
private func refreshWorkStatusVisibleCell() {
guard !viewModel.isMinimalTopRole,
let sectionIndex = currentSections.firstIndex(of: .workStatus) else {
return
}
let indexPath = IndexPath(item: 0, section: sectionIndex)
guard let cell = collectionView.cellForItem(at: indexPath) as? HomeWorkStatusCell else {
return
}
cell.apply(
isOnline: viewModel.isOnline,
countdownText: viewModel.countdownDisplayText,
reminderMinutes: viewModel.reminderMinutes
)
}
private func presentDialogsIfNeeded() {
if viewModel.showPermissionDialog {
presentDialog(.permission)
return
}
if viewModel.showScenicDialog {
presentDialog(.scenic)
return
}
if viewModel.showOnlineStatusDialog {
presentDialog(.onlineStatus)
return
}
if viewModel.showLocationTimeoutDialog {
presentDialog(.locationTimeout)
return
}
if viewModel.showLocationReportSuccessDialog {
presentDialog(.locationSuccess)
return
}
activeDialog = nil
}
private enum HomeDialogKind {
case permission
case scenic
case onlineStatus
case locationTimeout
case locationSuccess
}
private func presentDialog(_ kind: HomeDialogKind) {
guard activeDialog != kind, presentedViewController == nil else { return }
activeDialog = kind
switch kind {
case .permission: presentPermissionDialog()
case .scenic: presentScenicDialog()
case .onlineStatus: presentOnlineStatusDialog()
case .locationTimeout: presentLocationTimeoutDialog()
case .locationSuccess: presentLocationSuccessDialog()
}
}
private func presentPermissionDialog() {
let alert = UIAlertController(
title: "权限提示",
message: "您还没有权限,请先申请权限",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "取消", style: .cancel) { [weak self] _ in
self?.viewModel.hidePermissionDialog()
self?.activeDialog = nil
})
alert.addAction(UIAlertAction(title: "去申请", style: .default) { [weak self] _ in
self?.showToast("功能开发中")
self?.activeDialog = nil
})
present(alert, animated: true)
}
private func presentScenicDialog() {
let alert = UIAlertController(
title: "请选择景区",
message: "使用首页功能前需要先选择当前景区",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "稍后", style: .cancel) { [weak self] _ in
self?.viewModel.hideScenicDialog()
self?.activeDialog = nil
})
alert.addAction(UIAlertAction(title: "去选择", style: .default) { [weak self] _ in
self?.viewModel.hideScenicDialog()
self?.activeDialog = nil
self?.presentScenicSelection()
})
present(alert, animated: true)
}
private func presentOnlineStatusDialog() {
let goingOnline = !viewModel.isOnline
let alert = UIAlertController(
title: goingOnline ? "切换在线" : "切换离线",
message: goingOnline ? "确认切换到在线状态并上报位置?" : "确认切换到离线状态?",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "取消", style: .cancel) { [weak self] _ in
self?.viewModel.hideOnlineStatusSwitchDialog()
self?.activeDialog = nil
})
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in
guard let self else { return }
self.activeDialog = nil
Task { await self.viewModel.switchOnlineStatus(api: self.homeAPI) }
})
present(alert, animated: true)
}
private func presentLocationTimeoutDialog() {
let alert = UIAlertController(
title: "位置上报提醒",
message: "您的位置上报即将超时,请立即上报",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "稍后", style: .cancel) { [weak self] _ in
self?.viewModel.hideLocationTimeoutDialog()
self?.activeDialog = nil
})
alert.addAction(UIAlertAction(title: "立即上报", style: .default) { [weak self] _ in
guard let self else { return }
self.activeDialog = nil
Task { await self.viewModel.confirmLocationTimeoutReport(api: self.homeAPI) }
})
present(alert, animated: true)
}
private func presentLocationSuccessDialog() {
let alert = UIAlertController(
title: "上报成功",
message: "上报时间:\(viewModel.reportTimeText)",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in
self?.viewModel.hideLocationReportSuccessDialog()
self?.activeDialog = nil
})
present(alert, animated: true)
}
private func presentReminderPicker() {
let alert = UIAlertController(title: "提前提醒", message: "选择位置超时提前提醒时间", preferredStyle: .actionSheet)
[0, 5, 10, 15, 30].forEach { minutes in
let title = minutes == 0 ? "不提醒" : "提前\(minutes)分钟"
alert.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
self?.viewModel.updateReminderMinutes(minutes)
})
}
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
present(alert, animated: true)
}
private func presentScenicSelection() {
let scenicList = AppStore.shared.roleScenicList()
guard !scenicList.isEmpty else {
showToast("暂无可选景区")
return
}
let controller = ScenicSelectionViewController(scenicList: scenicList)
controller.onSelected = { [weak self] in
guard let self else { return }
Task {
await self.viewModel.reloadIfNeeded(api: self.homeAPI)
await self.viewModel.loadStoreListIfNeeded(api: self.homeAPI)
self.applyViewModel()
await self.evaluateDialogsWithDelay()
}
}
let nav = UINavigationController(rootViewController: controller)
nav.modalPresentationStyle = .pageSheet
present(nav, animated: true)
}
private func startCountdownTimer() {
countdownTimer?.invalidate()
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
guard let self else { return }
self.viewModel.triggerLocationTimeoutIfNeeded()
self.refreshWorkStatusVisibleCell()
}
}
@objc private func handleAccountDidSwitch() {
viewModel.markNeedsPermissionReload()
Task {
await viewModel.reloadIfNeeded(api: homeAPI)
applyViewModel()
await evaluateDialogsWithDelay()
}
}
@objc private func handleScenicDidChange() {
viewModel.refreshLocalDisplayState()
Task {
await viewModel.loadStoreListIfNeeded(api: homeAPI)
applyViewModel()
}
}
}
extension HomeViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard indexPath.section < currentSections.count,
currentSections[indexPath.section] == .commonApps,
let item = dataSource.itemIdentifier(for: indexPath),
case let .menu(menu) = item else {
return
}
HomeRouteHandler.open(
uri: menu.uri,
from: self,
storeItem: viewModel.storeItem,
onCommonMenusChanged: { [weak self] in
guard let self else { return }
self.viewModel.rebuildCommonMenus()
self.applyViewModel()
}
)
}
}