Files
suixinkan_uikit/suixinkan/UI/CloudDrive/CloudDriveDetailSheetViewController.swift

307 lines
12 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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
}
}