LoadingViewController.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. //
  2. // LoadingViewController.swift
  3. // QuickSearchLocation
  4. //
  5. // Created by Destiny on 2024/12/31.
  6. //
  7. import UIKit
  8. import Photos
  9. class LoadingViewController: UIViewController {
  10. private let photoClassifier = PhotoClassifier()
  11. private let progressView = UIProgressView(progressViewStyle: .default)
  12. private let statusLabel = UILabel()
  13. private let resultLabel = UILabel()
  14. private let collectionView: UICollectionView
  15. private var similarGroups: [[PHAsset]] = []
  16. private var timer: Timer?
  17. // 添加详细进度标签
  18. private let detailProgressLabel = UILabel()
  19. private let percentageLabel = UILabel()
  20. override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
  21. // 初始化 CollectionView
  22. let layout = UICollectionViewFlowLayout()
  23. layout.scrollDirection = .vertical
  24. layout.minimumLineSpacing = 10
  25. layout.minimumInteritemSpacing = 5
  26. let screenWidth = UIScreen.main.bounds.width
  27. let width = (screenWidth - 20) / 3
  28. layout.itemSize = CGSize(width: width, height: width)
  29. layout.sectionInset = UIEdgeInsets(top: 10, left: 5, bottom: 10, right: 5)
  30. layout.headerReferenceSize = CGSize(width: screenWidth, height: 40)
  31. collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
  32. super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
  33. }
  34. required init?(coder: NSCoder) {
  35. fatalError("init(coder:) has not been implemented")
  36. }
  37. override func viewDidLoad() {
  38. super.viewDidLoad()
  39. setupUI()
  40. startClassifying()
  41. }
  42. private func setupUI() {
  43. view.backgroundColor = .white
  44. // 设置进度容器视图
  45. let progressContainer = UIView()
  46. progressContainer.translatesAutoresizingMaskIntoConstraints = false
  47. view.addSubview(progressContainer)
  48. // 配置所有标签
  49. statusLabel.textAlignment = .center
  50. statusLabel.font = .systemFont(ofSize: 16, weight: .medium)
  51. detailProgressLabel.textAlignment = .center
  52. detailProgressLabel.font = .systemFont(ofSize: 14)
  53. detailProgressLabel.textColor = .darkGray
  54. percentageLabel.textAlignment = .center
  55. percentageLabel.font = .systemFont(ofSize: 14, weight: .medium)
  56. percentageLabel.textColor = .systemBlue
  57. resultLabel.textAlignment = .center
  58. resultLabel.numberOfLines = 0
  59. resultLabel.font = .systemFont(ofSize: 14)
  60. // 配置进度条
  61. progressView.progressTintColor = .systemBlue
  62. progressView.trackTintColor = .systemGray5
  63. // 添加所有视图
  64. [statusLabel, progressView, detailProgressLabel, percentageLabel, resultLabel].forEach {
  65. $0.translatesAutoresizingMaskIntoConstraints = false
  66. progressContainer.addSubview($0)
  67. }
  68. // 布局约束
  69. NSLayoutConstraint.activate([
  70. progressContainer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
  71. progressContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  72. progressContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  73. progressContainer.heightAnchor.constraint(equalToConstant: 180),
  74. statusLabel.topAnchor.constraint(equalTo: progressContainer.topAnchor, constant: 20),
  75. statusLabel.centerXAnchor.constraint(equalTo: progressContainer.centerXAnchor),
  76. progressView.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 15),
  77. progressView.centerXAnchor.constraint(equalTo: progressContainer.centerXAnchor),
  78. progressView.widthAnchor.constraint(equalTo: progressContainer.widthAnchor, multiplier: 0.8),
  79. detailProgressLabel.topAnchor.constraint(equalTo: progressView.bottomAnchor, constant: 10),
  80. detailProgressLabel.centerXAnchor.constraint(equalTo: progressContainer.centerXAnchor),
  81. percentageLabel.topAnchor.constraint(equalTo: detailProgressLabel.bottomAnchor, constant: 5),
  82. percentageLabel.centerXAnchor.constraint(equalTo: progressContainer.centerXAnchor),
  83. resultLabel.topAnchor.constraint(equalTo: percentageLabel.bottomAnchor, constant: 15),
  84. resultLabel.centerXAnchor.constraint(equalTo: progressContainer.centerXAnchor),
  85. resultLabel.widthAnchor.constraint(equalTo: progressContainer.widthAnchor, multiplier: 0.8)
  86. ])
  87. // 设置 CollectionView
  88. collectionView.backgroundColor = .white
  89. collectionView.delegate = self
  90. collectionView.dataSource = self
  91. collectionView.register(PhotoCell.self, forCellWithReuseIdentifier: "PhotoCell")
  92. collectionView.register(
  93. HeaderView.self,
  94. forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
  95. withReuseIdentifier: "Header"
  96. )
  97. collectionView.translatesAutoresizingMaskIntoConstraints = false
  98. view.addSubview(collectionView)
  99. NSLayoutConstraint.activate([
  100. collectionView.topAnchor.constraint(equalTo: progressContainer.bottomAnchor),
  101. collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  102. collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  103. collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
  104. ])
  105. }
  106. private func startClassifying() {
  107. // 重置UI
  108. progressView.progress = 0
  109. statusLabel.text = "正在准备..."
  110. detailProgressLabel.text = "初始化中"
  111. percentageLabel.text = "0%"
  112. resultLabel.text = ""
  113. // 请求相册权限
  114. PHPhotoLibrary.requestAuthorization { [weak self] status in
  115. guard let self = self else { return }
  116. DispatchQueue.main.async {
  117. if status == .authorized {
  118. self.beginPhotoClassification()
  119. } else {
  120. self.statusLabel.text = "需要相册访问权限"
  121. self.detailProgressLabel.text = "请在设置中允许访问相册"
  122. }
  123. }
  124. }
  125. }
  126. private func beginPhotoClassification() {
  127. statusLabel.text = "正在加载照片..."
  128. let fetchOptions = PHFetchOptions()
  129. let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
  130. // 更新总数
  131. detailProgressLabel.text = "共发现 \(allPhotos.count) 张照片"
  132. photoClassifier.classifyPhotos(
  133. assets: allPhotos,
  134. progressHandler: { [weak self] (stage, progress) in
  135. DispatchQueue.main.async {
  136. self?.updateProgress(stage: stage, progress: progress)
  137. }
  138. },
  139. completion: { [weak self] result in
  140. guard let self = self else { return }
  141. DispatchQueue.main.async {
  142. self.updateProgress(stage: "分类完成", progress: 1.0)
  143. // 更新结果显示
  144. var resultText = "分类结果:\n"
  145. resultText += "截图:\(result.screenshots.count) 张\n"
  146. resultText += "相似照片组:\(result.similarPhotos.count) 组"
  147. self.resultLabel.text = resultText
  148. // 更新相似照片展示
  149. self.similarGroups = result.similarPhotos
  150. self.collectionView.reloadData()
  151. }
  152. }
  153. )
  154. }
  155. private func updateProgress(stage: String, progress: Float) {
  156. statusLabel.text = stage
  157. progressView.progress = progress
  158. percentageLabel.text = "\(Int(progress * 100))%"
  159. switch stage {
  160. case "正在加载照片...":
  161. detailProgressLabel.text = "正在读取照片数据"
  162. case "正在提取特征...":
  163. detailProgressLabel.text = "正在分析照片内容"
  164. case "正在比较相似度...":
  165. detailProgressLabel.text = "正在查找相似照片"
  166. case "分类完成":
  167. detailProgressLabel.text = "处理完成"
  168. default:
  169. break
  170. }
  171. }
  172. }
  173. // MARK: - UICollectionView DataSource & Delegate
  174. extension LoadingViewController: UICollectionViewDataSource, UICollectionViewDelegate {
  175. func numberOfSections(in collectionView: UICollectionView) -> Int {
  176. return similarGroups.count
  177. }
  178. func collectionView(_ collectionView: UICollectionView,
  179. numberOfItemsInSection section: Int) -> Int {
  180. return similarGroups[section].count
  181. }
  182. func collectionView(_ collectionView: UICollectionView,
  183. cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  184. let cell = collectionView.dequeueReusableCell(
  185. withReuseIdentifier: "PhotoCell",
  186. for: indexPath
  187. ) as! PhotoCell
  188. let asset = similarGroups[indexPath.section][indexPath.item]
  189. cell.configure(with: asset)
  190. return cell
  191. }
  192. func collectionView(_ collectionView: UICollectionView,
  193. viewForSupplementaryElementOfKind kind: String,
  194. at indexPath: IndexPath) -> UICollectionReusableView {
  195. if kind == UICollectionView.elementKindSectionHeader {
  196. let header = collectionView.dequeueReusableSupplementaryView(
  197. ofKind: kind,
  198. withReuseIdentifier: "Header",
  199. for: indexPath
  200. ) as! HeaderView
  201. let groupCount = similarGroups[indexPath.section].count
  202. header.titleLabel.text = "相似组 \(indexPath.section + 1) (\(groupCount) 张)"
  203. return header
  204. }
  205. return UICollectionReusableView()
  206. }
  207. }
  208. // MARK: - PhotoCell
  209. class PhotoCell: UICollectionViewCell {
  210. private let imageView = UIImageView()
  211. override init(frame: CGRect) {
  212. super.init(frame: frame)
  213. setupUI()
  214. }
  215. required init?(coder: NSCoder) {
  216. fatalError("init(coder:) has not been implemented")
  217. }
  218. private func setupUI() {
  219. imageView.contentMode = .scaleAspectFill
  220. imageView.clipsToBounds = true
  221. imageView.translatesAutoresizingMaskIntoConstraints = false
  222. contentView.addSubview(imageView)
  223. NSLayoutConstraint.activate([
  224. imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
  225. imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
  226. imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
  227. imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
  228. ])
  229. }
  230. func configure(with asset: PHAsset) {
  231. let options = PHImageRequestOptions()
  232. options.deliveryMode = .fastFormat
  233. PHImageManager.default().requestImage(
  234. for: asset,
  235. targetSize: CGSize(width: 200, height: 200),
  236. contentMode: .aspectFill,
  237. options: options
  238. ) { [weak self] image, _ in
  239. self?.imageView.image = image
  240. }
  241. }
  242. }
  243. // MARK: - HeaderView
  244. class HeaderView: UICollectionReusableView {
  245. let titleLabel = UILabel()
  246. override init(frame: CGRect) {
  247. super.init(frame: frame)
  248. setupUI()
  249. }
  250. required init?(coder: NSCoder) {
  251. fatalError("init(coder:) has not been implemented")
  252. }
  253. private func setupUI() {
  254. titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
  255. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  256. addSubview(titleLabel)
  257. NSLayoutConstraint.activate([
  258. titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15),
  259. titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
  260. ])
  261. }
  262. }