Files
suixinkan_uikit/suixinkan/UI/PunchPoint/PunchPointListViewController.swift
汉秋 5138c1c11a 完善实名认证、钱包与打卡点模块 UI 与业务逻辑。
对齐 Android 实名认证流程与审核页、钱包提现/积分兑换界面,优化打卡点列表与详情交互,并补充相关资源、主题 token 与单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 16:28:10 +08:00

679 lines
27 KiB
Swift
Raw Permalink 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.

//
// PunchPointListViewController.swift
// suixinkan
//
import Kingfisher
import SnapKit
import UIKit
/// Android `PunchPointListScreen`
final class PunchPointListViewController: BaseViewController {
private let viewModel: PunchPointListViewModel
private let api: any PunchPointAPIProtocol
private let filterContainer = UIView()
private let filterButton = UIButton(type: .system)
private let filterTitleLabel = UILabel()
private let filterChevronView = UIImageView(image: UIImage(named: "punch_point_dropdown"))
private let filterDropdownView = UIView()
private let filterDropdownStack = UIStackView()
private let tableView = UITableView(frame: .zero, style: .plain)
private let bottomBar = UIView()
private let addButton = UIButton(type: .system)
private let emptyView = PunchPointEmptyView()
private var dataSource: UITableViewDiffableDataSource<Int, PunchPointItem>!
private var filterButtons: [(PunchPointFilterType, UIButton)] = []
private var needsRefreshOnAppear = false
///
init(
viewModel: PunchPointListViewModel = PunchPointListViewModel(),
api: (any PunchPointAPIProtocol)? = nil
) {
self.viewModel = viewModel
self.api = api ?? NetworkServices.shared.punchPointAPI
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
title = "打卡点列表"
navigationController?.navigationBar.titleTextAttributes = [
.font: UIFont.systemFont(ofSize: 18, weight: .regular),
.foregroundColor: UIColor(hex: 0x333333),
]
}
override func setupUI() {
view.backgroundColor = UIColor(hex: 0xF4F4F4)
filterContainer.backgroundColor = .white
filterButton.backgroundColor = UIColor(hex: 0xF5F5F5)
filterButton.layer.cornerRadius = 8
filterButton.accessibilityLabel = "筛选打卡点状态"
filterTitleLabel.text = PunchPointFilterType.all.title
filterTitleLabel.font = .systemFont(ofSize: 14)
filterTitleLabel.textColor = AppColor.textPrimary
filterChevronView.contentMode = .scaleAspectFit
filterDropdownView.backgroundColor = .white
filterDropdownView.layer.cornerRadius = 8
filterDropdownView.layer.shadowColor = UIColor.black.cgColor
filterDropdownView.layer.shadowOpacity = 0.12
filterDropdownView.layer.shadowRadius = 10
filterDropdownView.layer.shadowOffset = CGSize(width: 0, height: 4)
filterDropdownView.isHidden = true
filterDropdownStack.axis = .vertical
tableView.backgroundColor = UIColor(hex: 0xF4F4F4)
tableView.separatorStyle = .none
tableView.delegate = self
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 210
tableView.contentInset = UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0)
tableView.refreshControl = UIRefreshControl()
tableView.register(PunchPointListCell.self, forCellReuseIdentifier: PunchPointListCell.reuseIdentifier)
dataSource = UITableViewDiffableDataSource<Int, PunchPointItem>(tableView: tableView) { [weak self] tableView, indexPath, item in
let cell = tableView.dequeueReusableCell(
withIdentifier: PunchPointListCell.reuseIdentifier,
for: indexPath
) as! PunchPointListCell
cell.apply(item: item)
cell.onQRCode = { [weak self] in self?.viewModel.showQRCode(for: item) }
cell.onDelete = { [weak self] in self?.confirmDelete(item) }
return cell
}
bottomBar.backgroundColor = .white
addButton.setTitle("添加", for: .normal)
addButton.setTitleColor(.white, for: .normal)
addButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
addButton.backgroundColor = AppColor.primary
addButton.layer.cornerRadius = 8
addButton.accessibilityLabel = "添加打卡点"
emptyView.isHidden = true
emptyView.onAction = { [weak self] in self?.addTapped() }
view.addSubview(filterContainer)
filterContainer.addSubview(filterButton)
filterButton.addSubview(filterTitleLabel)
filterButton.addSubview(filterChevronView)
view.addSubview(tableView)
view.addSubview(emptyView)
view.addSubview(bottomBar)
bottomBar.addSubview(addButton)
view.addSubview(filterDropdownView)
filterDropdownView.addSubview(filterDropdownStack)
configureFilterDropdown()
}
override func setupConstraints() {
filterContainer.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide)
make.leading.trailing.equalToSuperview()
make.height.equalTo(64)
}
filterButton.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(15)
make.centerY.equalToSuperview()
make.height.equalTo(48)
}
filterTitleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(12)
make.centerY.equalToSuperview()
}
filterChevronView.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(12)
make.centerY.equalToSuperview()
make.size.equalTo(22)
}
bottomBar.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
}
addButton.snp.makeConstraints { make in
make.top.equalToSuperview().offset(16)
make.leading.trailing.equalToSuperview().inset(15)
make.height.equalTo(48)
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-16)
}
tableView.snp.makeConstraints { make in
make.top.equalTo(filterContainer.snp.bottom)
make.leading.trailing.equalToSuperview()
make.bottom.equalTo(bottomBar.snp.top)
}
emptyView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalTo(tableView)
make.leading.trailing.equalToSuperview().inset(24)
}
filterDropdownView.snp.makeConstraints { make in
make.top.equalTo(filterButton.snp.bottom)
make.leading.trailing.equalTo(filterButton)
}
filterDropdownStack.snp.makeConstraints { make in
make.edges.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) }
}
viewModel.onShowQRCode = { [weak self] url in
Task { @MainActor in self?.showQRCodeDialog(url: url) }
}
viewModel.onBeginDeleteAnimation = { [weak self] id in
Task { @MainActor in self?.animateRemoval(id: id) }
}
tableView.refreshControl?.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
filterButton.addTarget(self, action: #selector(filterTapped), for: .touchUpInside)
addButton.addTarget(self, action: #selector(addTapped), for: .touchUpInside)
}
override func viewDidLoad() {
super.viewDidLoad()
Task { await viewModel.loadInitial(api: api) }
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if needsRefreshOnAppear {
needsRefreshOnAppear = false
Task { await viewModel.refresh(api: api) }
}
}
@MainActor
private func applyViewModel() {
filterTitleLabel.text = viewModel.filterType.title
filterButtons.forEach { type, button in
let selected = type == viewModel.filterType
button.setTitleColor(selected ? AppColor.primary : AppColor.textPrimary, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14, weight: selected ? .medium : .regular)
}
var snapshot = NSDiffableDataSourceSnapshot<Int, PunchPointItem>()
snapshot.appendSections([0])
snapshot.appendItems(viewModel.items)
dataSource.apply(snapshot, animatingDifferences: true)
tableView.refreshControl?.endRefreshing()
emptyView.isHidden = !viewModel.items.isEmpty || viewModel.isLoading || viewModel.isRefreshing
viewModel.isLoading && viewModel.items.isEmpty ? showLoading() : hideLoading()
}
private func configureFilterDropdown() {
filterDropdownStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
filterButtons = PunchPointFilterType.allCases.map { type in
let button = UIButton(type: .system)
button.contentHorizontalAlignment = .left
button.setTitle(type.title, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14)
var configuration = UIButton.Configuration.plain()
configuration.contentInsets = NSDirectionalEdgeInsets(top: 13, leading: 12, bottom: 13, trailing: 12)
button.configuration = configuration
button.addAction(UIAction { [weak self] _ in
guard let self else { return }
self.filterDropdownView.isHidden = true
Task { await self.viewModel.selectFilter(type, api: self.api) }
}, for: .touchUpInside)
filterDropdownStack.addArrangedSubview(button)
return (type, button)
}
}
private func confirmDelete(_ item: PunchPointItem) {
let alert = PunchPointMaterialDialogController.confirmation(
title: "删除打卡点",
message: "确定删除\(item.name)打卡点吗?",
confirmTitle: "确定",
onConfirm: { [weak self] in
self?.dismiss(animated: true)
guard let self else { return }
Task { await self.viewModel.delete(item: item, api: self.api) }
}
) { [weak self] in
self?.dismiss(animated: true)
}
present(alert, animated: true)
}
@MainActor
private func animateRemoval(id: Int64) {
guard let index = viewModel.items.firstIndex(where: { $0.id == id }) else {
viewModel.completeDeleteAnimation(id: id)
return
}
let indexPath = IndexPath(row: index, section: 0)
guard let cell = tableView.cellForRow(at: indexPath) else {
viewModel.completeDeleteAnimation(id: id)
return
}
UIView.animate(
withDuration: 0.3,
delay: 0,
options: [.curveEaseIn, .allowUserInteraction]
) {
cell.contentView.transform = CGAffineTransform(translationX: self.tableView.bounds.width, y: 0)
cell.contentView.alpha = 0
} completion: { [weak self] _ in
guard let self else { return }
self.viewModel.completeDeleteAnimation(id: id)
}
}
private func showQRCodeDialog(url: String) {
let controller = PunchPointMaterialDialogController.qrCode(url: url) { [weak self] in
self?.dismiss(animated: true)
}
present(controller, animated: true)
}
@objc private func filterTapped() {
filterDropdownView.isHidden.toggle()
}
@objc private func refreshPulled() {
Task { await viewModel.refresh(api: api) }
}
@objc private func addTapped() {
let controller = PunchPointFormViewController(mode: .create)
controller.onSubmitSuccess = { [weak self] in self?.needsRefreshOnAppear = true }
navigationController?.pushViewController(controller, animated: true)
}
}
extension PunchPointListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
let controller = PunchPointDetailViewController(punchPointId: item.id)
controller.onChanged = { [weak self] in self?.needsRefreshOnAppear = true }
navigationController?.pushViewController(controller, animated: true)
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
Task { await viewModel.loadMoreIfNeeded(lastVisibleIndex: indexPath.row, api: api) }
}
}
///
/// Android
private final class PunchPointListCell: UITableViewCell {
static let reuseIdentifier = "PunchPointListCell"
private let cardView = UIView()
private let coverImageView = UIImageView()
private let nameLabel = UILabel()
private let operatingTag = PunchPointStatusChip()
private let creatorLabel = UILabel()
private let createdAtLabel = UILabel()
private let locationIcon = UIImageView(image: UIImage(named: "punch_point_location"))
private let addressLabel = UILabel()
private let reviewTag = PunchPointStatusChip()
private let auditTimeLabel = UILabel()
private let qrButton = UIButton(type: .system)
private let deleteButton = UIButton(type: .system)
var onQRCode: (() -> Void)?
var onDelete: (() -> Void)?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .clear
contentView.backgroundColor = .clear
selectionStyle = .none
cardView.backgroundColor = .white
cardView.layer.cornerRadius = 16
cardView.clipsToBounds = true
coverImageView.contentMode = .scaleAspectFill
coverImageView.clipsToBounds = true
coverImageView.layer.cornerRadius = 12
coverImageView.backgroundColor = UIColor(hex: 0xF5F5F5)
nameLabel.font = .systemFont(ofSize: 16, weight: .semibold)
nameLabel.textColor = .black
nameLabel.lineBreakMode = .byTruncatingTail
[creatorLabel, createdAtLabel, auditTimeLabel].forEach {
$0.font = .systemFont(ofSize: 12)
$0.textColor = AppColor.textSecondary
}
locationIcon.contentMode = .scaleAspectFit
addressLabel.font = .systemFont(ofSize: 12)
addressLabel.textColor = UIColor(hex: 0x4B5563)
addressLabel.numberOfLines = 2
qrButton.setImage(UIImage(named: "punch_point_qr"), for: .normal)
deleteButton.setImage(UIImage(named: "punch_point_delete"), for: .normal)
qrButton.accessibilityLabel = "查看二维码"
deleteButton.accessibilityLabel = "删除打卡点"
contentView.addSubview(cardView)
[coverImageView, nameLabel, operatingTag, creatorLabel, createdAtLabel, locationIcon, addressLabel, reviewTag, auditTimeLabel, qrButton, deleteButton].forEach(cardView.addSubview)
cardView.snp.makeConstraints { make in
make.top.bottom.equalToSuperview().inset(6)
make.leading.trailing.equalToSuperview().inset(15)
}
coverImageView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(14)
make.leading.equalToSuperview().offset(16)
make.size.equalTo(128)
}
nameLabel.snp.makeConstraints { make in
make.top.equalTo(coverImageView)
make.leading.equalTo(coverImageView.snp.trailing).offset(12)
make.trailing.lessThanOrEqualTo(operatingTag.snp.leading).offset(-8)
}
operatingTag.snp.makeConstraints { make in
make.centerY.equalTo(nameLabel)
make.trailing.equalToSuperview().inset(16)
}
creatorLabel.snp.makeConstraints { make in
make.top.equalTo(nameLabel.snp.bottom).offset(8)
make.leading.equalTo(nameLabel)
make.trailing.equalToSuperview().inset(16)
}
createdAtLabel.snp.makeConstraints { make in
make.top.equalTo(creatorLabel.snp.bottom).offset(6)
make.leading.trailing.equalTo(creatorLabel)
}
locationIcon.snp.makeConstraints { make in
make.top.equalTo(createdAtLabel.snp.bottom).offset(8)
make.leading.equalTo(nameLabel)
make.size.equalTo(14)
}
addressLabel.snp.makeConstraints { make in
make.top.equalTo(locationIcon).offset(-1)
make.leading.equalTo(locationIcon.snp.trailing).offset(4)
make.trailing.equalToSuperview().inset(16)
}
reviewTag.snp.makeConstraints { make in
make.top.equalTo(coverImageView.snp.bottom).offset(14)
make.leading.equalToSuperview().offset(16)
make.bottom.equalToSuperview().inset(16)
}
auditTimeLabel.snp.makeConstraints { make in
make.centerY.equalTo(reviewTag)
make.leading.equalTo(reviewTag.snp.trailing).offset(8)
}
deleteButton.snp.makeConstraints { make in
make.centerY.equalTo(reviewTag)
make.trailing.equalToSuperview().inset(16)
make.size.equalTo(32)
}
qrButton.snp.makeConstraints { make in
make.centerY.equalTo(reviewTag)
make.trailing.equalTo(deleteButton.snp.leading).offset(-4)
make.size.equalTo(32)
}
qrButton.addTarget(self, action: #selector(qrTapped), for: .touchUpInside)
deleteButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(item: PunchPointItem) {
contentView.transform = .identity
contentView.alpha = 1
nameLabel.text = item.name
creatorLabel.text = "负责人:\(item.creator?.nonEmptyTrimmed ?? "--")"
createdAtLabel.text = "创建时间:\(item.createdAt)"
addressLabel.text = item.displayAddress.nonEmptyTrimmed ?? "--"
auditTimeLabel.text = "审核时间:\(item.auditTime?.nonEmptyTrimmed ?? "--")"
operatingTag.apply(PunchPointDisplayFormatter.operatingStatus(item.status), compact: true)
reviewTag.apply(PunchPointDisplayFormatter.auditStatus(item.status), compact: false)
if let url = item.guideImages.first.flatMap(URL.init(string:)) {
coverImageView.kf.setImage(with: url, placeholder: UIImage(named: "SplashLogo"))
} else {
coverImageView.image = UIImage(named: "SplashLogo")
}
}
@objc private func qrTapped() {
onQRCode?()
}
@objc private func deleteTapped() {
onDelete?()
}
}
///
///
final class PunchPointStatusChip: UIView {
private let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
layer.cornerRadius = 4
clipsToBounds = true
label.font = .systemFont(ofSize: 12)
label.textAlignment = .center
addSubview(label)
label.snp.makeConstraints { make in make.edges.equalToSuperview() }
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func apply(_ presentation: PunchPointStatusPresentation, compact: Bool = false) {
label.text = presentation.title
backgroundColor = UIColor(hex: presentation.backgroundColor)
label.textColor = UIColor(hex: presentation.textColor)
label.snp.remakeConstraints { make in
make.edges.equalToSuperview().inset(
UIEdgeInsets(top: compact ? 2 : 4, left: compact ? 8 : 10, bottom: compact ? 2 : 4, right: compact ? 8 : 10)
)
}
}
}
///
///
private final class PunchPointEmptyView: UIView {
private let titleLabel = UILabel()
private let button = UIButton(type: .system)
var onAction: (() -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
titleLabel.text = "暂无打卡点,点击下方按钮即可添加"
titleLabel.font = .systemFont(ofSize: 14)
titleLabel.textColor = AppColor.textSecondary
titleLabel.textAlignment = .center
button.setTitle("立即添加", for: .normal)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
button.backgroundColor = AppColor.primary
button.layer.cornerRadius = 8
addSubview(titleLabel)
addSubview(button)
titleLabel.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
}
button.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(12)
make.centerX.bottom.equalToSuperview()
make.width.greaterThanOrEqualTo(96)
make.height.equalTo(40)
}
button.addTarget(self, action: #selector(actionTapped), for: .touchUpInside)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func actionTapped() {
onAction?()
}
}
/// Android Material3
private final class PunchPointMaterialDialogController: UIViewController {
private enum Content {
case confirmation(title: String, message: String, confirmTitle: String)
case qrCode(url: String)
}
private let content: Content
private let onConfirm: () -> Void
private let onCancel: (() -> Void)?
private init(content: Content, onConfirm: @escaping () -> Void, onCancel: (() -> Void)? = nil) {
self.content = content
self.onConfirm = onConfirm
self.onCancel = onCancel
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .overFullScreen
modalTransitionStyle = .crossDissolve
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
static func confirmation(
title: String,
message: String,
confirmTitle: String,
onConfirm: @escaping () -> Void,
onCancel: @escaping () -> Void
) -> PunchPointMaterialDialogController {
PunchPointMaterialDialogController(
content: .confirmation(title: title, message: message, confirmTitle: confirmTitle),
onConfirm: onConfirm,
onCancel: onCancel
)
}
static func qrCode(url: String, onConfirm: @escaping () -> Void) -> PunchPointMaterialDialogController {
PunchPointMaterialDialogController(content: .qrCode(url: url), onConfirm: onConfirm)
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black.withAlphaComponent(0.32)
view.accessibilityViewIsModal = true
let dialog = UIView()
dialog.backgroundColor = .white
dialog.layer.cornerRadius = 28
dialog.clipsToBounds = true
view.addSubview(dialog)
dialog.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.greaterThanOrEqualToSuperview().offset(32)
make.trailing.lessThanOrEqualToSuperview().offset(-32)
make.width.lessThanOrEqualTo(320)
}
switch content {
case let .confirmation(title, message, confirmTitle):
configureConfirmation(dialog: dialog, title: title, message: message, confirmTitle: confirmTitle)
case let .qrCode(url):
configureQRCode(dialog: dialog, url: url)
}
}
private func configureConfirmation(dialog: UIView, title: String, message: String, confirmTitle: String) {
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
titleLabel.textColor = UIColor(hex: 0x333333)
let messageLabel = UILabel()
messageLabel.text = message
messageLabel.font = .systemFont(ofSize: 14)
messageLabel.textColor = UIColor(hex: 0x666666)
messageLabel.numberOfLines = 0
let cancel = textButton(title: "取消", color: UIColor(hex: 0x666666), action: #selector(cancelTapped))
let confirm = textButton(title: confirmTitle, color: AppColor.primary, action: #selector(confirmTapped))
let actions = UIStackView(arrangedSubviews: [UIView(), cancel, confirm])
actions.axis = .horizontal
actions.spacing = 8
[titleLabel, messageLabel, actions].forEach(dialog.addSubview)
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(24)
make.leading.trailing.equalToSuperview().inset(24)
}
messageLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(16)
make.leading.trailing.equalToSuperview().inset(24)
}
actions.snp.makeConstraints { make in
make.top.equalTo(messageLabel.snp.bottom).offset(16)
make.leading.trailing.equalToSuperview().inset(16)
make.bottom.equalToSuperview().inset(12)
make.height.equalTo(44)
}
}
private func configureQRCode(dialog: UIView, url: String) {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 12
if let imageURL = URL(string: url) {
imageView.kf.setImage(with: imageURL)
}
let confirm = textButton(title: "确定", color: AppColor.primary, action: #selector(confirmTapped))
dialog.addSubview(imageView)
dialog.addSubview(confirm)
imageView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(32)
make.centerX.equalToSuperview()
make.size.equalTo(180)
make.leading.greaterThanOrEqualToSuperview().offset(32)
}
confirm.snp.makeConstraints { make in
make.top.equalTo(imageView.snp.bottom).offset(12)
make.trailing.equalToSuperview().inset(16)
make.bottom.equalToSuperview().inset(12)
make.width.equalTo(64)
make.height.equalTo(44)
}
}
private func textButton(title: String, color: UIColor, action: Selector) -> UIButton {
let button = UIButton(type: .system)
button.setTitle(title, for: .normal)
button.setTitleColor(color, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
button.addTarget(self, action: action, for: .touchUpInside)
return button
}
@objc private func confirmTapped() {
onConfirm()
}
@objc private func cancelTapped() {
onCancel?()
}
}
private extension String {
var nonEmptyTrimmed: String? {
let text = trimmingCharacters(in: .whitespacesAndNewlines)
return text.isEmpty ? nil : text
}
}