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

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,18 +6,15 @@
import SnapKit
import UIKit
///
/// Android `CloudStorageFileDetailBottomModal`
final class CloudDriveDetailSheetViewController: UIViewController {
private let file: CloudFile
private let onSave: (String) -> Void
private let dimView = UIView()
private let panelView = UIView()
private let titleLabel = UILabel()
private let nameTitleLabel = UILabel()
private let scrollView = UIScrollView()
private let contentView = UIView()
private let nameField = UITextField()
private let infoLabel = UILabel()
private let cancelButton = UIButton(type: .system)
private let saveButton = UIButton(type: .system)
///
init(file: CloudFile, onSave: @escaping (String) -> Void) {
@ -25,7 +22,6 @@ final class CloudDriveDetailSheetViewController: UIViewController {
self.onSave = onSave
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .overFullScreen
modalTransitionStyle = .crossDissolve
}
@available(*, unavailable)
@ -37,6 +33,25 @@ final class CloudDriveDetailSheetViewController: UIViewController {
super.viewDidLoad()
setupUI()
setupConstraints()
registerKeyboardNotifications()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
dimView.alpha = 0
panelView.transform = CGAffineTransform(translationX: 0, y: 620)
}
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
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
private func setupUI() {
@ -45,99 +60,241 @@ final class CloudDriveDetailSheetViewController: UIViewController {
dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelTapped)))
panelView.backgroundColor = .white
panelView.layer.cornerRadius = 18
panelView.layer.cornerRadius = 28
panelView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
panelView.clipsToBounds = true
titleLabel.text = "详情"
titleLabel.font = .app(.bodyMedium)
titleLabel.textColor = AppColor.textPrimary
titleLabel.textAlignment = .center
nameTitleLabel.text = "文件名称"
nameTitleLabel.font = .app(.caption)
nameTitleLabel.textColor = AppColor.textSecondary
nameField.text = file.name
nameField.font = .app(.body)
nameField.textColor = AppColor.textPrimary
nameField.clearButtonMode = .whileEditing
nameField.returnKeyType = .done
nameField.delegate = self
nameField.backgroundColor = AppColor.inputBackground
nameField.layer.cornerRadius = AppRadius.xs
nameField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: AppSpacing.sm, height: 1))
nameField.leftViewMode = .always
infoLabel.text = "\(file.createdAt) | \(CloudDriveFormatter.fileSize(file.fileSize))\n类型:\(CloudDriveFormatter.typeName(file.type))"
infoLabel.font = .app(.caption)
infoLabel.textColor = AppColor.textTertiary
infoLabel.numberOfLines = 0
cancelButton.setTitle("取消", for: .normal)
cancelButton.setTitleColor(AppColor.textSecondary, for: .normal)
cancelButton.titleLabel?.font = .app(.body)
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
saveButton.setTitle("保存", for: .normal)
saveButton.setTitleColor(.white, for: .normal)
saveButton.titleLabel?.font = .app(.bodyMedium)
saveButton.backgroundColor = AppColor.primary
saveButton.layer.cornerRadius = AppRadius.xs
saveButton.addTarget(self, action: #selector(saveTapped), for: .touchUpInside)
scrollView.alwaysBounceVertical = false
scrollView.keyboardDismissMode = .interactive
view.addSubview(dimView)
view.addSubview(panelView)
[titleLabel, nameTitleLabel, nameField, infoLabel, cancelButton, saveButton].forEach(panelView.addSubview)
panelView.addSubview(scrollView)
scrollView.addSubview(contentView)
let dragHandle = UIView()
dragHandle.backgroundColor = UIColor(hex: 0x79747E)
dragHandle.layer.cornerRadius = 2
let typeIcon = UIImageView(image: fileTypeIcon())
typeIcon.contentMode = .scaleAspectFit
let fileNameLabel = UILabel()
fileNameLabel.text = file.name
fileNameLabel.font = .systemFont(ofSize: 16)
fileNameLabel.textColor = UIColor(hex: 0x333333)
fileNameLabel.numberOfLines = 1
fileNameLabel.lineBreakMode = .byTruncatingTail
let metaLabel = UILabel()
metaLabel.text = "\(file.createdAt) | \(CloudDriveFormatter.fileSize(file.fileSize))"
metaLabel.font = .systemFont(ofSize: 12)
metaLabel.textColor = UIColor(hex: 0x999999)
metaLabel.numberOfLines = 1
let closeButton = UIButton(type: .system)
closeButton.setImage(
UIImage(systemName: "xmark")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 17, weight: .medium)),
for: .normal
)
closeButton.tintColor = UIColor(hex: 0x333333)
closeButton.accessibilityLabel = "关闭"
closeButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
let nameTitleLabel = makeFieldTitle("文件名称")
nameField.text = file.name
nameField.placeholder = file.name
nameField.font = .systemFont(ofSize: 16)
nameField.textColor = UIColor(hex: 0x333333)
nameField.clearButtonMode = .whileEditing
nameField.returnKeyType = .done
nameField.delegate = self
nameField.backgroundColor = .white
nameField.layer.cornerRadius = 4
nameField.layer.borderWidth = 1
nameField.layer.borderColor = UIColor(hex: 0x9CA3AF).cgColor
nameField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 1))
nameField.leftViewMode = .always
let infoStack = UIStackView()
infoStack.axis = .vertical
infoStack.spacing = 0
infoStack.addArrangedSubview(makeInfoRow(label: "文件类型", value: detailTypeText()))
if file.isFolder {
infoStack.addArrangedSubview(makeInfoRow(label: "项目数量", value: String(file.childNum)))
} else {
infoStack.addArrangedSubview(makeInfoRow(label: "文件大小", value: CloudDriveFormatter.fileSize(file.fileSize)))
}
infoStack.addArrangedSubview(makeInfoRow(label: "创建时间", value: file.createdAt))
infoStack.addArrangedSubview(makeInfoRow(label: "修改时间", value: file.updatedAt))
let saveButton = UIButton(type: .system)
saveButton.setTitle("保存", for: .normal)
saveButton.setTitleColor(.white, for: .normal)
saveButton.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium)
saveButton.backgroundColor = AppColor.primary
saveButton.layer.cornerRadius = 10
saveButton.addTarget(self, action: #selector(saveTapped), for: .touchUpInside)
[dragHandle, typeIcon, fileNameLabel, metaLabel, closeButton, nameTitleLabel, nameField, infoStack, saveButton]
.forEach(contentView.addSubview)
dragHandle.snp.makeConstraints { make in
make.top.equalToSuperview().offset(15)
make.centerX.equalToSuperview()
make.width.equalTo(40)
make.height.equalTo(4)
}
typeIcon.snp.makeConstraints { make in
make.top.equalTo(dragHandle.snp.bottom).offset(24)
make.leading.equalToSuperview().offset(16)
make.width.height.equalTo(40)
}
closeButton.snp.makeConstraints { make in
make.centerY.equalTo(typeIcon)
make.trailing.equalToSuperview().inset(4)
make.width.height.equalTo(44)
}
fileNameLabel.snp.makeConstraints { make in
make.top.equalTo(typeIcon)
make.leading.equalTo(typeIcon.snp.trailing).offset(12)
make.trailing.equalTo(closeButton.snp.leading).offset(-8)
}
metaLabel.snp.makeConstraints { make in
make.top.equalTo(fileNameLabel.snp.bottom).offset(4)
make.leading.trailing.equalTo(fileNameLabel)
}
nameTitleLabel.snp.makeConstraints { make in
make.top.equalTo(typeIcon.snp.bottom).offset(16)
make.leading.trailing.equalToSuperview().inset(16)
}
nameField.snp.makeConstraints { make in
make.top.equalTo(nameTitleLabel.snp.bottom).offset(6)
make.leading.trailing.equalTo(nameTitleLabel)
make.height.equalTo(52)
}
infoStack.snp.makeConstraints { make in
make.top.equalTo(nameField.snp.bottom).offset(10)
make.leading.trailing.equalTo(nameField)
}
saveButton.snp.makeConstraints { make in
make.top.equalTo(infoStack.snp.bottom).offset(20)
make.leading.trailing.equalTo(nameField)
make.height.equalTo(44)
make.bottom.equalToSuperview().inset(12)
}
}
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.leading.trailing.equalToSuperview()
make.bottom.equalToSuperview()
make.top.greaterThanOrEqualTo(view.safeAreaLayoutGuide).offset(16)
make.height.equalTo(590).priority(.high)
}
titleLabel.snp.makeConstraints { make in
scrollView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.height.equalTo(54)
make.bottom.equalTo(view.safeAreaLayoutGuide)
}
nameTitleLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.sm)
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
contentView.snp.makeConstraints { make in
make.edges.equalTo(scrollView.contentLayoutGuide)
make.width.equalTo(scrollView.frameLayoutGuide)
}
nameField.snp.makeConstraints { make in
make.top.equalTo(nameTitleLabel.snp.bottom).offset(AppSpacing.xs)
make.leading.trailing.equalTo(nameTitleLabel)
make.height.equalTo(42)
}
private func makeFieldTitle(_ text: String) -> UILabel {
let label = UILabel()
label.text = text
label.font = .systemFont(ofSize: 13)
label.textColor = UIColor(hex: 0x7B8EAA)
return label
}
private func makeInfoRow(label: String, value: String) -> UIView {
let container = UIView()
let labelView = makeFieldTitle(label)
let valueContainer = UIView()
valueContainer.backgroundColor = UIColor(hex: 0xF4F4F4)
valueContainer.layer.cornerRadius = 6
let valueLabel = UILabel()
valueLabel.text = value
valueLabel.font = .systemFont(ofSize: 14)
valueLabel.textColor = UIColor(hex: 0x333333)
valueLabel.numberOfLines = 1
valueLabel.lineBreakMode = .byTruncatingTail
container.addSubview(labelView)
container.addSubview(valueContainer)
valueContainer.addSubview(valueLabel)
labelView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
}
infoLabel.snp.makeConstraints { make in
make.top.equalTo(nameField.snp.bottom).offset(AppSpacing.md)
make.leading.trailing.equalTo(nameTitleLabel)
valueContainer.snp.makeConstraints { make in
make.top.equalTo(labelView.snp.bottom).offset(4)
make.leading.trailing.equalToSuperview()
make.height.equalTo(36)
make.bottom.equalToSuperview().inset(6)
}
cancelButton.snp.makeConstraints { make in
make.top.equalTo(infoLabel.snp.bottom).offset(AppSpacing.lg)
make.leading.equalToSuperview().offset(AppSpacing.md)
make.height.equalTo(44)
make.width.equalTo(saveButton)
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(AppSpacing.md)
valueLabel.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(12)
make.centerY.equalToSuperview()
}
saveButton.snp.makeConstraints { make in
make.top.bottom.width.equalTo(cancelButton)
make.leading.equalTo(cancelButton.snp.trailing).offset(AppSpacing.md)
make.trailing.equalToSuperview().inset(AppSpacing.md)
return container
}
private func fileTypeIcon() -> UIImage? {
if file.isFolder {
return CloudDriveAsset.image(named: "icon_cloud_storage_file_type_folder", fallbackSystemName: "folder.fill")
}
if file.isVideo {
return CloudDriveAsset.image(named: "icon_cloud_storage_file_type_video", fallbackSystemName: "video.fill")
}
return CloudDriveAsset.image(named: "icon_cloud_storage_file_type_photo", fallbackSystemName: "photo.fill")
}
private func detailTypeText() -> String {
if file.isFolder { return "文件夹" }
let suffix = (file.name as NSString).pathExtension.uppercased()
return suffix.isEmpty ? "文件" : suffix
}
private func registerKeyboardNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardFrameChanged(_:)),
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil
)
}
private func close(completion: (() -> Void)? = nil) {
view.endEditing(true)
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 cancelTapped() {
dismiss(animated: true)
close()
}
@objc private func saveTapped() {
let name = nameField.text ?? ""
dismiss(animated: true) { [onSave] in
onSave(name)
}
let name = (nameField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
close { [onSave] in onSave(name) }
}
@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, view.bounds.maxY - frameInView.minY)
scrollView.contentInset.bottom = overlap
scrollView.verticalScrollIndicatorInsets.bottom = overlap
guard overlap > 0 else { return }
let fieldFrame = nameField.convert(nameField.bounds, to: scrollView)
scrollView.scrollRectToVisible(fieldFrame.insetBy(dx: 0, dy: -16), animated: true)
}
}