同步相册云盘界面并完善相关交互
This commit is contained in:
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user