同步相册云盘界面并完善相关交互

This commit is contained in:
2026-07-10 17:15:15 +08:00
parent ab5220e460
commit ceca780ab3
13 changed files with 2269 additions and 532 deletions

View File

@ -6,8 +6,14 @@
import SnapKit
import UIKit
/// Android
/// Android
final class CloudDriveActionSheetViewController: UIViewController {
///
enum Layout {
case upload
case controls
}
///
struct Item {
///
@ -20,19 +26,20 @@ final class CloudDriveActionSheetViewController: UIViewController {
let handler: () -> Void
}
private let sheetTitle: String?
private let sheetTitle: String
private let layout: Layout
private let items: [Item]
private let dimView = UIView()
private let panelView = UIView()
private let stackView = UIStackView()
private let contentStack = UIStackView()
///
init(title: String? = nil, items: [Item]) {
self.sheetTitle = title
/// Android
init(title: String, layout: Layout, items: [Item]) {
sheetTitle = title
self.layout = layout
self.items = items
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .overFullScreen
modalTransitionStyle = .crossDissolve
}
@available(*, unavailable)
@ -46,92 +53,607 @@ final class CloudDriveActionSheetViewController: UIViewController {
setupConstraints()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
dimView.alpha = 0
panelView.transform = CGAffineTransform(translationX: 0, y: 420)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.24, delay: 0, options: [.curveEaseOut]) {
self.dimView.alpha = 1
self.panelView.transform = .identity
}
}
private func setupUI() {
view.backgroundColor = .clear
dimView.backgroundColor = UIColor.black.withAlphaComponent(0.35)
dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelTapped)))
panelView.backgroundColor = .white
panelView.layer.cornerRadius = 18
panelView.layer.cornerRadius = 16
panelView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
panelView.clipsToBounds = true
stackView.axis = .vertical
stackView.spacing = 0
contentStack.axis = .vertical
contentStack.spacing = 0
view.addSubview(dimView)
view.addSubview(panelView)
panelView.addSubview(stackView)
panelView.addSubview(contentStack)
if let sheetTitle {
let titleLabel = UILabel()
titleLabel.text = sheetTitle
titleLabel.font = .app(.bodyMedium)
titleLabel.textColor = AppColor.textPrimary
titleLabel.textAlignment = .center
titleLabel.snp.makeConstraints { make in
make.height.equalTo(54)
}
stackView.addArrangedSubview(titleLabel)
let titleContainer = UIView()
let titleLabel = UILabel()
titleLabel.text = sheetTitle
titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
titleLabel.textColor = .black
titleLabel.textAlignment = .left
titleContainer.addSubview(titleLabel)
titleContainer.snp.makeConstraints { make in make.height.equalTo(52) }
titleLabel.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(16)
make.centerY.equalToSuperview()
}
contentStack.addArrangedSubview(titleContainer)
let titleDivider = UIView()
titleDivider.backgroundColor = UIColor(hex: 0xEEEEEE)
titleDivider.snp.makeConstraints { make in make.height.equalTo(0.5) }
contentStack.addArrangedSubview(titleDivider)
switch layout {
case .upload:
contentStack.addArrangedSubview(makeUploadContent())
case .controls:
contentStack.addArrangedSubview(makeControlsContent())
}
for item in items {
stackView.addArrangedSubview(makeItemButton(item))
}
let spacer = UIView()
spacer.backgroundColor = AppColor.pageBackground
spacer.snp.makeConstraints { make in
make.height.equalTo(8)
}
stackView.addArrangedSubview(spacer)
let cancelButton = UIButton(type: .system)
cancelButton.setTitle("取消", for: .normal)
cancelButton.setTitleColor(AppColor.textPrimary, for: .normal)
cancelButton.titleLabel?.font = .app(.body)
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
cancelButton.snp.makeConstraints { make in
make.height.equalTo(54)
}
stackView.addArrangedSubview(cancelButton)
let divider = UIView()
divider.backgroundColor = UIColor(hex: 0xEEEEEE)
divider.snp.makeConstraints { make in make.height.equalTo(0.5) }
contentStack.addArrangedSubview(divider)
contentStack.addArrangedSubview(makeCancelArea())
let bottomSpacer = UIView()
bottomSpacer.snp.makeConstraints { make in make.height.equalTo(12) }
contentStack.addArrangedSubview(bottomSpacer)
}
private func setupConstraints() {
dimView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
dimView.snp.makeConstraints { make in make.edges.equalToSuperview() }
panelView.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
make.top.greaterThanOrEqualTo(view.safeAreaLayoutGuide).offset(20)
}
stackView.snp.makeConstraints { make in
contentStack.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.bottom.equalTo(view.safeAreaLayoutGuide)
}
}
private func makeItemButton(_ item: Item) -> UIButton {
let button = UIButton(type: .system)
button.setTitle(item.title, for: .normal)
button.setImage(item.image, for: .normal)
button.tintColor = item.isDestructive ? AppColor.danger : AppColor.textPrimary
button.setTitleColor(item.isDestructive ? AppColor.danger : AppColor.textPrimary, for: .normal)
button.titleLabel?.font = .app(.body)
button.contentHorizontalAlignment = .left
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: AppSpacing.xl, bottom: 0, right: AppSpacing.xl)
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: AppSpacing.md)
button.addAction(UIAction { [weak self] _ in
self?.dismiss(animated: true) {
item.handler()
private func makeUploadContent() -> UIView {
let container = UIView()
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 12
container.addSubview(stack)
stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(16) }
items.enumerated().forEach { index, item in
let row = UIView()
row.layer.cornerRadius = 8
row.layer.borderWidth = 1
row.layer.borderColor = UIColor(hex: 0xF4F4F4).cgColor
let iconView = UIImageView(image: item.image?.withRenderingMode(.alwaysOriginal))
iconView.contentMode = .scaleAspectFit
let label = UILabel()
label.text = item.title
label.font = .systemFont(ofSize: 14, weight: .bold)
label.textColor = .black
let button = UIButton(type: .custom)
button.tag = index
button.accessibilityLabel = item.title
button.addTarget(self, action: #selector(itemTapped(_:)), for: .touchUpInside)
row.addSubview(iconView)
row.addSubview(label)
row.addSubview(button)
iconView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16)
make.centerY.equalToSuperview()
make.width.height.equalTo(32)
}
}, for: .touchUpInside)
button.snp.makeConstraints { make in
make.height.equalTo(54)
label.snp.makeConstraints { make in
make.leading.equalTo(iconView.snp.trailing).offset(12)
make.centerY.equalToSuperview()
make.trailing.lessThanOrEqualToSuperview().inset(16)
}
button.snp.makeConstraints { make in make.edges.equalToSuperview() }
row.snp.makeConstraints { make in make.height.equalTo(56) }
stack.addArrangedSubview(row)
}
return container
}
private func makeControlsContent() -> UIView {
let row = UIStackView()
row.axis = .horizontal
row.distribution = .fillEqually
items.enumerated().forEach { index, item in
let container = UIView()
let iconView = UIImageView(image: item.image?.withRenderingMode(.alwaysOriginal))
iconView.contentMode = .scaleAspectFit
let label = UILabel()
label.text = item.title
label.font = .systemFont(ofSize: 14)
label.textColor = UIColor(hex: 0x4B5563)
label.textAlignment = .center
let button = UIButton(type: .custom)
button.tag = index
button.accessibilityLabel = item.title
button.addTarget(self, action: #selector(itemTapped(_:)), for: .touchUpInside)
container.addSubview(iconView)
container.addSubview(label)
container.addSubview(button)
iconView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(28)
make.centerX.equalToSuperview()
make.width.height.equalTo(24)
}
label.snp.makeConstraints { make in
make.top.equalTo(iconView.snp.bottom).offset(8)
make.leading.trailing.equalToSuperview().inset(4)
}
button.snp.makeConstraints { make in make.edges.equalToSuperview() }
container.snp.makeConstraints { make in make.height.equalTo(105) }
row.addArrangedSubview(container)
}
return row
}
private func makeCancelArea() -> UIView {
let container = UIView()
let button = UIButton(type: .system)
button.setTitle("取消", for: .normal)
button.setTitleColor(UIColor(hex: 0x4B5563), for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
button.backgroundColor = UIColor(hex: 0xF4F4F4)
button.layer.cornerRadius = 12
button.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
container.addSubview(button)
button.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(16)
make.height.equalTo(48)
}
return container
}
private func close(completion: (() -> Void)? = nil) {
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseIn]) {
self.dimView.alpha = 0
self.panelView.transform = CGAffineTransform(translationX: 0, y: self.panelView.bounds.height + 40)
} completion: { _ in
self.dismiss(animated: false, completion: completion)
}
}
@objc private func itemTapped(_ sender: UIButton) {
guard items.indices.contains(sender.tag) else { return }
let handler = items[sender.tag].handler
close(completion: handler)
}
@objc private func cancelTapped() {
close()
}
}
/// Android
class CloudDriveOverlayDialogViewController: UIViewController {
let dimView = UIView()
let cardView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
modalPresentationStyle = .overFullScreen
view.backgroundColor = .clear
dimView.backgroundColor = UIColor.black.withAlphaComponent(0.35)
cardView.backgroundColor = .white
cardView.layer.cornerRadius = 12
cardView.clipsToBounds = true
view.addSubview(dimView)
view.addSubview(cardView)
dimView.snp.makeConstraints { make in make.edges.equalToSuperview() }
dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelOverlay)))
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
dimView.alpha = 0
cardView.alpha = 0
cardView.transform = CGAffineTransform(scaleX: 0.94, y: 0.94)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.2) {
self.dimView.alpha = 1
self.cardView.alpha = 1
self.cardView.transform = .identity
}
}
func dismissOverlay(completion: (() -> Void)? = nil) {
UIView.animate(withDuration: 0.16) {
self.dimView.alpha = 0
self.cardView.alpha = 0
self.cardView.transform = CGAffineTransform(scaleX: 0.96, y: 0.96)
} completion: { _ in
self.dismiss(animated: false, completion: completion)
}
}
@objc private func cancelOverlay() {
dismissOverlay()
}
}
/// Android `WeDialog`
final class CloudDriveConfirmationDialogViewController: CloudDriveOverlayDialogViewController {
private let dialogTitle: String
private let message: String
private let onConfirm: () -> Void
///
init(title: String, message: String, onConfirm: @escaping () -> Void) {
dialogTitle = title
self.message = message
self.onConfirm = onConfirm
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .overFullScreen
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
let titleLabel = UILabel()
titleLabel.text = dialogTitle
titleLabel.font = .systemFont(ofSize: 17, weight: .bold)
titleLabel.textColor = .black
titleLabel.textAlignment = .center
let messageLabel = UILabel()
messageLabel.text = message
messageLabel.font = .systemFont(ofSize: 17)
messageLabel.textColor = .black
messageLabel.textAlignment = .center
messageLabel.numberOfLines = 0
let divider = UIView()
divider.backgroundColor = UIColor(hex: 0xD1D5DB)
let verticalDivider = UIView()
verticalDivider.backgroundColor = UIColor(hex: 0xD1D5DB)
let cancelButton = makeDialogButton(title: "取消", color: .black, action: #selector(cancelTapped))
let confirmButton = makeDialogButton(title: "确定", color: AppColor.primary, action: #selector(confirmTapped))
[titleLabel, messageLabel, divider, cancelButton, verticalDivider, confirmButton].forEach(cardView.addSubview)
cardView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.equalToSuperview().multipliedBy(0.8)
}
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(32)
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)
}
divider.snp.makeConstraints { make in
make.top.equalTo(messageLabel.snp.bottom).offset(32)
make.leading.trailing.equalToSuperview()
make.height.equalTo(0.5)
}
cancelButton.snp.makeConstraints { make in
make.top.equalTo(divider.snp.bottom)
make.leading.bottom.equalToSuperview()
make.height.equalTo(56)
}
confirmButton.snp.makeConstraints { make in
make.top.bottom.width.equalTo(cancelButton)
make.leading.equalTo(cancelButton.snp.trailing)
make.trailing.equalToSuperview()
}
verticalDivider.snp.makeConstraints { make in
make.top.equalTo(divider.snp.bottom)
make.bottom.equalToSuperview()
make.centerX.equalToSuperview()
make.width.equalTo(0.5)
}
}
private func makeDialogButton(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: 17, weight: .bold)
button.addTarget(self, action: action, for: .touchUpInside)
return button
}
@objc private func cancelTapped() {
dismiss(animated: true)
dismissOverlay()
}
@objc private func confirmTapped() {
dismissOverlay(completion: onConfirm)
}
}
/// Android `AddFolderDialog`
final class CloudDriveTextInputDialogViewController: CloudDriveOverlayDialogViewController {
private let dialogTitle: String
private let placeholder: String
private let onConfirm: (String) -> Void
private let textField = UITextField()
private let confirmButton = UIButton(type: .system)
///
init(title: String, placeholder: String, onConfirm: @escaping (String) -> Void) {
dialogTitle = title
self.placeholder = placeholder
self.onConfirm = onConfirm
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .overFullScreen
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
cardView.layer.cornerRadius = 16
let titleLabel = UILabel()
titleLabel.text = dialogTitle
titleLabel.font = .systemFont(ofSize: 18, weight: .bold)
titleLabel.textColor = .black
let divider = UIView()
divider.backgroundColor = UIColor(hex: 0xEEEEEE)
textField.placeholder = placeholder
textField.font = .systemFont(ofSize: 14)
textField.textColor = UIColor(hex: 0x333333)
textField.backgroundColor = UIColor(hex: 0xF4F4F4)
textField.layer.cornerRadius = 8
textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 1))
textField.leftViewMode = .always
textField.clearButtonMode = .whileEditing
textField.returnKeyType = .done
textField.addTarget(self, action: #selector(textChanged), for: .editingChanged)
textField.addTarget(self, action: #selector(confirmTapped), for: .editingDidEndOnExit)
let cancelButton = UIButton(type: .system)
cancelButton.setTitle("取消", for: .normal)
cancelButton.setTitleColor(AppColor.primary, for: .normal)
cancelButton.titleLabel?.font = .systemFont(ofSize: 14)
cancelButton.backgroundColor = UIColor(hex: 0xEFF6FF)
cancelButton.layer.cornerRadius = 8
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
confirmButton.setTitle("确定", for: .normal)
confirmButton.setTitleColor(.white, for: .normal)
confirmButton.titleLabel?.font = .systemFont(ofSize: 14)
confirmButton.layer.cornerRadius = 8
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
confirmButton.isEnabled = false
updateConfirmAppearance()
[titleLabel, divider, textField, cancelButton, confirmButton].forEach(cardView.addSubview)
cardView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.trailing.equalToSuperview().inset(28)
}
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview()
make.leading.trailing.equalToSuperview().inset(16)
make.height.equalTo(52)
}
divider.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom)
make.leading.trailing.equalToSuperview()
make.height.equalTo(1)
}
textField.snp.makeConstraints { make in
make.top.equalTo(divider.snp.bottom).offset(16)
make.leading.trailing.equalToSuperview().inset(16)
make.height.equalTo(48)
}
confirmButton.snp.makeConstraints { make in
make.top.equalTo(textField.snp.bottom).offset(20)
make.trailing.bottom.equalToSuperview().inset(16)
make.width.equalTo(72)
make.height.equalTo(38)
}
cancelButton.snp.makeConstraints { make in
make.trailing.equalTo(confirmButton.snp.leading).offset(-8)
make.centerY.width.height.equalTo(confirmButton)
}
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardFrameChanged(_:)),
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil
)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
private func updateConfirmAppearance() {
confirmButton.backgroundColor = confirmButton.isEnabled ? AppColor.primary : AppColor.buttonDisabled
}
@objc private func textChanged() {
confirmButton.isEnabled = !(textField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
updateConfirmAppearance()
}
@objc private func cancelTapped() {
view.endEditing(true)
dismissOverlay()
}
@objc private func confirmTapped() {
let value = (textField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
guard !value.isEmpty else { return }
view.endEditing(true)
dismissOverlay { [onConfirm] in onConfirm(value) }
}
@objc private func keyboardFrameChanged(_ notification: Notification) {
guard let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
let frameInView = view.convert(frame, from: nil)
let overlap = max(0, cardView.frame.maxY + 16 - frameInView.minY)
UIView.animate(withDuration: 0.2) {
self.cardView.transform = CGAffineTransform(translationX: 0, y: -overlap)
}
}
}
/// Android `MoveFileDialog`
final class CloudDriveMoveDialogViewController: CloudDriveOverlayDialogViewController, UITableViewDataSource, UITableViewDelegate {
private let folders: [CloudFile]
private let onConfirm: (Int) -> Void
private let tableView = UITableView(frame: .zero, style: .plain)
private var selectedID: Int
///
init(folders: [CloudFile], onConfirm: @escaping (Int) -> Void) {
self.folders = folders
self.onConfirm = onConfirm
selectedID = folders.first?.id ?? 0
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .overFullScreen
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
cardView.layer.cornerRadius = 28
let titleLabel = UILabel()
titleLabel.text = "移动到"
titleLabel.font = .systemFont(ofSize: 16, weight: .semibold)
titleLabel.textColor = UIColor(hex: 0x333333)
let messageLabel = UILabel()
messageLabel.text = "请选择目标文件夹"
messageLabel.font = .systemFont(ofSize: 14)
messageLabel.textColor = UIColor(hex: 0x666666)
tableView.backgroundColor = .white
tableView.separatorStyle = .none
tableView.rowHeight = 44
tableView.dataSource = self
tableView.delegate = self
let cancelButton = makeTextButton(title: "取消", color: UIColor(hex: 0x666666), action: #selector(cancelTapped))
let confirmButton = makeTextButton(title: "确定", color: AppColor.primary, action: #selector(confirmTapped))
[titleLabel, messageLabel, tableView, cancelButton, confirmButton].forEach(cardView.addSubview)
cardView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.trailing.equalToSuperview().inset(28)
}
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(12)
make.leading.trailing.equalTo(titleLabel)
}
tableView.snp.makeConstraints { make in
make.top.equalTo(messageLabel.snp.bottom).offset(12)
make.leading.trailing.equalToSuperview().inset(16)
make.height.equalTo(min(CGFloat(max(folders.count, 1)) * 44, UIScreen.main.bounds.height * 0.6))
}
confirmButton.snp.makeConstraints { make in
make.top.equalTo(tableView.snp.bottom).offset(8)
make.trailing.bottom.equalToSuperview().inset(12)
make.width.equalTo(64)
make.height.equalTo(44)
}
cancelButton.snp.makeConstraints { make in
make.trailing.equalTo(confirmButton.snp.leading)
make.centerY.width.height.equalTo(confirmButton)
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
folders.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let reuseIdentifier = "CloudDriveMoveFolderCell"
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier)
?? UITableViewCell(style: .default, reuseIdentifier: reuseIdentifier)
configure(cell, folder: folders[indexPath.row])
return cell
}
private func configure(_ cell: UITableViewCell, folder: CloudFile) {
cell.textLabel?.text = folder.name
cell.textLabel?.font = .systemFont(ofSize: 14)
cell.textLabel?.textColor = UIColor(hex: 0x333333)
cell.imageView?.image = UIImage(
systemName: selectedID == folder.id ? "circle.inset.filled" : "circle",
withConfiguration: UIImage.SymbolConfiguration(pointSize: 20)
)
cell.imageView?.tintColor = AppColor.primary
cell.selectionStyle = .none
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let previousID = selectedID
selectedID = folders[indexPath.row].id
if let previousIndex = folders.firstIndex(where: { $0.id == previousID }),
let previousCell = tableView.cellForRow(at: IndexPath(row: previousIndex, section: 0)) {
configure(previousCell, folder: folders[previousIndex])
}
if let selectedCell = tableView.cellForRow(at: indexPath) {
configure(selectedCell, folder: folders[indexPath.row])
}
}
private func makeTextButton(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 cancelTapped() {
dismissOverlay()
}
@objc private func confirmTapped() {
let selectedID = selectedID
dismissOverlay { [onConfirm] in onConfirm(selectedID) }
}
}