Files
suixinkan_uikit/suixinkan/UI/CloudDrive/CloudDriveDetailSheetViewController.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
}
}