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

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,21 +6,34 @@
import SnapKit
import UIKit
/// Android `CloudStorageTransitScreen`
/// Android `CloudStorageTransitScreen`
final class CloudDriveTransferViewController: BaseViewController {
private enum Tab: Int {
case upload
case download
}
private enum Section {
case main
}
private let manager: CloudTransferManager
private let segmentedControl = UISegmentedControl(items: ["上传", "下载"])
private let navigationDivider = UIView()
private let tabContainer = UIView()
private let uploadTabButton = UIButton(type: .system)
private let downloadTabButton = UIButton(type: .system)
private let tabIndicator = UIView()
private let tableView = UITableView(frame: .zero, style: .plain)
private var dataSource: UITableViewDiffableDataSource<Section, String>!
private var selectedTab: Tab = .upload
private var previousStandardAppearance: UINavigationBarAppearance?
private var previousScrollEdgeAppearance: UINavigationBarAppearance?
private var previousCompactAppearance: UINavigationBarAppearance?
private var previousNavigationTintColor: UIColor?
///
init(manager: CloudTransferManager = .shared) {
self.manager = manager
init(manager: CloudTransferManager? = nil) {
self.manager = manager ?? .shared
super.init(nibName: nil, bundle: nil)
}
@ -30,81 +43,217 @@ final class CloudDriveTransferViewController: BaseViewController {
}
override func setupNavigationBar() {
title = "传输管理"
let titleLabel = UILabel()
titleLabel.text = "传输管理"
titleLabel.textColor = UIColor(hex: 0x333333)
titleLabel.font = .systemFont(ofSize: 18)
navigationItem.titleView = titleLabel
let backButton = UIButton(type: .system)
backButton.setImage(
UIImage(systemName: "chevron.left")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)),
for: .normal
)
backButton.tintColor = .black
backButton.accessibilityLabel = "返回"
backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
backButton.snp.makeConstraints { make in make.width.height.equalTo(44) }
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButton)
}
override func setupUI() {
view.backgroundColor = AppColor.pageBackground
segmentedControl.selectedSegmentIndex = selectedTab.rawValue
navigationDivider.backgroundColor = UIColor(hex: 0xEEEEEE)
tabContainer.backgroundColor = .white
configureTabButton(uploadTabButton, title: "上传", action: #selector(uploadTabTapped))
configureTabButton(downloadTabButton, title: "下载", action: #selector(downloadTabTapped))
tabIndicator.backgroundColor = AppColor.primary
tableView.backgroundColor = AppColor.pageBackground
tableView.separatorStyle = .none
tableView.dataSource = self
tableView.showsVerticalScrollIndicator = false
tableView.contentInsetAdjustmentBehavior = .never
tableView.delegate = self
tableView.register(CloudTransferCell.self, forCellReuseIdentifier: CloudTransferCell.reuseIdentifier)
view.addSubview(segmentedControl)
view.addSubview(navigationDivider)
view.addSubview(tabContainer)
tabContainer.addSubview(uploadTabButton)
tabContainer.addSubview(downloadTabButton)
tabContainer.addSubview(tabIndicator)
view.addSubview(tableView)
configureDataSource()
}
override func setupConstraints() {
segmentedControl.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(AppSpacing.sm)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.height.equalTo(36)
navigationDivider.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide)
make.leading.trailing.equalToSuperview()
make.height.equalTo(0.5)
}
tabContainer.snp.makeConstraints { make in
make.top.equalTo(navigationDivider.snp.bottom)
make.leading.trailing.equalToSuperview()
make.height.equalTo(48)
}
uploadTabButton.snp.makeConstraints { make in
make.top.leading.bottom.equalToSuperview()
make.width.equalToSuperview().multipliedBy(0.5)
}
downloadTabButton.snp.makeConstraints { make in
make.top.trailing.bottom.equalToSuperview()
make.width.equalTo(uploadTabButton)
}
tabIndicator.snp.makeConstraints { make in
make.bottom.equalToSuperview()
make.height.equalTo(2)
make.width.equalToSuperview().multipliedBy(0.5)
make.leading.equalToSuperview()
}
tableView.snp.makeConstraints { make in
make.top.equalTo(segmentedControl.snp.bottom).offset(AppSpacing.sm)
make.top.equalTo(tabContainer.snp.bottom)
make.leading.trailing.bottom.equalToSuperview()
}
}
override func bindActions() {
segmentedControl.addTarget(self, action: #selector(tabChanged), for: .valueChanged)
manager.onTasksChange = { [weak self] in
Task { @MainActor in self?.tableView.reloadData() }
Task { @MainActor in self?.applySnapshot(animated: true) }
}
updateTabs(animated: false)
applySnapshot(animated: false)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
applyCloudNavigationBarAppearance()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
restoreNavigationBarAppearance()
}
private var visibleTasks: [CloudTransferTask] {
selectedTab == .upload ? manager.uploadTasks : manager.downloadTasks
}
@objc private func tabChanged() {
selectedTab = Tab(rawValue: segmentedControl.selectedSegmentIndex) ?? .upload
tableView.reloadData()
}
}
extension CloudDriveTransferViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
visibleTasks.count
private func configureTabButton(_ button: UIButton, title: String, action: Selector) {
button.setTitle(title, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14)
button.addTarget(self, action: action, for: .touchUpInside)
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
112
private func applyCloudNavigationBarAppearance() {
guard let navigationBar = navigationController?.navigationBar else { return }
previousStandardAppearance = navigationBar.standardAppearance
previousScrollEdgeAppearance = navigationBar.scrollEdgeAppearance
previousCompactAppearance = navigationBar.compactAppearance
previousNavigationTintColor = navigationBar.tintColor
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = .white
appearance.shadowColor = .clear
navigationBar.standardAppearance = appearance
navigationBar.scrollEdgeAppearance = appearance
navigationBar.compactAppearance = appearance
navigationBar.tintColor = .black
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CloudTransferCell.reuseIdentifier, for: indexPath) as! CloudTransferCell
let task = visibleTasks[indexPath.row]
cell.apply(task: task)
cell.onPrimaryAction = { [weak self] in
guard let self else { return }
if task.kind == .upload {
task.status == .uploading ? self.manager.pauseUpload(id: task.id) : self.manager.resumeUpload(id: task.id)
} else {
task.status == .downloading ? self.manager.pauseDownload(id: task.id) : self.manager.resumeDownload(id: task.id)
private func restoreNavigationBarAppearance() {
guard let navigationBar = navigationController?.navigationBar else { return }
if let previousStandardAppearance {
navigationBar.standardAppearance = previousStandardAppearance
}
navigationBar.scrollEdgeAppearance = previousScrollEdgeAppearance
navigationBar.compactAppearance = previousCompactAppearance
navigationBar.tintColor = previousNavigationTintColor
}
private func configureDataSource() {
dataSource = UITableViewDiffableDataSource<Section, String>(tableView: tableView) { [weak self] tableView, indexPath, taskID in
guard let self,
let task = self.visibleTasks.first(where: { $0.id == taskID }),
let cell = tableView.dequeueReusableCell(
withIdentifier: CloudTransferCell.reuseIdentifier,
for: indexPath
) as? CloudTransferCell else {
return UITableViewCell()
}
cell.apply(task: task)
cell.onPrimaryAction = { [weak self] in
guard let self else { return }
switch task.kind {
case .upload:
task.status == .uploading ? self.manager.pauseUpload(id: task.id) : self.manager.resumeUpload(id: task.id)
case .download:
task.status == .downloading ? self.manager.pauseDownload(id: task.id) : self.manager.resumeDownload(id: task.id)
}
}
cell.onCancel = { [weak self] in
guard let self else { return }
task.kind == .upload ? self.manager.cancelUpload(id: task.id) : self.manager.cancelDownload(id: task.id)
}
return cell
}
cell.onCancel = { [weak self] in
guard let self else { return }
task.kind == .upload ? self.manager.cancelUpload(id: task.id) : self.manager.cancelDownload(id: task.id)
dataSource.defaultRowAnimation = .fade
}
private func applySnapshot(animated: Bool) {
let previousIDs = Set(dataSource.snapshot().itemIdentifiers)
let taskIDs = visibleTasks.map(\.id)
var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
snapshot.appendSections([.main])
snapshot.appendItems(taskIDs)
snapshot.reconfigureItems(taskIDs.filter(previousIDs.contains))
dataSource.apply(snapshot, animatingDifferences: animated)
}
private func updateTabs(animated: Bool) {
let uploadSelected = selectedTab == .upload
uploadTabButton.setTitleColor(uploadSelected ? AppColor.primary : UIColor(hex: 0x333333), for: .normal)
downloadTabButton.setTitleColor(uploadSelected ? UIColor(hex: 0x333333) : AppColor.primary, for: .normal)
uploadTabButton.titleLabel?.font = .systemFont(ofSize: 14, weight: uploadSelected ? .medium : .regular)
downloadTabButton.titleLabel?.font = .systemFont(ofSize: 14, weight: uploadSelected ? .regular : .medium)
tabIndicator.snp.updateConstraints { make in
make.leading.equalToSuperview().offset(uploadSelected ? 0 : view.bounds.width / 2)
}
return cell
guard animated else {
tabContainer.layoutIfNeeded()
return
}
UIView.animate(withDuration: 0.2) { self.tabContainer.layoutIfNeeded() }
}
private func selectTab(_ tab: Tab) {
guard selectedTab != tab else { return }
selectedTab = tab
updateTabs(animated: true)
applySnapshot(animated: true)
tableView.setContentOffset(.zero, animated: false)
}
@objc private func backTapped() {
navigationController?.popViewController(animated: true)
}
@objc private func uploadTabTapped() {
selectTab(.upload)
}
@objc private func downloadTabTapped() {
selectTab(.download)
}
}
/// Cell
extension CloudDriveTransferViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
119
}
}
/// Cell Android /
private final class CloudTransferCell: UITableViewCell {
static let reuseIdentifier = "CloudTransferCell"
@ -137,90 +286,124 @@ private final class CloudTransferCell: UITableViewCell {
onCancel = nil
}
///
func apply(task: CloudTransferTask) {
iconView.image = CloudDriveAsset.image(
named: task.fileType == 1 ? "icon_cloud_storage_file_type_video" : "icon_cloud_storage_file_type_photo",
fallbackSystemName: task.fileType == 1 ? "video.fill" : "photo.fill"
)
titleLabel.text = task.fileName
statusLabel.text = "\(task.status.title) \(CloudDriveFormatter.fileSize(task.fileSize))"
statusLabel.text = statusText(for: task)
progressView.progress = Float(task.progress) / 100
percentLabel.text = "\(task.progress)%"
let isRunning = task.status == .uploading || task.status == .downloading
primaryButton.setImage(UIImage(systemName: isRunning ? "pause.fill" : "play.fill"), for: .normal)
primaryButton.isHidden = task.status == .completed
let canResume = task.status == .paused || task.status == .failed
primaryButton.setImage(
UIImage(systemName: isRunning ? "pause.fill" : "play.fill")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20)),
for: .normal
)
primaryButton.isHidden = !(isRunning || canResume)
}
private func statusText(for task: CloudTransferTask) -> String {
switch task.kind {
case .upload:
let status: String
switch task.status {
case .uploading: status = "上传中"
case .paused: status = "已暂停"
case .failed: status = "失败"
default: status = ""
}
let size = CloudDriveFormatter.fileSize(task.fileSize)
return status.isEmpty ? size : "\(status) \(size)"
case .download:
switch task.status {
case .downloading: return "下载中"
case .paused: return "已暂停"
case .failed: return "失败"
case .completed: return "已完成"
default: return ""
}
}
}
private func setupUI() {
backgroundColor = .clear
selectionStyle = .none
cardView.backgroundColor = .white
cardView.layer.cornerRadius = AppRadius.sm
cardView.layer.cornerRadius = 8
cardView.clipsToBounds = true
iconView.tintColor = AppColor.primary
iconView.contentMode = .scaleAspectFit
titleLabel.font = .app(.body)
titleLabel.textColor = AppColor.textPrimary
titleLabel.font = .systemFont(ofSize: 15)
titleLabel.textColor = UIColor(hex: 0x333333)
titleLabel.lineBreakMode = .byTruncatingMiddle
statusLabel.font = .app(.caption)
statusLabel.textColor = AppColor.textTertiary
statusLabel.font = .systemFont(ofSize: 12)
statusLabel.textColor = .gray
progressView.progressTintColor = AppColor.primary
percentLabel.font = .app(.caption)
percentLabel.textColor = AppColor.textSecondary
progressView.trackTintColor = UIColor(hex: 0xE5E7EB)
progressView.transform = CGAffineTransform(scaleX: 1, y: 2)
percentLabel.font = .systemFont(ofSize: 11)
percentLabel.textColor = UIColor(hex: 0x4B5563)
percentLabel.textAlignment = .right
primaryButton.tintColor = AppColor.primary
cancelButton.tintColor = AppColor.textSecondary
cancelButton.setImage(UIImage(systemName: "xmark"), for: .normal)
primaryButton.tintColor = UIColor(hex: 0x333333)
cancelButton.tintColor = UIColor(hex: 0x333333)
cancelButton.setImage(
UIImage(systemName: "xmark")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20)),
for: .normal
)
primaryButton.accessibilityLabel = "暂停或继续"
cancelButton.accessibilityLabel = "取消任务"
primaryButton.addTarget(self, action: #selector(primaryTapped), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
contentView.addSubview(cardView)
cardView.addSubview(iconView)
cardView.addSubview(titleLabel)
cardView.addSubview(statusLabel)
cardView.addSubview(primaryButton)
cardView.addSubview(cancelButton)
cardView.addSubview(progressView)
cardView.addSubview(percentLabel)
[iconView, titleLabel, statusLabel, primaryButton, cancelButton, progressView, percentLabel]
.forEach(cardView.addSubview)
}
private func setupConstraints() {
cardView.snp.makeConstraints { make in
make.top.bottom.equalToSuperview().inset(AppSpacing.xs)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
make.top.equalToSuperview().offset(12)
make.leading.trailing.equalToSuperview().inset(12)
make.bottom.equalToSuperview()
}
iconView.snp.makeConstraints { make in
make.leading.top.equalToSuperview().offset(AppSpacing.sm)
make.leading.top.equalToSuperview().offset(12)
make.width.height.equalTo(48)
}
cancelButton.snp.makeConstraints { make in
make.trailing.top.equalToSuperview().inset(AppSpacing.xs)
make.width.height.equalTo(36)
make.trailing.equalToSuperview()
make.top.equalToSuperview().offset(8)
make.width.height.equalTo(44)
}
primaryButton.snp.makeConstraints { make in
make.trailing.equalTo(cancelButton.snp.leading)
make.centerY.equalTo(cancelButton)
make.width.height.equalTo(36)
make.width.height.equalTo(44)
}
titleLabel.snp.makeConstraints { make in
make.leading.equalTo(iconView.snp.trailing).offset(AppSpacing.sm)
make.leading.equalTo(iconView.snp.trailing).offset(12)
make.top.equalTo(iconView)
make.trailing.equalTo(primaryButton.snp.leading).offset(-AppSpacing.xs)
make.trailing.equalTo(primaryButton.snp.leading).offset(-4)
}
statusLabel.snp.makeConstraints { make in
make.leading.trailing.equalTo(titleLabel)
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.xxs)
make.top.equalTo(titleLabel.snp.bottom).offset(4)
}
progressView.snp.makeConstraints { make in
make.leading.equalTo(iconView)
make.trailing.equalToSuperview().inset(AppSpacing.sm)
make.top.equalTo(iconView.snp.bottom).offset(AppSpacing.sm)
make.leading.equalToSuperview().offset(12)
make.trailing.equalToSuperview().inset(12)
make.top.equalTo(iconView.snp.bottom).offset(12)
make.height.equalTo(4)
}
percentLabel.snp.makeConstraints { make in
make.top.equalTo(progressView.snp.bottom).offset(AppSpacing.xxs)
make.top.equalTo(progressView.snp.bottom).offset(4)
make.trailing.equalTo(progressView)
make.bottom.equalToSuperview().inset(12)
}
}