Commit 9100f541 authored by CZ1004's avatar CZ1004

压缩更新

parent 01da70c0
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import Photos
typealias CompressSelectCellCallback = (ResourceModel,Bool)->Void typealias CompressSelectCellCallback = (ResourceModel,Bool)->Void
...@@ -13,18 +14,46 @@ class CompressSelectCell : UICollectionViewCell { ...@@ -13,18 +14,46 @@ class CompressSelectCell : UICollectionViewCell {
var callBack : CompressSelectCellCallback = {model,choose in} var callBack : CompressSelectCellCallback = {model,choose in}
var currentMediaType : Int = 0
var model : ResourceModel? { var model : ResourceModel? {
didSet{ didSet{
guard let model = self.model else {return} guard let model = self.model else {return}
let image = PhotoAndVideoMananger.mananger.getImageFromAssetID(id: model.ident) self.backImageView.image = nil
self.backImageView.image = image let viewModel = CompressViewModel()
viewModel.getImageFromAssetIdentifier(identifier: model.ident) {[weak self] image in
guard let self else { return}
DispatchQueue.main.async {
self.backImageView.image = image
}
}
// 把压缩前的值减去压缩后的值就为可以节省的值。然后这里需要判定下如果是大于1000MB,则再除以1024换算成GB // 把压缩前的值减去压缩后的值就为可以节省的值。然后这里需要判定下如果是大于1000MB,则再除以1024换算成GB
let saveSize = model.orgSize - model.compressSize let options = PHImageRequestOptions()
if saveSize > 1000 { options.isSynchronous = false
self.saveSizeLabel.text = String(format: "Save %.2f GB" ,saveSize/1024.0) options.deliveryMode = .highQualityFormat
}else{ let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [model.ident], options: nil)
self.saveSizeLabel.text = String(format: "Save %.2f MB" ,saveSize)
PHImageManager.default().requestImage(for: fetchResult.firstObject!, targetSize: PHImageManagerMaximumSize, contentMode:.aspectFit, options: options) { (image, _) in
if let originalImage = image {
// 项目中用到的是【0.2、0.5和0.8】,这里我们初始化的时候使用0.2去计算
if let compressedData = originalImage.jpegData(compressionQuality: 0.2) {
let compressCompletedSize = Double(compressedData.count)
let saveSize = model.orgSize - compressCompletedSize
let sizeKB : Double = saveSize/1024
DispatchQueue.main.async {
if sizeKB < 1024{
self.saveSizeLabel.text = String(format: "Save %.2f KB" ,sizeKB)
}else if sizeKB < (1024 * 1024) && sizeKB > 1024{
self.saveSizeLabel.text = String(format: "Save %.2f MB" ,sizeKB/1024)
}else{
self.saveSizeLabel.text = String(format: "Save %.2f GB" ,sizeKB/(1024 * 1024))
}
}
}
}
} }
} }
} }
...@@ -96,7 +125,12 @@ class CompressSelectCell : UICollectionViewCell { ...@@ -96,7 +125,12 @@ class CompressSelectCell : UICollectionViewCell {
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
self.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer()
tap.addTarget(self, action: #selector(imageClick))
self.addGestureRecognizer(tap)
self.addSubview(self.backImageView) self.addSubview(self.backImageView)
self.addSubview(self.saveSizeView) self.addSubview(self.saveSizeView)
...@@ -110,14 +144,14 @@ class CompressSelectCell : UICollectionViewCell { ...@@ -110,14 +144,14 @@ class CompressSelectCell : UICollectionViewCell {
self.saveSizeView.snp.makeConstraints { make in self.saveSizeView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(12) make.left.equalToSuperview().offset(12)
make.bottom.equalToSuperview().offset(-12) make.bottom.equalToSuperview().offset(-12)
make.height.equalTo(48) make.height.equalTo(25)
make.width.equalTo(120) make.width.equalTo(120)
} }
self.saveSizeLabel.snp.makeConstraints { make in self.saveSizeLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(8) make.left.equalToSuperview().offset(8)
make.centerY.equalToSuperview() make.centerY.equalToSuperview()
make.height.equalTo(48) make.height.equalTo(25)
make.width.equalTo(120) make.width.equalTo(105)
} }
self.moreImageView.snp.makeConstraints { make in self.moreImageView.snp.makeConstraints { make in
...@@ -139,4 +173,18 @@ class CompressSelectCell : UICollectionViewCell { ...@@ -139,4 +173,18 @@ class CompressSelectCell : UICollectionViewCell {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@objc func imageClick(){
if self.currentMediaType == 0 {
// 如果是图片
let vc : PreViewController = PreViewController()
vc.imageIdent = self.model!.ident
self.responderViewController()?.navigationController?.pushViewController(vc, animated: true)
}else{
// 如果是视频
let vc : PreVideoController = PreVideoController(localIdentifier: self.model!.ident)
self.responderViewController()?.navigationController?.pushViewController(vc, animated: true)
}
}
} }
...@@ -6,11 +6,18 @@ ...@@ -6,11 +6,18 @@
// //
import Foundation import Foundation
import Photos
class CompressCompletedViewController : BaseViewController{ class CompressCompletedViewController : BaseViewController{
var model : [ResourceModel]? var model : [ResourceModel]?
var comDataSource : [Data] = []
var comVideoDataSource : [URL?] = []
var currentMediaType : Int = 0
lazy var imageView: UIImageView = { lazy var imageView: UIImageView = {
let imageView = UIImageView() let imageView = UIImageView()
imageView.clipsToBounds = true imageView.clipsToBounds = true
...@@ -248,24 +255,90 @@ class CompressCompletedViewController : BaseViewController{ ...@@ -248,24 +255,90 @@ class CompressCompletedViewController : BaseViewController{
} }
@objc func completedAction(){ @objc func completedAction(){
if currentMediaType == 0 {
// 将压缩后的照片存到相册
for imageData in self.comDataSource {
PHPhotoLibrary.shared().performChanges({
let creationRequest = PHAssetCreationRequest.forAsset()
creationRequest.addResource(with: .photo, data: imageData, options: nil)
}) { success, error in
if(success){
print("保存照片成功")
}else {
if let error = error {
print("保存相片时出错: \(error.localizedDescription)")
}
}
}
}
}else{
for item in self.comVideoDataSource {
guard let item else {
print("保存视频失败,URL为空")
self.jumpToCompressVC()
return
}
// 保存视频到相册
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: item as URL)
}) { (success, saveError) in
if success {
print("保存视频成功")
}else{
if let error = saveError {
print("保存视频时出错: \(error.localizedDescription)")
}
}
}
}
}
// 提示是否删除照片 // 删除文件逻辑【系统自动提示是否删除】
let compressTipView : CompressTipView = CompressTipView(frame: self.view.bounds) var count = 0
self.view.addSubview(compressTipView) for data in self.model! {
compressTipView.callBack = {[weak self] allow in let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [data.ident], options: nil)
guard let self else {return} let assetToDelete = fetchResult.firstObject
PHPhotoLibrary.shared().performChanges ({
PHAssetChangeRequest.deleteAssets([assetToDelete] as NSFastEnumeration)
}){ success, error in
if(success){
self.updateCompressData(flag: data.ident)
print("删除文件成功")
}else {
if let error = error {
print("删除文件时出错: \(error.localizedDescription)")
}
}
count = count + 1
if count == self.model?.count{
self.jumpToCompressVC()
}
}
}
}
func jumpToCompressVC(){
DispatchQueue.main.async {
if let targetVC = self.navigationController?.viewControllers.first(where: { $0 is CompressController }) { if let targetVC = self.navigationController?.viewControllers.first(where: { $0 is CompressController }) {
self.navigationController?.popToViewController(targetVC, animated: true) self.navigationController?.popToViewController(targetVC, animated: true)
} }
// 如果不允许就两张都放进去,否则异步删除照片
if allow as! Bool == false {
// 删除照片逻辑
}
} }
} }
/// 更新数据
/// - Parameter flag: 图片的ident
func updateCompressData(flag : String){
DispatchQueue.main.async {
// 移除VC中的数据
let compressVC = self.navigationController?.viewControllers.first(where: { $0 is CompressController }) as! CompressController as CompressController
compressVC.resourceData.removeAll { $0.ident == flag }
// 移除单利中的数据
Singleton.shared.resourceModel.removeAll{ $0.ident == flag }
}
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
......
...@@ -96,19 +96,42 @@ class CompressController : BaseViewController { ...@@ -96,19 +96,42 @@ class CompressController : BaseViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .white view.backgroundColor = .white
self.navigationController?.navigationBar.isHidden = true self.navigationController?.navigationBar.isHidden = true
getViewData()
setUI() setUI()
} }
/// 获取当前页面数据
func getViewData(){
self.resourceData.removeAll()
let datas = Singleton.shared.resourceModel
if datas.count > 0 {
// 这里需要重新排序下
if self.currentSort == 0 {
self.resourceData = datas.sorted {$0.orgSize > $1.orgSize }
}else if self.currentSort == 1 {
self.resourceData = datas.sorted {$0.orgSize < $1.orgSize }
}else if self.currentSort == 2 {
self.resourceData = datas.sorted {$0.createDate > $1.createDate }
}else{
self.resourceData = datas.sorted {$0.createDate < $1.createDate }
}
}else{
CompressViewModel().getAllPhotosToAssets(sortType: self.currentSort, assetType: self.currentResourceType) { [weak self] models in
guard let self else {return}
self.resourceData = models
}
}
}
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
// 这里默认去请求下当前的数据资源
let viewModel = CompressViewModel() // 目的是为了消除cell 的选择按钮状态
viewModel.getAllPhotos {[weak self] models in if self.selectedModel.count == 0 {
guard let self else {return} self.collectionView.reloadData()
self.resourceData = models
} }
} }
} }
...@@ -130,6 +153,7 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo ...@@ -130,6 +153,7 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CompressSelectCell", for: indexPath) as! CompressSelectCell let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CompressSelectCell", for: indexPath) as! CompressSelectCell
cell.model = self.resourceData[indexPath.row] cell.model = self.resourceData[indexPath.row]
cell.currentMediaType = self.currentResourceType
if self.selectedModel.count == 0 { if self.selectedModel.count == 0 {
cell.choose = false cell.choose = false
} }
...@@ -207,6 +231,23 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo ...@@ -207,6 +231,23 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo
self.sortByType(sortType: self.currentSort, header: header) self.sortByType(sortType: self.currentSort, header: header)
} }
} }
header.changeView.callBack = {[weak self] flag in
guard let self else {return}
if self.currentResourceType != flag as! Int {
self.currentResourceType = flag as! Int
// 先移除下,防止点到
self.resourceData.removeAll()
// 如果是图片,直接从缓存中加载
if self.currentResourceType == 0 {
self.getViewData()
}else{
CompressViewModel().getAllPhotosToAssets(sortType: self.currentSort, assetType: flag as! Int) { [weak self] models in
guard let self else {return}
self.resourceData = models
}
}
}
}
header.modeData = self.resourceData header.modeData = self.resourceData
return header return header
}else{ }else{
...@@ -275,6 +316,7 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo ...@@ -275,6 +316,7 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo
// 先将值传到下一个页面 // 先将值传到下一个页面
let vc : CompressQualityController = CompressQualityController() let vc : CompressQualityController = CompressQualityController()
vc.model = self.selectedModel vc.model = self.selectedModel
vc.currentMediaType = self.currentResourceType
vc.detailTiplabel.text = "You've selected \(self.selectedModel.count) out of \(self.resourceData.count) photos to compress." vc.detailTiplabel.text = "You've selected \(self.selectedModel.count) out of \(self.resourceData.count) photos to compress."
self.navigationController?.pushViewController(vc, animated: true) self.navigationController?.pushViewController(vc, animated: true)
...@@ -288,4 +330,6 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo ...@@ -288,4 +330,6 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo
self.selectedModel.removeAll() self.selectedModel.removeAll()
self.updateSubmitButton() self.updateSubmitButton()
} }
} }
...@@ -20,6 +20,8 @@ class CompressQualityController : BaseViewController{ ...@@ -20,6 +20,8 @@ class CompressQualityController : BaseViewController{
var currentQulityType : Int = 0 var currentQulityType : Int = 0
var currentMediaType : Int = 0
private var compressNav:CompressNavView? private var compressNav:CompressNavView?
...@@ -179,6 +181,40 @@ class CompressQualityController : BaseViewController{ ...@@ -179,6 +181,40 @@ class CompressQualityController : BaseViewController{
} }
} }
fileprivate func updateNextView(_ compressAllSize: Double, _ compressingView: CompressingView,_ comDataSource : [Data],_ comVideoDataSource : [URL?]) {
DispatchQueue.main.async {
compressingView.removeFromSuperview()
let vc = CompressCompletedViewController()
vc.currentMediaType = self.currentMediaType
vc.comDataSource = comDataSource
vc.comVideoDataSource = comVideoDataSource
vc.model = self.model
vc.detailTipToplabel.text = "\(self.model!.count) items"
vc.detailTipBottomlabel.text = "\(self.model!.count) items"
var sum = 0.0
var orgAllSize = 0.0
for modelData in self.model! {
orgAllSize = orgAllSize + modelData.orgSize
}
sum = orgAllSize - compressAllSize
sum = sum / 1024
if sum < 1024{
vc.sizeToplabel.text = String(format:"%.2lfKB",sum)
}else if sum < (1024 * 1024) && sum > 1024{
sum = sum / 1024
vc.sizeToplabel.text = String(format:"%.2lfMB",sum)
}else{
sum = sum / (1024 * 1024)
vc.sizeToplabel.text = String(format:"%.2lfGB",sum)
}
let str = String(format:"%.2lf",(orgAllSize - compressAllSize) / orgAllSize)
vc.sizeBottomlabel.text = "\(str)%"
self.navigationController?.pushViewController(vc, animated: true)
}
}
@objc func submitAction(){ @objc func submitAction(){
let compressingView : CompressingView = CompressingView(frame: self.view.bounds) let compressingView : CompressingView = CompressingView(frame: self.view.bounds)
...@@ -195,49 +231,55 @@ class CompressQualityController : BaseViewController{ ...@@ -195,49 +231,55 @@ class CompressQualityController : BaseViewController{
if currentQulityType == 2 { if currentQulityType == 2 {
currentQulity = 0.8 currentQulity = 0.8
} }
manager.compress(assets: self.model!, compressionQuality: currentQulity) {progress in var comDataSource : [Data] = []
compressingView.animationView.setProgress(CGFloat(progress), animated: false, duration: 0.1)
} completion: { compressedDataArray, errorArray in if self.currentMediaType == 0 {
// 表示压缩图片
for (index, data) in compressedDataArray.enumerated() { manager.compress(assets: self.model!, compressionQuality: currentQulity) {progress in
if let error = errorArray[index] { compressingView.animationView.setProgress(CGFloat(progress), animated: false, duration: 0.1)
print("第 \(index + 1) 张图片压缩出错: \(error.localizedDescription)") } completion: { compressedDataArray, errorArray in
} else if let data = data {
print("第 \(index + 1) 张图片压缩完成,压缩后大小: \(data.count) 字节")
} else {
print("第 \(index + 1) 张图片压缩失败")
}
}
DispatchQueue.main.async {
compressingView.removeFromSuperview()
let vc = CompressCompletedViewController()
vc.model = self.model
vc.detailTipToplabel.text = "\(self.model!.count) items"
vc.detailTipBottomlabel.text = "\(self.model!.count) items"
var sum = 0.0
var orgAllSize = 0.0
var compressAllSize = 0.0 var compressAllSize = 0.0
for modelData in self.model! { for (index, data) in compressedDataArray.enumerated() {
orgAllSize = orgAllSize + modelData.orgSize if let error = errorArray[index] {
compressAllSize = compressAllSize + modelData.compressSize print("第 \(index + 1) 个文件压缩出错: \(error.localizedDescription)")
sum = sum + modelData.orgSize - modelData.compressSize } else if let data = data {
print("第 \(index + 1) 个文件压缩完成,压缩后大小: \(data.count) 字节")
compressAllSize = compressAllSize + Double(data.count)
comDataSource.append(data)
} else {
print("第 \(index + 1) 个文件压缩失败")
}
} }
if sum > 1000 { self.updateNextView(compressAllSize,compressingView,comDataSource,[])
sum = sum / 1024 }
vc.sizeToplabel.text = "\(sum)GB" }else{
}else{ // 压缩视频
vc.sizeToplabel.text = "\(sum)MB" var compressAllSize : Double = 0.0
manager.compressVideos(models: self.model!, quality: Float(currentQulity)) { (identifier, progress) in
compressingView.animationView.setProgress(CGFloat(progress), animated: false, duration: 0.1)
} completion: { (outputURLs, errors) in
for (index, outputURL) in outputURLs.enumerated() {
if let outputURL = outputURL {
do {
let attributes = try FileManager.default.attributesOfItem(atPath: outputURL.path)
if let fileSize = attributes[.size] as? Int64 {
compressAllSize = compressAllSize + Double(fileSize)
}
} catch {
Print("获取视频文件大小失败")
}
print("Compressed video \(index) saved at: \(outputURL)")
} else if let error = errors[index] {
print("Error compressing video \(index): \(error.localizedDescription)")
}
} }
let str = String(format:"%.2lf",(orgAllSize - compressAllSize) / orgAllSize) self.updateNextView(compressAllSize,compressingView,[],outputURLs)
vc.sizeBottomlabel.text = "\(str)%"
self.navigationController?.pushViewController(vc, animated: true)
} }
} }
} }
} }
//
// PreVideoController.swift
// PhoneManager
//
// Created by 赵前 on 2025/4/6.
//
import Foundation
import AVFoundation
import Photos
class PreVideoController : BaseViewController {
private var player: AVPlayer?
private var playerLayer: AVPlayerLayer?
private let localIdentifier: String
init(localIdentifier: String) {
self.localIdentifier = localIdentifier
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupVideoPlayer()
setupTapGesture()
}
private func setupVideoPlayer() {
guard let asset = fetchAsset() else { return }
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { [weak self] (avAsset, _, _) in
guard let self = self, let avAsset = avAsset as? AVURLAsset else { return }
DispatchQueue.main.async {
self.player = AVPlayer(url: avAsset.url)
self.playerLayer = AVPlayerLayer(player: self.player)
self.playerLayer?.frame = CGRect(x: 0, y: self.titleView.height, width: self.view.width, height: self.view.height - self.titleView.height)
self.view.layer.addSublayer(self.playerLayer!)
self.player?.play()
}
}
}
private func fetchAsset() -> PHAsset? {
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil)
return fetchResult.firstObject
}
private func setupTapGesture() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
view.addGestureRecognizer(tapGesture)
}
@objc private func handleTap() {
if presentedViewController != nil {
dismiss(animated: true, completion: nil)
} else {
navigationController?.popViewController(animated: true)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
player?.pause()
}
}
//
// PreViewController.swift
// PhoneManager
//
// Created by 赵前 on 2025/4/6.
//
import Foundation
class PreViewController : BaseViewController {
var imageIdent : String = ""
lazy var preImageView : UIImageView = {
let view = UIImageView(frame: CGRect(x: 0, y: self.titleView.height, width: self.view.width, height: self.view.height - self.titleView.height))
view.backgroundColor = .white
view.image = PhotoAndVideoMananger.mananger.getImageFromAssetID(id: self.imageIdent)
view.contentMode = .scaleAspectFit
view.clipsToBounds = true
view.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer()
tap.addTarget(self, action: #selector(selectClick))
view.addGestureRecognizer(tap)
return view
}()
@objc func selectClick(){
self.navigationController?.popViewController(animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.preImageView)
}
}
...@@ -10,16 +10,12 @@ import Photos ...@@ -10,16 +10,12 @@ import Photos
struct ResourceModel : Equatable{ struct ResourceModel : Equatable{
var ident : String var ident : String
var compressSize : Double
var orgSize: Double var orgSize: Double
var createDate : Date var createDate : Date
var imageAsset: PHAsset
init(ident: String, compressSize: Double, orgSize: Double, createDate: Date, imageAsset: PHAsset) { init(ident: String, orgSize: Double, createDate: Date) {
self.ident = ident self.ident = ident
self.compressSize = compressSize
self.orgSize = orgSize self.orgSize = orgSize
self.createDate = createDate self.createDate = createDate
self.imageAsset = imageAsset
} }
} }
...@@ -19,18 +19,24 @@ class CompressCustomHeaderView: UICollectionReusableView{ ...@@ -19,18 +19,24 @@ class CompressCustomHeaderView: UICollectionReusableView{
var saveSum = 0.0 var saveSum = 0.0
for model in self.modeData{ for model in self.modeData{
sum = sum + model.orgSize sum = sum + model.orgSize
saveSum = saveSum + (model.orgSize - model.compressSize) saveSum = saveSum + model.orgSize * 0.8
} }
if sum > 1000 { sum = sum / 1000
self.siezLabel.text = String(format: "%.2f GB" ,(sum/1024)) saveSum = saveSum / 1024
if sum < 1024 {
self.siezLabel.text = String(format: "%.2f KB" ,(sum))
}else if sum < (1024 * 1024) && sum > 1024{
self.siezLabel.text = String(format: "%.2f MB" ,(sum/1024))
}else{ }else{
self.siezLabel.text = String(format: "%.2f MB" ,(sum)) self.siezLabel.text = String(format: "%.2f GB" ,sum/(1024*1024))
} }
if saveSum > 1000 { if saveSum < 1024 {
self.saveSizeLabel.text = String(format: "%.2f GB" ,(saveSum/1024.0)) self.saveSizeLabel.text = String(format: "%.2f KB" ,(saveSum))
}else { }else if saveSum < (1024 * 1024) && saveSum > 1024{
self.saveSizeLabel.text = String(format: "%.2f MB" ,(saveSum)) self.saveSizeLabel.text = String(format: "%.2f MB" ,(saveSum/1024))
}else{
self.saveSizeLabel.text = String(format: "%.2f GB" ,saveSum/(1024*1024))
} }
} }
...@@ -152,7 +158,7 @@ class CompressCustomHeaderView: UICollectionReusableView{ ...@@ -152,7 +158,7 @@ class CompressCustomHeaderView: UICollectionReusableView{
self.selectlabel.snp.makeConstraints { make in self.selectlabel.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-12) make.right.equalToSuperview().offset(-12)
make.top.equalToSuperview().offset(6) make.top.equalToSuperview().offset(6)
make.width.equalTo(51 * RScreenW()) make.width.equalTo(60 * RScreenW())
make.height.equalTo(20) make.height.equalTo(20)
} }
self.siezLabel.snp.makeConstraints { make in self.siezLabel.snp.makeConstraints { make in
...@@ -196,6 +202,7 @@ class CompressCustomHeaderView: UICollectionReusableView{ ...@@ -196,6 +202,7 @@ class CompressCustomHeaderView: UICollectionReusableView{
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
self.backgroundColor = .white
setUI() setUI()
......
...@@ -98,6 +98,12 @@ class CompressSortView : UIView,UITableViewDelegate,UITableViewDataSource { ...@@ -98,6 +98,12 @@ class CompressSortView : UIView,UITableViewDelegate,UITableViewDataSource {
lazy var backView : UIView = { lazy var backView : UIView = {
let view = UIView() let view = UIView()
view.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5000) view.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5000)
view.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer()
tap.addTarget(self, action: #selector(backViewClick))
view.addGestureRecognizer(tap)
return view return view
}() }()
...@@ -154,6 +160,9 @@ class CompressSortView : UIView,UITableViewDelegate,UITableViewDataSource { ...@@ -154,6 +160,9 @@ class CompressSortView : UIView,UITableViewDelegate,UITableViewDataSource {
// 移除自身 // 移除自身
self.removeFromSuperview() self.removeFromSuperview()
} }
@objc func backViewClick(){
// 移除自身
self.removeFromSuperview()
}
} }
...@@ -9,6 +9,8 @@ import Foundation ...@@ -9,6 +9,8 @@ import Foundation
class CompressSwitchView : UIView { class CompressSwitchView : UIView {
var callBack: callBack<Any> = {flag in }
var currentButton : UIButton? var currentButton : UIButton?
lazy var leftButton : UIButton = { lazy var leftButton : UIButton = {
...@@ -18,6 +20,7 @@ class CompressSwitchView : UIView { ...@@ -18,6 +20,7 @@ class CompressSwitchView : UIView {
button.setTitle("Photos", for: .normal) button.setTitle("Photos", for: .normal)
button.addTarget(self, action: #selector(selectedButtonAction(_:)), for: .touchUpInside) button.addTarget(self, action: #selector(selectedButtonAction(_:)), for: .touchUpInside)
button.setTitleColor(UIColor(red: 0.7, green: 0.7, blue: 0.7, alpha: 1), for: .normal) button.setTitleColor(UIColor(red: 0.7, green: 0.7, blue: 0.7, alpha: 1), for: .normal)
button.tag = 1000
return button return button
}() }()
...@@ -28,6 +31,7 @@ class CompressSwitchView : UIView { ...@@ -28,6 +31,7 @@ class CompressSwitchView : UIView {
button.setTitle("Videos", for: .normal) button.setTitle("Videos", for: .normal)
button.addTarget(self, action: #selector(selectedButtonAction(_:)), for: .touchUpInside) button.addTarget(self, action: #selector(selectedButtonAction(_:)), for: .touchUpInside)
button.setTitleColor(UIColor(red: 0.7, green: 0.7, blue: 0.7, alpha: 1), for: .normal) button.setTitleColor(UIColor(red: 0.7, green: 0.7, blue: 0.7, alpha: 1), for: .normal)
button.tag = 1001
return button return button
}() }()
...@@ -74,6 +78,8 @@ class CompressSwitchView : UIView { ...@@ -74,6 +78,8 @@ class CompressSwitchView : UIView {
sender.setTitleColor(UIColor(red: 0, green: 0.51, blue: 1, alpha: 1), for: .normal) sender.setTitleColor(UIColor(red: 0, green: 0.51, blue: 1, alpha: 1), for: .normal)
sender.backgroundColor = .white sender.backgroundColor = .white
self.currentButton = sender self.currentButton = sender
self.callBack(sender.tag - 1000)
} }
} }
...@@ -86,7 +86,7 @@ class CompressTipView : UIView { ...@@ -86,7 +86,7 @@ class CompressTipView : UIView {
lazy var tipLabel: UILabel = { lazy var tipLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.text = "Allow ”Al Cleaner“ to delete2 photos?" label.text = "Allow ”Al Cleaner“ to delete 2 photos?"
label.textAlignment = .center label.textAlignment = .center
label.numberOfLines = 0 label.numberOfLines = 0
label.font = UIFont.systemFont(ofSize: 17, weight: .bold) label.font = UIFont.systemFont(ofSize: 17, weight: .bold)
......
...@@ -8,70 +8,122 @@ ...@@ -8,70 +8,122 @@
import Foundation import Foundation
import Photos import Photos
typealias Finished = ([ResourceModel])->Void typealias Finished = ([ResourceModel])->Void
struct PhotoInfo {
var localIdentifier : String
}
class CompressViewModel{ class CompressViewModel{
/// 校验相册权限
func checkAndRequestPhotoLibraryPermission() {
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
switch status {
case .authorized:
print("已授权访问相册")
case .notDetermined:
PHPhotoLibrary.requestAuthorization(for: .readWrite) { newStatus in
if newStatus == .authorized {
print("用户已授权访问相册")
// 在这里执行需要相册权限的操作
} else {
print("用户拒绝授权访问相册")
self.showPermissionAlert()
}
}
case .restricted, .denied:
print("用户未授权或受限访问相册")
self.showPermissionAlert()
case .limited:
self.showPermissionAlert()
@unknown default:
print("出现未知的权限状态")
}
}
func processData(finised: @escaping() -> Void) { func showPermissionAlert() {
// 模拟异步操作 let alertController = UIAlertController(
DispatchQueue.global().async { title: "权限请求",
// 模拟耗时操作 message: "为了使用此功能,需要访问您的相册。请在设置中授予相册访问权限。",
Thread.sleep(forTimeInterval: 2) preferredStyle: .alert
finised() )
let settingsAction = UIAlertAction(title: "前往设置", style: .default) { _ in
if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil)
}
}
let cancelAction = UIAlertAction(title: "取消", style: .cancel, handler: nil)
alertController.addAction(settingsAction)
alertController.addAction(cancelAction)
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first {
window.rootViewController?.present(alertController, animated: true, completion: nil)
} }
} }
/// 获取所有图片信息
/// 获取相册所有图片
/// - Returns: 图片信息 /// - Returns: 图片信息
func getAllPhotos(_ finished : @escaping Finished){ func getAllPhotosToAssets(sortType: Int, assetType : Int,_ finished: @escaping Finished){
var models : [ResourceModel] = []
let fetchOptions = PHFetchOptions() let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let photosAssets = PHAsset.fetchAssets(with: .image, options: fetchOptions) var photosAssets : PHFetchResult<PHAsset>
if assetType == 0 {
photosAssets = PHAsset.fetchAssets(with: .image, options: fetchOptions)
}else{
photosAssets = PHAsset.fetchAssets(with: .video, options: fetchOptions)
}
// 获取到了所有图片的assets // 获取到了所有图片的assets
let assetsArray = photosAssets.objects(at: IndexSet(0..<photosAssets.count)) let assetsArray = photosAssets.objects(at: IndexSet(0..<photosAssets.count))
let group = DispatchGroup()
// 初始化一个model数组 var count = 0
var models : [ResourceModel] = []
let options = PHImageRequestOptions()
options.isSynchronous = false
options.deliveryMode = .highQualityFormat
for asset in assetsArray { for asset in assetsArray {
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) { (imageData, _, _, _) in group.enter()
if let data = imageData { DispatchQueue.global().async {
// 图片大小 // 图片大小
let sizeInBytes = Double(data.count) let resources = PHAssetResource.assetResources(for: asset)
let sizeInMB = sizeInBytes / (1024 * 1024) var assetSize : Int = 0
for resource in resources {
// 获取图片的日期 if let fileSize = resource.value(forKey: "fileSize") as? Int64 {
let creationDate = asset.creationDate assetSize += Int(fileSize)
// 获取图片的localIdentifier
let localIdentifier = asset.localIdentifier
// 计算压缩后的大小
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode:.aspectFit, options: options) { (image, _) in
if let originalImage = image {
// 项目中用到的是【0.2、0.5和0.8】,这里我们初始化的时候使用0.2去计算
if let compressedData = originalImage.jpegData(compressionQuality: 0.2) {
let compressCompletedSize = Double(compressedData.count) / (1024 * 1024)
// 构建model
let model = ResourceModel(ident: localIdentifier, compressSize: compressCompletedSize, orgSize: sizeInMB, createDate: creationDate!,imageAsset: asset)
models.append(model)
finished(models)
}
}
} }
} }
let sizeInMB = assetSize
// 获取图片的日期
let creationDate = asset.creationDate!
// 获取图片的localIdentifier
let localIdentifier = asset.localIdentifier
let model = ResourceModel.init(ident: localIdentifier, orgSize: Double(sizeInMB), createDate: creationDate)
models.append(model)
count = count + 1
group.leave()
if count == assetsArray.count {
// 默认按照文件大小排序
if sortType == 0 {
finished(models.sorted { $0.orgSize > $1.orgSize })
}else if sortType == 1 {
finished(models.sorted { $0.orgSize < $1.orgSize })
}else if sortType == 2 {
finished(models.sorted { $0.createDate > $1.createDate })
}else{
finished(models.sorted { $0.createDate < $1.createDate })
}
}
} }
} }
} }
/// 对资源进行排序 /// 对资源进行排序
/// - Parameters: /// - Parameters:
/// - resource: 原资源 /// - resource: 原资源
...@@ -96,6 +148,163 @@ class CompressViewModel{ ...@@ -96,6 +148,163 @@ class CompressViewModel{
/// 视频压缩
/// - Parameters:
/// - assetIdentifiers: 视频文件标识
/// - quality: 要锁质量
/// - progress: 进度回调
/// - completion: 完成回调
func compressVideos(models: [ResourceModel], quality: Float,
progress: @escaping (String, Float) -> Void,
completion: @escaping ([URL?], [Error?]) -> Void) {
var outputURLs: [URL?] = Array(repeating: nil, count: models.count)
var errors: [Error?] = Array(repeating: nil, count: models.count)
var completedCount = 0
func processNextVideo(index: Int) {
guard index < models.count else {
completion(outputURLs, errors)
return
}
let model = models[index]
let fetchOptions = PHFetchOptions()
let assets = PHAsset.fetchAssets(withLocalIdentifiers: [model.ident], options: fetchOptions)
guard let asset = assets.firstObject else {
errors[index] = NSError(domain: "VideoCompressor", code: 1, userInfo: [NSLocalizedDescriptionKey: "Asset not found"])
completedCount += 1
processNextVideo(index: index + 1)
return
}
let options = PHVideoRequestOptions()
options.version = .current
options.deliveryMode = .highQualityFormat
PHImageManager.default().requestAVAsset(forVideo: asset, options: options) { (avAsset, audioMix, info) in
guard let avAsset = avAsset as? AVURLAsset else {
errors[index] = NSError(domain: "VideoCompressor", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to get AVURLAsset"])
completedCount += 1
processNextVideo(index: index + 1)
return
}
let composition = AVMutableComposition()
guard let videoTrack = avAsset.tracks(withMediaType: .video).first else {
errors[index] = NSError(domain: "VideoCompressor", code: 6, userInfo: [NSLocalizedDescriptionKey: "No video track found"])
completedCount += 1
processNextVideo(index: index + 1)
return
}
let compositionVideoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
do {
try compositionVideoTrack?.insertTimeRange(CMTimeRangeMake(start: .zero, duration: avAsset.duration), of: videoTrack, at: .zero)
} catch {
errors[index] = error
completedCount += 1
processNextVideo(index: index + 1)
return
}
let audioTracks = avAsset.tracks(withMediaType: .audio)
if let audioTrack = audioTracks.first {
let compositionAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
do {
try compositionAudioTrack?.insertTimeRange(CMTimeRangeMake(start: .zero, duration: avAsset.duration), of: audioTrack, at: .zero)
} catch {
errors[index] = error
completedCount += 1
processNextVideo(index: index + 1)
return
}
}
let outputURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("compressed_video_\(index).mp4")
if FileManager.default.fileExists(atPath: outputURL.path) {
do {
try FileManager.default.removeItem(at: outputURL)
} catch {
errors[index] = error
completedCount += 1
processNextVideo(index: index + 1)
return
}
}
let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)
guard let exportSession = exportSession else {
errors[index] = NSError(domain: "VideoCompressor", code: 3, userInfo: [NSLocalizedDescriptionKey: "Failed to create export session"])
completedCount += 1
processNextVideo(index: index + 1)
return
}
exportSession.outputURL = outputURL
exportSession.outputFileType = .mp4
exportSession.shouldOptimizeForNetworkUse = true
let compressionSettings: [String: Any] = [
AVVideoAverageBitRateKey: Int(videoTrack.estimatedDataRate) * Int(quality),
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel
]
let _: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill,
AVVideoCompressionPropertiesKey: compressionSettings
]
let _: [String: Any] = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVNumberOfChannelsKey: 2,
AVSampleRateKey: 44100,
AVEncoderBitRateKey: 128000
]
let videoComposition = AVMutableVideoComposition()
videoComposition.renderSize = videoTrack.naturalSize
videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
let instruction = AVMutableVideoCompositionInstruction()
instruction.timeRange = CMTimeRangeMake(start: .zero, duration: avAsset.duration)
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
instruction.layerInstructions = [layerInstruction]
videoComposition.instructions = [instruction]
exportSession.videoComposition = videoComposition
let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
progress(model.ident, exportSession.progress)
}
exportSession.exportAsynchronously {
progressTimer.invalidate()
switch exportSession.status {
case .completed:
outputURLs[index] = outputURL
errors[index] = nil
case .failed:
outputURLs[index] = nil
errors[index] = exportSession.error
case .cancelled:
outputURLs[index] = nil
errors[index] = NSError(domain: "VideoCompressor", code: 4, userInfo: [NSLocalizedDescriptionKey: "Export cancelled"])
default:
outputURLs[index] = nil
errors[index] = NSError(domain: "VideoCompressor", code: 5, userInfo: [NSLocalizedDescriptionKey: "Unknown export status"])
}
completedCount += 1
processNextVideo(index: index + 1)
}
}
}
processNextVideo(index: 0)
}
/// 压缩多张图片 /// 压缩多张图片
...@@ -112,7 +321,8 @@ class CompressViewModel{ ...@@ -112,7 +321,8 @@ class CompressViewModel{
let totalCount = assets.count let totalCount = assets.count
for (index, model) in assets.enumerated() { for (index, model) in assets.enumerated() {
compressSingleAsset(model.imageAsset,compressionQuality) { (progress) in let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [model.ident], options: nil)
compressSingleAsset(fetchResult.firstObject!,compressionQuality) { (progress) in
let singleProgress = Float(completedCount) / Float(totalCount) + progress / Float(totalCount) let singleProgress = Float(completedCount) / Float(totalCount) + progress / Float(totalCount)
progressHandler(singleProgress) progressHandler(singleProgress)
} completion: { (compressedData, error) in } completion: { (compressedData, error) in
...@@ -133,7 +343,6 @@ class CompressViewModel{ ...@@ -133,7 +343,6 @@ class CompressViewModel{
options.deliveryMode = .highQualityFormat options.deliveryMode = .highQualityFormat
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) { (imageData, _, _, error) in PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) { (imageData, _, _, error) in
guard let originalData = imageData else { guard let originalData = imageData else {
completion(nil, nil) completion(nil, nil)
return return
...@@ -179,4 +388,25 @@ class CompressViewModel{ ...@@ -179,4 +388,25 @@ class CompressViewModel{
} }
} }
// 通过identifier获取image
func getImageFromAssetIdentifier(identifier: String, completion: @escaping (UIImage?) -> Void) {
let fetchOptions = PHFetchOptions()
let assets = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: fetchOptions)
guard let asset = assets.firstObject else {
completion(nil)
return
}
let imageManager = PHCachingImageManager()
let options = PHImageRequestOptions()
options.isSynchronous = false
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = true
imageManager.requestImage(for: asset, targetSize: CGSize(width: 400, height: 400), contentMode: .aspectFill, options: options) { (image, _) in
completion(image)
}
}
} }
//
// Singleton.swift
// PhoneManager
//
// Created by 赵前 on 2025/4/6.
//
import Foundation
class Singleton {
// 使用静态常量来保存单例实例
static let shared = Singleton()
// 私有化初始化方法,防止外部创建新实例
private init() {}
var resourceModel : [ResourceModel] = []
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment