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