Files
suixinkan_ios_uikit/suixinkan_ios/Features/Home/ViewControllers/HomeViewController.swift
汉秋 d99a5b1bf8 Advance UIKit rewrite with AMap integration and core UI modules.
Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 15:16:12 +08:00

783 lines
30 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
// MARK: - Diffable
/// section
private enum HomeSection: Hashable {
case workStatus
case locationReport
case storeInfo
case quickActions
case commonMenus
}
/// item item diff
private enum HomeItem: Hashable {
case workStatus(isOnline: Bool, secondsUntilReport: Int, reminderMinutes: Int)
case locationReport
case storeInfo(storeID: Int)
case quickActions(isOnline: Bool)
case menu(uri: String)
}
///
final class HomeViewController: UIViewController {
private let viewModel = HomeViewModel()
private let commonMenuStore = HomeCommonMenuStore()
private var commonURIs: [String] = []
private var isOnline = false
private var reminderMinutes = 0
private var secondsUntilReport = 0
private var countdownTimer: Timer?
private let minimalTopRoleIds: Set<Int> = [46, 47, 52, 53, 54]
private lazy var scenicButton: UIButton = {
var config = UIButton.Configuration.plain()
config.baseForegroundColor = UIColor(hex: 0x333333)
config.image = UIImage(systemName: "chevron.down")?.withConfiguration(
UIImage.SymbolConfiguration(pointSize: AppMetrics.FontSize.subheadline, weight: .bold)
)
config.imagePlacement = .trailing
config.imagePadding = AppMetrics.Spacing.xSmall
config.contentInsets = .zero
let button = UIButton(configuration: config)
button.contentHorizontalAlignment = .leading
button.addTarget(self, action: #selector(scenicTapped), for: .touchUpInside)
return button
}()
private lazy var collectionView: UICollectionView = {
let layout = makeCollectionLayout()
let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
collection.backgroundColor = AppDesignUIKit.pageBackground
collection.showsVerticalScrollIndicator = false
collection.delegate = self
collection.register(HomeWorkStatusCell.self, forCellWithReuseIdentifier: HomeWorkStatusCell.reuseID)
collection.register(HomeLocationReportCell.self, forCellWithReuseIdentifier: HomeLocationReportCell.reuseID)
collection.register(HomeStoreInfoCell.self, forCellWithReuseIdentifier: HomeStoreInfoCell.reuseID)
collection.register(HomeQuickActionsCell.self, forCellWithReuseIdentifier: HomeQuickActionsCell.reuseID)
collection.register(HomeMenuItemCell.self, forCellWithReuseIdentifier: HomeMenuItemCell.reuseID)
collection.register(
CollectionSectionHeaderView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: CollectionSectionHeaderView.reuseID
)
return collection
}()
/// Diffable section
private var dataSource: UICollectionViewDiffableDataSource<HomeSection, HomeItem>!
/// UI
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = AppDesignUIKit.pageBackground
setupTopBar()
configureDataSource()
setupCollectionView()
bindViewModel()
rebuildMenus()
observeContextChanges()
}
///
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: animated)
startCountdownTimerIfNeeded()
}
///
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
countdownTimer?.invalidate()
countdownTimer = nil
}
/// TopBar UI
private func setupTopBar() {
let topBar = UIView()
topBar.backgroundColor = .white
view.addSubview(topBar)
topBar.addSubview(scenicButton)
topBar.snp.makeConstraints { make in
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
make.height.equalTo(78)
}
scenicButton.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(AppMetrics.Spacing.pageHorizontal)
make.centerY.equalToSuperview()
}
updateScenicTitle()
}
/// CollectionView
private func setupCollectionView() {
view.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(78)
make.leading.trailing.bottom.equalToSuperview()
}
}
/// Compositional Layout section
private func makeCollectionLayout() -> UICollectionViewCompositionalLayout {
UICollectionViewCompositionalLayout { [weak self] sectionIndex, _ in
guard let self,
let section = self.dataSource?.snapshot().sectionIdentifiers[safe: sectionIndex]
else {
return CollectionDiffableLayout.fullWidthSection()
}
switch section {
case .workStatus:
return CollectionDiffableLayout.fullWidthSection(height: 100)
case .locationReport:
return CollectionDiffableLayout.fullWidthSection(height: 148)
case .storeInfo:
return CollectionDiffableLayout.fullWidthSection(height: 88)
case .quickActions:
return CollectionDiffableLayout.fullWidthSection(height: 118)
case .commonMenus:
return CollectionDiffableLayout.addHeader(
to: CollectionDiffableLayout.gridSection(itemHeight: 102),
height: 36
)
}
}
}
/// Diffable Cell
private func configureDataSource() {
dataSource = UICollectionViewDiffableDataSource<HomeSection, HomeItem>(
collectionView: collectionView
) { [weak self] collectionView, indexPath, item in
guard let self else { return UICollectionViewCell() }
switch item {
case .workStatus(let online, let seconds, let reminder):
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: HomeWorkStatusCell.reuseID,
for: indexPath
) as! HomeWorkStatusCell
cell.configure(
isOnline: online,
countdownText: self.countdownText(seconds: seconds),
reminderText: self.reminderText(minutes: reminder),
onOnlineTap: { [weak self] in self?.onlineTapped() },
onReminderTap: { [weak self] in self?.reminderTapped() }
)
return cell
case .locationReport:
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: HomeLocationReportCell.reuseID,
for: indexPath
) as! HomeLocationReportCell
cell.configure(onReportTap: { [weak self] in self?.locationReportTapped() })
return cell
case .storeInfo(let storeID):
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: HomeStoreInfoCell.reuseID,
for: indexPath
) as! HomeStoreInfoCell
if let store = self.appServices.accountContext.currentStore, store.id == storeID {
cell.configure(
storeName: store.name,
scenicName: self.appServices.accountContext.currentScenic?.name
)
}
return cell
case .quickActions(let online):
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: HomeQuickActionsCell.reuseID,
for: indexPath
) as! HomeQuickActionsCell
cell.configure(
isOnline: online,
onPaymentTap: { [weak self] in self?.paymentTapped() },
onTaskCreateTap: { [weak self] in self?.taskCreateTapped() },
onOnlineTap: { [weak self] in self?.onlineTapped() }
)
return cell
case .menu(let uri):
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: HomeMenuItemCell.reuseID,
for: indexPath
) as! HomeMenuItemCell
if let menuItem = self.menuItem(for: uri) ?? self.displayMenuItems.first(where: { $0.uri == uri }) {
cell.configure(item: menuItem)
}
return cell
}
}
dataSource.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in
guard let self,
kind == UICollectionView.elementKindSectionHeader,
let section = self.dataSource.snapshot().sectionIdentifiers[safe: indexPath.section],
section == .commonMenus,
let header = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: CollectionSectionHeaderView.reuseID,
for: indexPath
) as? CollectionSectionHeaderView
else { return nil }
header.configure(title: "常用应用")
return header
}
}
/// snapshot diff
private func applySnapshot(animated: Bool = true) {
var snapshot = NSDiffableDataSourceSnapshot<HomeSection, HomeItem>()
if shouldShowWorkStatus {
snapshot.appendSections([.workStatus, .locationReport])
snapshot.appendItems(
[.workStatus(isOnline: isOnline, secondsUntilReport: secondsUntilReport, reminderMinutes: reminderMinutes)],
toSection: .workStatus
)
snapshot.appendItems([.locationReport], toSection: .locationReport)
}
if isStoreManager, let store = appServices.accountContext.currentStore {
snapshot.appendSections([.storeInfo])
snapshot.appendItems([.storeInfo(storeID: store.id)], toSection: .storeInfo)
}
if shouldShowWorkStatus {
snapshot.appendSections([.quickActions])
snapshot.appendItems([.quickActions(isOnline: isOnline)], toSection: .quickActions)
}
snapshot.appendSections([.commonMenus])
snapshot.appendItems(
displayMenuItems.map { HomeItem.menu(uri: $0.uri) },
toSection: .commonMenus
)
dataSource.apply(snapshot, animatingDifferences: animated)
}
/// ViewModel
private func bindViewModel() {
viewModel.onChange = { [weak self] in
self?.applySnapshot()
}
}
///
private func observeContextChanges() {
let services = appServices
services.permissionContext.onChange = { [weak self] in
self?.rebuildMenus()
}
services.accountContext.onChange = { [weak self] in
self?.updateScenicTitle()
self?.applySnapshot()
}
}
///
private func rebuildMenus() {
let services = appServices
viewModel.buildMenus(
from: services.permissionContext.rolePermissions,
currentRoleId: services.permissionContext.currentRole?.id
)
commonURIs = commonMenuStore.load(menuItems: viewModel.menuItems)
applySnapshot()
}
///
private func updateScenicTitle() {
let name = appServices.accountContext.currentScenic?.name ?? "请选择景区"
scenicButton.configuration?.attributedTitle = AttributedString(
name,
attributes: AttributeContainer([
.font: UIFont.systemFont(ofSize: AppMetrics.FontSize.title3, weight: .semibold)
])
)
}
/// ID
private var currentRoleId: Int? {
appServices.permissionContext.currentRole?.id
}
///
private var shouldShowWorkStatus: Bool {
guard let currentRoleId else { return true }
return !minimalTopRoleIds.contains(currentRoleId)
}
/// roleId = 46
private var isStoreManager: Bool {
currentRoleId == 46
}
/// 3 3
private var displayMenuItems: [HomeMenuItem] {
let selected = commonURIs.compactMap { menuItem(for: $0) }
let fallback = selected.isEmpty ? Array(viewModel.menuItems.prefix(3)) : selected
return fallback + [HomeMenuItem(title: "更多功能", uri: "more_functions", iconSrc: nil)]
}
/// URI
private func menuItem(for uri: String) -> HomeMenuItem? {
let availableURIs = Set(viewModel.menuItems.map(\.uri))
let resolvedUri = HomeMenuRouter.canonicalURI(for: uri, availableURIs: availableURIs)
guard let existing = viewModel.menuItems.first(where: { $0.uri == resolvedUri }) else { return nil }
return HomeMenuItem(
title: HomeMenuRouter.displayTitle(for: resolvedUri, fallback: existing.title),
uri: resolvedUri,
iconSrc: existing.iconSrc
)
}
///
private func countdownText(seconds: Int) -> String {
let hours = seconds / 3_600
let minutes = (seconds % 3_600) / 60
let secs = seconds % 60
return "\(hours):\(String(format: "%02d", minutes)):\(String(format: "%02d", secs))"
}
///
private func reminderText(minutes: Int) -> String {
minutes == 0 ? "不提醒" : "提前\(minutes)分钟"
}
/// 线
private func startCountdownTimerIfNeeded() {
countdownTimer?.invalidate()
guard isOnline else { return }
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
guard let self, self.isOnline, self.secondsUntilReport > 0 else { return }
self.secondsUntilReport -= 1
self.applySnapshot(animated: false)
}
}
///
@objc private func scenicTapped() {
HomeMenuRouting.push(.scenicSelection, from: self)
}
/// 线 / 线
@objc private func onlineTapped() {
let message = isOnline
? "是否确认切换为离线状态?离线后将暂停位置上报。"
: "是否确认切换为在线状态?在线后将开始位置上报和计时。"
let alert = UIAlertController(title: "切换在线状态", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in
guard let self else { return }
self.isOnline.toggle()
if self.isOnline {
self.secondsUntilReport = 7_200
self.startCountdownTimerIfNeeded()
} else {
self.countdownTimer?.invalidate()
}
self.applySnapshot()
})
present(alert, animated: true)
}
///
@objc private func reminderTapped() {
let sheet = UIAlertController(title: "提前提醒时间", message: nil, preferredStyle: .actionSheet)
for minute in [0, 5, 10, 15, 30] {
let title = minute == 0 ? "不提醒" : "\(minute)分钟"
sheet.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
self?.reminderMinutes = minute
self?.applySnapshot()
})
}
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
present(sheet, animated: true)
}
///
@objc private func locationReportTapped() {
HomeMenuRouting.openRoute(HomeMenuRouter.resolve(uri: "location_report", title: "位置上报"), from: self)
}
///
@objc private func paymentTapped() {
HomeMenuRouting.openRoute(HomeMenuRouter.resolve(uri: "payment_collection", title: "立即收款"), from: self)
}
///
@objc private func taskCreateTapped() {
HomeMenuRouting.openRoute(HomeMenuRouter.resolve(uri: "task_create", title: "提交任务"), from: self)
}
}
// MARK: - UICollectionViewDelegate
extension HomeViewController: UICollectionViewDelegate {
///
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath),
case .menu(let uri) = item,
let menuItem = displayMenuItems.first(where: { $0.uri == uri })
else { return }
HomeMenuRouting.openMenu(menuItem, from: self)
}
}
// MARK: - Card Cells
/// 线 Cell线
private final class HomeWorkStatusCell: UICollectionViewCell {
static let reuseID = "HomeWorkStatusCell"
private let cardView = UIView()
private let onlineButton = UIButton(type: .system)
private let clockLabel = UILabel()
private let reminderButton = UIButton(type: .system)
///
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
cardView.backgroundColor = .white
cardView.layer.cornerRadius = 8
contentView.addSubview(cardView)
onlineButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.caption, weight: .medium)
onlineButton.layer.cornerRadius = 4
onlineButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12)
clockLabel.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .medium)
clockLabel.textColor = AppDesignUIKit.primary
reminderButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .medium)
reminderButton.setTitleColor(AppDesignUIKit.primary, for: .normal)
let stack = UIStackView(arrangedSubviews: [onlineButton, clockLabel, reminderButton])
stack.axis = .horizontal
stack.distribution = .equalSpacing
stack.alignment = .center
cardView.addSubview(stack)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
stack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(15)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// 线
func configure(
isOnline: Bool,
countdownText: String,
reminderText: String,
onOnlineTap: @escaping () -> Void,
onReminderTap: @escaping () -> Void
) {
onlineButton.setTitle(isOnline ? "在线" : "离线", for: .normal)
onlineButton.setTitleColor(isOnline ? UIColor(hex: 0xF0FDF4) : UIColor(hex: 0x7B8EAA), for: .normal)
onlineButton.backgroundColor = isOnline ? UIColor(hex: 0x22C55E) : UIColor(hex: 0xF4F4F4)
clockLabel.text = " \(countdownText)"
reminderButton.setTitle(" \(reminderText)", for: .normal)
onlineButton.removeAction(identifiedBy: UIAction.Identifier("online"), for: .touchUpInside)
reminderButton.removeAction(identifiedBy: UIAction.Identifier("reminder"), for: .touchUpInside)
onlineButton.addAction(UIAction(identifier: UIAction.Identifier("online")) { _ in onOnlineTap() }, for: .touchUpInside)
reminderButton.addAction(UIAction(identifier: UIAction.Identifier("reminder")) { _ in onReminderTap() }, for: .touchUpInside)
}
}
/// Cell
private final class HomeLocationReportCell: UICollectionViewCell {
static let reuseID = "HomeLocationReportCell"
private let cardView = UIView()
private let actionButton = UIButton(type: .system)
///
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
cardView.backgroundColor = .white
cardView.layer.cornerRadius = 8
contentView.addSubview(cardView)
let titleLabel = UILabel()
titleLabel.text = "立即上报"
titleLabel.font = .systemFont(ofSize: 22, weight: .bold)
titleLabel.textColor = UIColor(hex: 0x333333)
let subtitleLabel = UILabel()
subtitleLabel.text = "您已进入打卡范围"
subtitleLabel.font = .systemFont(ofSize: 15)
subtitleLabel.textColor = UIColor(hex: 0x999999)
let textStack = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
textStack.axis = .vertical
textStack.spacing = AppMetrics.Spacing.xxSmall
actionButton.setImage(UIImage(systemName: "hand.tap.fill"), for: .normal)
actionButton.tintColor = .white
actionButton.backgroundColor = AppDesignUIKit.primary
actionButton.layer.cornerRadius = 46
cardView.addSubview(textStack)
cardView.addSubview(actionButton)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
textStack.snp.makeConstraints { make in
make.leading.top.bottom.equalToSuperview().inset(15)
make.trailing.lessThanOrEqualTo(actionButton.snp.leading).offset(-12)
}
actionButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(15)
make.centerY.equalToSuperview()
make.width.height.equalTo(92)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func configure(onReportTap: @escaping () -> Void) {
actionButton.removeAction(identifiedBy: UIAction.Identifier("report"), for: .touchUpInside)
actionButton.addAction(UIAction(identifier: UIAction.Identifier("report")) { _ in onReportTap() }, for: .touchUpInside)
}
}
/// Cell
private final class HomeStoreInfoCell: UICollectionViewCell {
static let reuseID = "HomeStoreInfoCell"
private let cardView = UIView()
private let nameLabel = UILabel()
private let scenicLabel = UILabel()
///
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
cardView.backgroundColor = .white
cardView.layer.cornerRadius = 8
contentView.addSubview(cardView)
nameLabel.font = .systemFont(ofSize: AppMetrics.FontSize.title2, weight: .bold)
scenicLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
scenicLabel.textColor = UIColor(hex: 0x7B8EAA)
scenicLabel.numberOfLines = 2
let statusLabel = UILabel()
statusLabel.text = "营业中"
statusLabel.font = .systemFont(ofSize: AppMetrics.FontSize.caption)
statusLabel.textColor = UIColor(hex: 0x22C55E)
statusLabel.backgroundColor = UIColor(hex: 0xF0FDF4)
statusLabel.layer.cornerRadius = 4
statusLabel.clipsToBounds = true
statusLabel.textAlignment = .center
let textStack = UIStackView(arrangedSubviews: [nameLabel, scenicLabel])
textStack.axis = .vertical
textStack.spacing = AppMetrics.Spacing.xSmall
cardView.addSubview(textStack)
cardView.addSubview(statusLabel)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
textStack.snp.makeConstraints { make in
make.leading.top.bottom.equalToSuperview().inset(AppMetrics.Spacing.medium)
make.trailing.lessThanOrEqualTo(statusLabel.snp.leading).offset(-8)
}
statusLabel.snp.makeConstraints { make in
make.trailing.top.equalToSuperview().inset(AppMetrics.Spacing.medium)
make.width.greaterThanOrEqualTo(52)
make.height.equalTo(22)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func configure(storeName: String, scenicName: String?) {
nameLabel.text = storeName
scenicLabel.text = scenicName
}
}
/// Cell线
private final class HomeQuickActionsCell: UICollectionViewCell {
static let reuseID = "HomeQuickActionsCell"
private let stackView = UIStackView()
///
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
stackView.axis = .horizontal
stackView.spacing = AppMetrics.Spacing.small
stackView.distribution = .fillEqually
contentView.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func configure(
isOnline: Bool,
onPaymentTap: @escaping () -> Void,
onTaskCreateTap: @escaping () -> Void,
onOnlineTap: @escaping () -> Void
) {
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
stackView.addArrangedSubview(quickActionCard(
icon: "qrcode",
title: "立即收款",
active: false,
action: onPaymentTap
))
stackView.addArrangedSubview(quickActionCard(
icon: "checklist.checked",
title: "提交任务",
active: false,
action: onTaskCreateTap
))
stackView.addArrangedSubview(quickActionCard(
icon: isOnline ? "wifi" : "wifi.slash",
title: isOnline ? "在线" : "离线",
active: isOnline,
action: onOnlineTap
))
}
///
private func quickActionCard(
icon: String,
title: String,
active: Bool,
action: @escaping () -> Void
) -> UIView {
let card = UIView()
card.backgroundColor = active ? UIColor(hex: 0xE3F2FD) : .white
card.layer.cornerRadius = 8
let iconView = UIImageView(image: UIImage(systemName: icon))
iconView.tintColor = AppDesignUIKit.primary
iconView.contentMode = .scaleAspectFit
let label = UILabel()
label.text = title
label.font = .systemFont(ofSize: AppMetrics.FontSize.body)
label.textAlignment = .center
let innerStack = UIStackView(arrangedSubviews: [iconView, label])
innerStack.axis = .vertical
innerStack.spacing = AppMetrics.Spacing.xSmall
innerStack.alignment = .center
card.addSubview(innerStack)
innerStack.snp.makeConstraints { make in
make.center.equalToSuperview()
}
iconView.snp.makeConstraints { make in
make.height.equalTo(34)
}
let button = UIButton(type: .custom)
button.addAction(UIAction { _ in action() }, for: .touchUpInside)
card.addSubview(button)
button.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
card.snp.makeConstraints { make in
make.height.equalTo(102)
}
return card
}
}
/// Cell
private final class HomeMenuItemCell: UICollectionViewCell {
static let reuseID = "HomeMenuItemCell"
private let iconView = UIImageView()
private let titleLabel = UILabel()
///
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .white
contentView.layer.cornerRadius = 8
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.body)
titleLabel.textColor = UIColor(hex: 0x4B5563)
titleLabel.textAlignment = .center
titleLabel.numberOfLines = 1
titleLabel.adjustsFontSizeToFitWidth = true
let stack = UIStackView(arrangedSubviews: [iconView, titleLabel])
stack.axis = .vertical
stack.spacing = AppMetrics.Spacing.small
stack.alignment = .center
contentView.addSubview(stack)
stack.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.trailing.equalToSuperview().inset(4)
}
iconView.snp.makeConstraints { make in
make.width.height.equalTo(30)
}
iconView.contentMode = .scaleAspectFit
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func configure(item: HomeMenuItem) {
titleLabel.text = item.title
let symbol = UIImage(systemName: HomeIconCatalog.iconName(for: item.uri))
iconView.loadRemoteImage(urlString: item.iconSrc, contentMode: .scaleAspectFit, placeholder: symbol)
iconView.tintColor = AppDesign.primary
}
}
/// section
private extension Array {
subscript(safe index: Int) -> Element? {
indices.contains(index) ? self[index] : nil
}
}