307 lines
12 KiB
Swift
307 lines
12 KiB
Swift
//
|
||
// CloudDriveDetailSheetViewController.swift
|
||
// suixinkan
|
||
//
|
||
|
||
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 scrollView = UIScrollView()
|
||
private let contentView = UIView()
|
||
private let nameField = UITextField()
|
||
|
||
/// 初始化云盘文件详情表单。
|
||
init(file: CloudFile, onSave: @escaping (String) -> Void) {
|
||
self.file = file
|
||
self.onSave = onSave
|
||
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()
|
||
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() {
|
||
view.backgroundColor = .clear
|
||
dimView.backgroundColor = UIColor.black.withAlphaComponent(0.35)
|
||
dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelTapped)))
|
||
|
||
panelView.backgroundColor = .white
|
||
panelView.layer.cornerRadius = 28
|
||
panelView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||
panelView.clipsToBounds = true
|
||
scrollView.alwaysBounceVertical = false
|
||
scrollView.keyboardDismissMode = .interactive
|
||
|
||
view.addSubview(dimView)
|
||
view.addSubview(panelView)
|
||
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() }
|
||
panelView.snp.makeConstraints { make in
|
||
make.leading.trailing.equalToSuperview()
|
||
make.bottom.equalToSuperview()
|
||
make.top.greaterThanOrEqualTo(view.safeAreaLayoutGuide).offset(16)
|
||
make.height.equalTo(590).priority(.high)
|
||
}
|
||
scrollView.snp.makeConstraints { make in
|
||
make.top.leading.trailing.equalToSuperview()
|
||
make.bottom.equalTo(view.safeAreaLayoutGuide)
|
||
}
|
||
contentView.snp.makeConstraints { make in
|
||
make.edges.equalTo(scrollView.contentLayoutGuide)
|
||
make.width.equalTo(scrollView.frameLayoutGuide)
|
||
}
|
||
}
|
||
|
||
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()
|
||
}
|
||
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)
|
||
}
|
||
valueLabel.snp.makeConstraints { make in
|
||
make.leading.trailing.equalToSuperview().inset(12)
|
||
make.centerY.equalToSuperview()
|
||
}
|
||
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() {
|
||
close()
|
||
}
|
||
|
||
@objc private func saveTapped() {
|
||
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)
|
||
}
|
||
}
|
||
|
||
extension CloudDriveDetailSheetViewController: UITextFieldDelegate {
|
||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||
textField.resignFirstResponder()
|
||
return true
|
||
}
|
||
}
|