Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Sign in / Register
Toggle navigation
P
PhoneManager
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Packages
Packages
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Yang
PhoneManager
Commits
409eabe4
Commit
409eabe4
authored
Apr 12, 2025
by
CZ1004
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
修改
parent
fe59c4f0
Show whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
923 additions
and
235 deletions
+923
-235
project.pbxproj
PhoneManager.xcodeproj/project.pbxproj
+14
-12
AppDelegate.swift
PhoneManager/AppDelegate.swift
+9
-0
CompressSelectCell.swift
...ager/Class/Session/Compress/Cell/CompressSelectCell.swift
+59
-11
CompressCompletedViewController.swift
...Compress/Controller/CompressCompletedViewController.swift
+84
-11
CompressController.swift
...lass/Session/Compress/Controller/CompressController.swift
+50
-6
CompressQualityController.swift
...ssion/Compress/Controller/CompressQualityController.swift
+78
-64
PreVideoController.swift
...lass/Session/Compress/Controller/PreVideoController.swift
+71
-0
PreViewController.swift
...Class/Session/Compress/Controller/PreViewController.swift
+37
-0
ResourceModel.swift
...eManager/Class/Session/Compress/Model/ResourceModel.swift
+1
-5
CompressCustomHeaderView.swift
...lass/Session/Compress/View/CompressCustomHeaderView.swift
+16
-9
CompressSortView.swift
...anager/Class/Session/Compress/View/CompressSortView.swift
+10
-1
CompressSwitchView.swift
...ager/Class/Session/Compress/View/CompressSwitchView.swift
+6
-0
CompressTipView.swift
...Manager/Class/Session/Compress/View/CompressTipView.swift
+1
-1
CompressViewModel.swift
.../Class/Session/Compress/ViewModel/CompressViewModel.swift
+274
-48
HomePhotosModel .swift
...ager/Class/Session/Home/View/Model/HomePhotosModel .swift
+83
-67
PhotoAndVideoMananger.swift
...l/Class/PhotoAndVideoMananger/PhotoAndVideoMananger.swift
+113
-0
Singleton.swift
PhoneManager/Class/Tool/Singleton/Singleton.swift
+17
-0
No files found.
PhoneManager.xcodeproj/project.pbxproj
View file @
409eabe4
...
...
@@ -165,10 +165,14 @@
inputFileListPaths
=
(
"${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-resources-${CONFIGURATION}-input-files.xcfilelist"
,
);
inputPaths
=
(
);
name
=
"[CP] Copy Pods Resources"
;
outputFileListPaths
=
(
"${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-resources-${CONFIGURATION}-output-files.xcfilelist"
,
);
outputPaths
=
(
);
runOnlyForDeploymentPostprocessing
=
0
;
shellPath
=
/bin/sh
;
shellScript
=
"\"${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-resources.sh\"\n"
;
...
...
@@ -204,10 +208,14 @@
inputFileListPaths
=
(
"${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-frameworks-${CONFIGURATION}-input-files.xcfilelist"
,
);
inputPaths
=
(
);
name
=
"[CP] Embed Pods Frameworks"
;
outputFileListPaths
=
(
"${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-frameworks-${CONFIGURATION}-output-files.xcfilelist"
,
);
outputPaths
=
(
);
runOnlyForDeploymentPostprocessing
=
0
;
shellPath
=
/bin/sh
;
shellScript
=
"\"${PODS_ROOT}/Target Support Files/Pods-PhoneManager/Pods-PhoneManager-frameworks.sh\"\n"
;
...
...
@@ -234,11 +242,9 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME
=
AccentColor
;
CLANG_ENABLE_MODULES
=
YES
;
CODE_SIGN_IDENTITY
=
"Apple Development"
;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]"
=
"iPhone Developer"
;
CODE_SIGN_STYLE
=
Manual
;
CODE_SIGN_STYLE
=
Automatic
;
CURRENT_PROJECT_VERSION
=
1
;
DEVELOPMENT_TEAM
=
""
;
"DEVELOPMENT_TEAM[sdk=iphoneos*]"
=
6K23946NQ5
;
DEVELOPMENT_TEAM
=
7X8JL97Z3E
;
GENERATE_INFOPLIST_FILE
=
YES
;
INFOPLIST_FILE
=
PhoneManager/Info.plist
;
INFOPLIST_KEY_NSLocalNetworkUsageDescription
=
"We need to access the network to load content"
;
...
...
@@ -254,10 +260,9 @@
"@executable_path/Frameworks"
,
);
MARKETING_VERSION
=
1.0
;
PRODUCT_BUNDLE_IDENTIFIER
=
com.app.phonemanager
;
PRODUCT_BUNDLE_IDENTIFIER
=
com.app.phonemanager
1
;
PRODUCT_NAME
=
"$(TARGET_NAME)"
;
PROVISIONING_PROFILE_SPECIFIER
=
""
;
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]"
=
phonemanager_dev
;
SWIFT_EMIT_LOC_STRINGS
=
YES
;
SWIFT_OBJC_BRIDGING_HEADER
=
"PhoneManager/Class/Tool/Class/OC/PhoneManager-Bridging-Header.h"
;
SWIFT_OPTIMIZATION_LEVEL
=
"-Onone"
;
...
...
@@ -274,11 +279,9 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME
=
AccentColor
;
CLANG_ENABLE_MODULES
=
YES
;
CODE_SIGN_IDENTITY
=
"Apple Development"
;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]"
=
"iPhone Developer"
;
CODE_SIGN_STYLE
=
Manual
;
CODE_SIGN_STYLE
=
Automatic
;
CURRENT_PROJECT_VERSION
=
1
;
DEVELOPMENT_TEAM
=
""
;
"DEVELOPMENT_TEAM[sdk=iphoneos*]"
=
6K23946NQ5
;
DEVELOPMENT_TEAM
=
7X8JL97Z3E
;
GENERATE_INFOPLIST_FILE
=
YES
;
INFOPLIST_FILE
=
PhoneManager/Info.plist
;
INFOPLIST_KEY_NSLocalNetworkUsageDescription
=
"We need to access the network to load content"
;
...
...
@@ -294,10 +297,9 @@
"@executable_path/Frameworks"
,
);
MARKETING_VERSION
=
1.0
;
PRODUCT_BUNDLE_IDENTIFIER
=
com.app.phonemanager
;
PRODUCT_BUNDLE_IDENTIFIER
=
com.app.phonemanager
1
;
PRODUCT_NAME
=
"$(TARGET_NAME)"
;
PROVISIONING_PROFILE_SPECIFIER
=
""
;
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]"
=
phonemanager_dev
;
SWIFT_EMIT_LOC_STRINGS
=
YES
;
SWIFT_OBJC_BRIDGING_HEADER
=
"PhoneManager/Class/Tool/Class/OC/PhoneManager-Bridging-Header.h"
;
SWIFT_VERSION
=
5.0
;
...
...
PhoneManager/AppDelegate.swift
View file @
409eabe4
...
...
@@ -29,6 +29,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
window
?
.
makeKeyAndVisible
()
}
// 加载压缩里面的数据
let
viewModel
=
CompressViewModel
()
viewModel
.
getAllPhotosToAssets
(
sortType
:
0
,
assetType
:
0
,
{[
weak
self
]
models
in
guard
let
self
else
{
return
}
let
singleton
=
Singleton
.
shared
singleton
.
resourceModel
=
models
})
return
true
}
...
...
PhoneManager/Class/Session/Compress/Cell/CompressSelectCell.swift
View file @
409eabe4
...
...
@@ -6,6 +6,7 @@
//
import
Foundation
import
Photos
typealias
CompressSelectCellCallback
=
(
ResourceModel
,
Bool
)
->
Void
...
...
@@ -13,20 +14,48 @@ class CompressSelectCell : UICollectionViewCell {
var
callBack
:
CompressSelectCellCallback
=
{
model
,
choose
in
}
var
currentMediaType
:
Int
=
0
var
model
:
ResourceModel
?
{
didSet
{
guard
let
model
=
self
.
model
else
{
return
}
let
image
=
PhotoAndVideoMananger
.
mananger
.
getImageFromAssetID
(
id
:
model
.
ident
)
self
.
backImageView
.
image
=
nil
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
let
saveSize
=
model
.
orgSize
-
model
.
compressSize
if
saveSize
>
1000
{
self
.
saveSizeLabel
.
text
=
String
(
format
:
"Save %.2f GB"
,
saveSize
/
1024.0
)
let
options
=
PHImageRequestOptions
()
options
.
isSynchronous
=
false
options
.
deliveryMode
=
.
highQualityFormat
let
fetchResult
=
PHAsset
.
fetchAssets
(
withLocalIdentifiers
:
[
model
.
ident
],
options
:
nil
)
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 MB"
,
saveSize
)
self
.
saveSizeLabel
.
text
=
String
(
format
:
"Save %.2f GB"
,
sizeKB
/
(
1024
*
1024
))
}
}
}
}
}
}
}
var
choose
:
Bool
=
false
{
didSet
{
...
...
@@ -98,6 +127,11 @@ class CompressSelectCell : UICollectionViewCell {
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
.
saveSizeView
)
self
.
saveSizeView
.
addSubview
(
self
.
saveSizeLabel
)
...
...
@@ -110,14 +144,14 @@ class CompressSelectCell : UICollectionViewCell {
self
.
saveSizeView
.
snp
.
makeConstraints
{
make
in
make
.
left
.
equalToSuperview
()
.
offset
(
12
)
make
.
bottom
.
equalToSuperview
()
.
offset
(
-
12
)
make
.
height
.
equalTo
(
48
)
make
.
height
.
equalTo
(
25
)
make
.
width
.
equalTo
(
120
)
}
self
.
saveSizeLabel
.
snp
.
makeConstraints
{
make
in
make
.
left
.
equalToSuperview
()
.
offset
(
8
)
make
.
centerY
.
equalToSuperview
()
make
.
height
.
equalTo
(
48
)
make
.
width
.
equalTo
(
1
20
)
make
.
height
.
equalTo
(
25
)
make
.
width
.
equalTo
(
1
05
)
}
self
.
moreImageView
.
snp
.
makeConstraints
{
make
in
...
...
@@ -139,4 +173,18 @@ class CompressSelectCell : UICollectionViewCell {
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
)
}
}
}
PhoneManager/Class/Session/Compress/Controller/CompressCompletedViewController.swift
View file @
409eabe4
...
...
@@ -6,11 +6,18 @@
//
import
Foundation
import
Photos
class
CompressCompletedViewController
:
BaseViewController
{
var
model
:
[
ResourceModel
]?
var
comDataSource
:
[
Data
]
=
[]
var
comVideoDataSource
:
[
URL
?]
=
[]
var
currentMediaType
:
Int
=
0
lazy
var
imageView
:
UIImageView
=
{
let
imageView
=
UIImageView
()
imageView
.
clipsToBounds
=
true
...
...
@@ -248,25 +255,91 @@ class CompressCompletedViewController : BaseViewController{
}
@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
)
self
.
view
.
addSubview
(
compressTipView
)
compressTipView
.
callBack
=
{[
weak
self
]
allow
in
guard
let
self
else
{
return
}
}
// 删除文件逻辑【系统自动提示是否删除】
var
count
=
0
for
data
in
self
.
model
!
{
let
fetchResult
=
PHAsset
.
fetchAssets
(
withLocalIdentifiers
:
[
data
.
ident
],
options
:
nil
)
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
})
{
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
()
{
super
.
viewDidLoad
()
setUI
()
...
...
PhoneManager/Class/Session/Compress/Controller/CompressController.swift
View file @
409eabe4
...
...
@@ -96,21 +96,44 @@ class CompressController : BaseViewController {
override
func
viewDidLoad
()
{
super
.
viewDidLoad
()
view
.
backgroundColor
=
.
white
self
.
navigationController
?
.
navigationBar
.
isHidden
=
true
getViewData
()
setUI
()
}
override
func
viewWillAppear
(
_
animated
:
Bool
)
{
super
.
viewWillAppear
(
animated
)
// 这里默认去请求下当前的数据资源
let
viewModel
=
CompressViewModel
()
viewModel
.
getAllPhotos
{[
weak
self
]
models
in
/// 获取当前页面数据
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
)
{
super
.
viewWillAppear
(
animated
)
// 目的是为了消除cell 的选择按钮状态
if
self
.
selectedModel
.
count
==
0
{
self
.
collectionView
.
reloadData
()
}
}
}
...
...
@@ -130,6 +153,7 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo
let
cell
=
collectionView
.
dequeueReusableCell
(
withReuseIdentifier
:
"CompressSelectCell"
,
for
:
indexPath
)
as!
CompressSelectCell
cell
.
model
=
self
.
resourceData
[
indexPath
.
row
]
cell
.
currentMediaType
=
self
.
currentResourceType
if
self
.
selectedModel
.
count
==
0
{
cell
.
choose
=
false
}
...
...
@@ -207,6 +231,23 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo
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
return
header
}
else
{
...
...
@@ -275,6 +316,7 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo
// 先将值传到下一个页面
let
vc
:
CompressQualityController
=
CompressQualityController
()
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."
self
.
navigationController
?
.
pushViewController
(
vc
,
animated
:
true
)
...
...
@@ -288,4 +330,6 @@ extension CompressController:WaterfallMutiSectionDelegate,UICollectionViewDataSo
self
.
selectedModel
.
removeAll
()
self
.
updateSubmitButton
()
}
}
PhoneManager/Class/Session/Compress/Controller/CompressQualityController.swift
View file @
409eabe4
...
...
@@ -20,6 +20,8 @@ class CompressQualityController : BaseViewController{
var
currentQulityType
:
Int
=
0
var
currentMediaType
:
Int
=
0
private
var
compressNav
:
CompressNavView
?
...
...
@@ -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
(){
let
compressingView
:
CompressingView
=
CompressingView
(
frame
:
self
.
view
.
bounds
)
...
...
@@ -195,77 +231,55 @@ class CompressQualityController : BaseViewController{
if
currentQulityType
==
2
{
currentQulity
=
0.8
}
var
comDataSource
:
[
Data
]
=
[]
if
self
.
currentMediaType
==
0
{
// 表示压缩图片
manager
.
compress
(
assets
:
self
.
model
!
,
compressionQuality
:
currentQulity
)
{
progress
in
compressingView
.
animationView
.
setProgress
(
CGFloat
(
progress
),
animated
:
tru
e
,
duration
:
0.1
)
compressingView
.
animationView
.
setProgress
(
CGFloat
(
progress
),
animated
:
fals
e
,
duration
:
0.1
)
}
completion
:
{
compressedDataArray
,
errorArray
in
var
compressAllSize
=
0.0
for
(
index
,
data
)
in
compressedDataArray
.
enumerated
()
{
if
let
error
=
errorArray
[
index
]
{
print
(
"第
\(
index
+
1
)
张图片
压缩出错:
\(
error
.
localizedDescription
)
"
)
print
(
"第
\(
index
+
1
)
个文件
压缩出错:
\(
error
.
localizedDescription
)
"
)
}
else
if
let
data
=
data
{
print
(
"第
\(
index
+
1
)
张图片压缩完成,压缩后大小:
\(
data
.
count
)
字节"
)
print
(
"第
\(
index
+
1
)
个文件压缩完成,压缩后大小:
\(
data
.
count
)
字节"
)
compressAllSize
=
compressAllSize
+
Double
(
data
.
count
)
comDataSource
.
append
(
data
)
}
else
{
print
(
"第
\(
index
+
1
)
张图片
压缩失败"
)
print
(
"第
\(
index
+
1
)
个文件
压缩失败"
)
}
}
DispatchQueue
.
main
.
asyncAfter
(
deadline
:
.
now
()
+
2
)
{
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
for
modelData
in
self
.
model
!
{
orgAllSize
=
orgAllSize
+
modelData
.
orgSize
compressAllSize
=
compressAllSize
+
modelData
.
compressSize
sum
=
sum
+
modelData
.
orgSize
-
modelData
.
compressSize
self
.
updateNextView
(
compressAllSize
,
compressingView
,
comDataSource
,[])
}
if
sum
>
1000
{
sum
=
sum
/
1024
vc
.
sizeToplabel
.
text
=
"
\(
sum
)
GB"
}
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
)
vc
.
sizeBottomlabel
.
text
=
"
\(
str
)
%"
self
.
navigationController
?
.
pushViewController
(
vc
,
animated
:
true
)
}
self
.
updateNextView
(
compressAllSize
,
compressingView
,[],
outputURLs
)
}
}
// 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
// for modelData in self.model! {
// orgAllSize = orgAllSize + modelData.orgSize
// compressAllSize = compressAllSize + modelData.compressSize
// sum = sum + modelData.orgSize - modelData.compressSize
// }
// if sum > 1000 {
// sum = sum / 1024
// vc.sizeToplabel.text = "\(sum)GB"
// }else{
// vc.sizeToplabel.text = "\(sum)MB"
// }
// let str = String(format:"%.2lf",(orgAllSize - compressAllSize) / orgAllSize)
// vc.sizeBottomlabel.text = "\(str)%"
//
//
// self.navigationController?.pushViewController(vc, animated: true)
// }
}
}
PhoneManager/Class/Session/Compress/Controller/PreVideoController.swift
0 → 100644
View file @
409eabe4
//
// 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
()
}
}
PhoneManager/Class/Session/Compress/Controller/PreViewController.swift
0 → 100644
View file @
409eabe4
//
// 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
)
}
}
PhoneManager/Class/Session/Compress/Model/ResourceModel.swift
View file @
409eabe4
...
...
@@ -10,16 +10,12 @@ import Photos
struct
ResourceModel
:
Equatable
{
var
ident
:
String
var
compressSize
:
Double
var
orgSize
:
Double
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
.
compressSize
=
compressSize
self
.
orgSize
=
orgSize
self
.
createDate
=
createDate
self
.
imageAsset
=
imageAsset
}
}
PhoneManager/Class/Session/Compress/View/CompressCustomHeaderView.swift
View file @
409eabe4
...
...
@@ -19,18 +19,24 @@ class CompressCustomHeaderView: UICollectionReusableView{
var
saveSum
=
0.0
for
model
in
self
.
modeData
{
sum
=
sum
+
model
.
orgSize
saveSum
=
saveSum
+
(
model
.
orgSize
-
model
.
compressSize
)
}
if
sum
>
1000
{
self
.
siezLabel
.
text
=
String
(
format
:
"%.2f GB"
,(
sum
/
1024
))
saveSum
=
saveSum
+
model
.
orgSize
*
0.8
}
sum
=
sum
/
1000
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
{
self
.
siezLabel
.
text
=
String
(
format
:
"%.2f
MB"
,(
sum
))
self
.
siezLabel
.
text
=
String
(
format
:
"%.2f
GB"
,
sum
/
(
1024*
1024
))
}
if
saveSum
>
1000
{
self
.
saveSizeLabel
.
text
=
String
(
format
:
"%.2f GB"
,(
saveSum
/
1024.0
))
}
else
{
self
.
saveSizeLabel
.
text
=
String
(
format
:
"%.2f MB"
,(
saveSum
))
if
saveSum
<
1024
{
self
.
saveSizeLabel
.
text
=
String
(
format
:
"%.2f KB"
,(
saveSum
))
}
else
if
saveSum
<
(
1024
*
1024
)
&&
saveSum
>
1024
{
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{
self
.
selectlabel
.
snp
.
makeConstraints
{
make
in
make
.
right
.
equalToSuperview
()
.
offset
(
-
12
)
make
.
top
.
equalToSuperview
()
.
offset
(
6
)
make
.
width
.
equalTo
(
51
*
RScreenW
())
make
.
width
.
equalTo
(
60
*
RScreenW
())
make
.
height
.
equalTo
(
20
)
}
self
.
siezLabel
.
snp
.
makeConstraints
{
make
in
...
...
@@ -196,6 +202,7 @@ class CompressCustomHeaderView: UICollectionReusableView{
override
init
(
frame
:
CGRect
)
{
super
.
init
(
frame
:
frame
)
self
.
backgroundColor
=
.
white
setUI
()
...
...
PhoneManager/Class/Session/Compress/View/CompressSortView.swift
View file @
409eabe4
...
...
@@ -98,6 +98,12 @@ class CompressSortView : UIView,UITableViewDelegate,UITableViewDataSource {
lazy
var
backView
:
UIView
=
{
let
view
=
UIView
()
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
}()
...
...
@@ -154,6 +160,9 @@ class CompressSortView : UIView,UITableViewDelegate,UITableViewDataSource {
// 移除自身
self
.
removeFromSuperview
()
}
@objc
func
backViewClick
(){
// 移除自身
self
.
removeFromSuperview
()
}
}
PhoneManager/Class/Session/Compress/View/CompressSwitchView.swift
View file @
409eabe4
...
...
@@ -9,6 +9,8 @@ import Foundation
class
CompressSwitchView
:
UIView
{
var
callBack
:
callBack
<
Any
>
=
{
flag
in
}
var
currentButton
:
UIButton
?
lazy
var
leftButton
:
UIButton
=
{
...
...
@@ -18,6 +20,7 @@ class CompressSwitchView : UIView {
button
.
setTitle
(
"Photos"
,
for
:
.
normal
)
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
.
tag
=
1000
return
button
}()
...
...
@@ -28,6 +31,7 @@ class CompressSwitchView : UIView {
button
.
setTitle
(
"Videos"
,
for
:
.
normal
)
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
.
tag
=
1001
return
button
}()
...
...
@@ -74,6 +78,8 @@ class CompressSwitchView : UIView {
sender
.
setTitleColor
(
UIColor
(
red
:
0
,
green
:
0.51
,
blue
:
1
,
alpha
:
1
),
for
:
.
normal
)
sender
.
backgroundColor
=
.
white
self
.
currentButton
=
sender
self
.
callBack
(
sender
.
tag
-
1000
)
}
}
PhoneManager/Class/Session/Compress/View/CompressTipView.swift
View file @
409eabe4
...
...
@@ -86,7 +86,7 @@ class CompressTipView : UIView {
lazy
var
tipLabel
:
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
.
numberOfLines
=
0
label
.
font
=
UIFont
.
systemFont
(
ofSize
:
17
,
weight
:
.
bold
)
...
...
PhoneManager/Class/Session/Compress/ViewModel/CompressViewModel.swift
View file @
409eabe4
...
...
@@ -8,70 +8,122 @@
import
Foundation
import
Photos
typealias
Finished
=
([
ResourceModel
])
->
Void
struct
PhotoInfo
{
var
localIdentifier
:
String
}
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
)
{
// 模拟异步操作
DispatchQueue
.
global
()
.
async
{
// 模拟耗时操作
Thread
.
sleep
(
forTimeInterval
:
2
)
finised
()
func
showPermissionAlert
()
{
let
alertController
=
UIAlertController
(
title
:
"权限请求"
,
message
:
"为了使用此功能,需要访问您的相册。请在设置中授予相册访问权限。"
,
preferredStyle
:
.
alert
)
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: 图片信息
func
getAllPhotos
(
_
finished
:
@escaping
Finished
){
func
getAllPhotosToAssets
(
sortType
:
Int
,
assetType
:
Int
,
_
finished
:
@escaping
Finished
){
var
models
:
[
ResourceModel
]
=
[]
let
fetchOptions
=
PHFetchOptions
()
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
let
assetsArray
=
photosAssets
.
objects
(
at
:
IndexSet
(
0
..<
photosAssets
.
count
))
// 初始化一个model数组
var
models
:
[
ResourceModel
]
=
[]
let
options
=
PHImageRequestOptions
()
options
.
isSynchronous
=
false
options
.
deliveryMode
=
.
highQualityFormat
let
group
=
DispatchGroup
()
var
count
=
0
for
asset
in
assetsArray
{
PHImageManager
.
default
()
.
requestImageDataAndOrientation
(
for
:
asset
,
options
:
options
)
{
(
imageData
,
_
,
_
,
_
)
in
if
let
data
=
imageData
{
group
.
enter
()
DispatchQueue
.
global
()
.
async
{
// 图片大小
let
sizeInBytes
=
Double
(
data
.
count
)
let
sizeInMB
=
sizeInBytes
/
(
1024
*
1024
)
let
resources
=
PHAssetResource
.
assetResources
(
for
:
asset
)
var
assetSize
:
Int
=
0
for
resource
in
resources
{
if
let
fileSize
=
resource
.
value
(
forKey
:
"fileSize"
)
as?
Int64
{
assetSize
+=
Int
(
fileSize
)
}
}
let
sizeInMB
=
assetSize
// 获取图片的日期
let
creationDate
=
asset
.
creationDate
let
creationDate
=
asset
.
creationDate
!
// 获取图片的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
)
let
model
=
ResourceModel
.
init
(
ident
:
localIdentifier
,
orgSize
:
Double
(
sizeInMB
),
createDate
:
creationDate
)
models
.
append
(
model
)
finished
(
models
)
}
}
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:
/// - resource: 原资源
...
...
@@ -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{
let
totalCount
=
assets
.
count
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
)
progressHandler
(
singleProgress
)
}
completion
:
{
(
compressedData
,
error
)
in
...
...
@@ -133,11 +343,6 @@ class CompressViewModel{
options
.
deliveryMode
=
.
highQualityFormat
PHImageManager
.
default
()
.
requestImageDataAndOrientation
(
for
:
asset
,
options
:
options
)
{
(
imageData
,
_
,
_
,
error
)
in
if
let
error
=
error
{
completion
(
nil
,
error
as?
Error
)
return
}
guard
let
originalData
=
imageData
else
{
completion
(
nil
,
nil
)
return
...
...
@@ -183,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
)
}
}
}
PhoneManager/Class/Session/Home/View/Model/HomePhotosModel .swift
View file @
409eabe4
...
...
@@ -37,19 +37,24 @@ class PhotoDataManager {
func
loadFromFileSystem
(
filename
:
String
=
"photosManagerData.json"
,
resultModel
:
@escaping
(
_
model
:
PhotosManagerModel
)
->
()
=
{
mdoel
in
})
{
let
url
=
getDocumentsDirectory
()
.
appendingPathComponent
(
filename
)
do
{
let
data
=
try
Data
(
contentsOf
:
url
)
let
decoder
=
JSONDecoder
()
let
model
=
try
decoder
.
decode
(
PhotosManagerModel
.
self
,
from
:
data
)
resultModel
(
model
)
}
catch
{
// do {
// let data = try Data(contentsOf: url)
// let decoder = JSONDecoder()
// let model = try decoder.decode(PhotosManagerModel.self, from: data)
// resultModel(model)
// } catch {
//
// loadDataFromPhotos { model in
//
// resultModel(model)
// }
// }
loadDataFromPhotos
{
model
in
resultModel
(
model
)
}
}
}
private
func
loadDataFromPhotos
(
resultModel
:
@escaping
(
_
model
:
PhotosManagerModel
)
->
()
=
{
mdoel
in
})
{
...
...
@@ -72,65 +77,76 @@ class PhotoDataManager {
let
allModel
:
PhotosManagerModel
=
PhotosManagerModel
(
allFileNumber
:
0
,
allFileSize
:
0
,
titleModelArray
:
[
model1
,
model2
],
otherModelArray
:
[
model3
,
model4
,
model5
,
model6
,
model7
])
resultModel
(
allModel
)
PhotoAndVideoMananger
.
mananger
.
doCompareSimilarPhotos
(
assets
:
PhotoAndVideoMananger
.
mananger
.
allAssets
)
{
currentAssets
in
PhotoAndVideoMananger
.
mananger
.
fetXSOther
{
array
in
let
dispatchGroup
=
DispatchGroup
()
dispatchGroup
.
enter
()
PhotoSimilarityFinder
.
processSimilarPhotoGroups
(
simalr
:
1
,
assetGroups
:
array
)
{
array
,
fileSize
in
model1
.
assets
=
array
model1
.
allFileSize
=
fileSize
resultModel
(
allModel
)
dispatchGroup
.
leave
()
}
dispatchGroup
.
enter
()
PhotoSimilarityFinder
.
processSimilarPhotoGroups
(
assetGroups
:
array
)
{
array
,
fileSize
in
model2
.
assets
=
array
model2
.
allFileSize
=
fileSize
model1
.
assets
=
currentAssets
model1
.
allFileSize
=
0
resultModel
(
allModel
)
dispatchGroup
.
leave
()
}
dispatchGroup
.
enter
()
model2
.
assets
=
[
ResourceManager
.
manager
.
getAllVideo
()]
dispatchGroup
.
leave
()
dispatchGroup
.
enter
()
PhotoAndVideoMananger
.
mananger
.
fetXSVideo
{
array
in
PhotoSimilarityFinder
.
processSimilarVideoGroups
(
videoGroups
:
array
)
{
ids
in
model4
.
assets
=
ids
}
completeHandler
:
{
finallyAsset
in
model1
.
assets
=
finallyAsset
model1
.
allFileSize
=
0
resultModel
(
allModel
)
dispatchGroup
.
leave
()
}
}
dispatchGroup
.
notify
(
queue
:
.
main
)
{
print
(
"所有异步操作都已完成"
)
if
model1
.
assets
.
count
!=
0
||
model2
.
assets
.
count
!=
0
||
model3
.
assets
.
count
!=
0
{
PhotoDataManager
.
manager
.
saveToFileSystem
(
model
:
allModel
)
resultModel
(
allModel
)
}
}
}
// PhotoAndVideoMananger.mananger.fetXSOther { array in
//
// let dispatchGroup = DispatchGroup()
//
// dispatchGroup.enter()
// PhotoSimilarityFinder.processSimilarPhotoGroups(simalr:1,assetGroups: array) { array,fileSize in
//
// model1.assets = array
// model1.allFileSize = fileSize
//
// resultModel(allModel)
//
// dispatchGroup.leave()
// }
//
// dispatchGroup.enter()
// PhotoSimilarityFinder.processSimilarPhotoGroups(assetGroups: array) {array,fileSize in
//
// model2.assets = array
// model2.allFileSize = fileSize
//
// resultModel(allModel)
//
// dispatchGroup.leave()
// }
// dispatchGroup.enter()
// model2.assets = [ResourceManager.manager.getAllVideo()]
// dispatchGroup.leave()
//
// dispatchGroup.enter()
// PhotoAndVideoMananger.mananger.fetXSVideo { array in
//
// PhotoSimilarityFinder.processSimilarVideoGroups(videoGroups: array) {ids in
//
// model4.assets = ids
//
// resultModel(allModel)
//
// dispatchGroup.leave()
// }
// }
//
//
//
//
//
// dispatchGroup.notify(queue: .main) {
// print("所有异步操作都已完成")
//
// if model1.assets.count != 0 || model2.assets.count != 0 || model3.assets.count != 0 {
//
// PhotoDataManager.manager.saveToFileSystem(model: allModel)
// resultModel(allModel)
// }
//
// }
// }
...
...
PhoneManager/Class/Tool/Class/PhotoAndVideoMananger/PhotoAndVideoMananger.swift
View file @
409eabe4
...
...
@@ -50,6 +50,119 @@ class PhotoAndVideoMananger {
var
ids
:[
String
]
=
[]
// func doCompareSimilarPhotos(assets:[PHAsset],processHandler:@escaping ([[String]])->Void,completeHandler:@escaping ([[String]])->Void){
//
// let syncQueue = DispatchQueue(label: "com.example.syncQueue")
// // 创建信号量,最多允许3个任务同时执行
// let semaphore = DispatchSemaphore(value: 2)
// let options = PHImageRequestOptions()
// options.isSynchronous = true
// options.deliveryMode = .highQualityFormat
//
// // 双重循环
// var groupAssets : [[String]] = []
// for index in 0..<assets.count{
//
// var currentGroup : [String] = []
// //获取第一张图片
// currentGroup.append(assets[index].localIdentifier)
// let nextIndex = index + 1
// if (nextIndex < assets.count - 1){
// semaphore.wait()
// syncQueue.async {
// PHImageManager.default().requestImage(for: assets[index], targetSize: CGSizeMake(400, 400), contentMode:.aspectFit, options: options) { (image1, _) in
//
// for changeIndex in nextIndex..<assets.count{
// PHImageManager.default().requestImage(for: assets[changeIndex], targetSize: CGSizeMake(400, 400), contentMode:.aspectFit, options: options) { (image2, _) in
// let isSimilar = OpenCVWrapper.areImagesSimilar(image1, withImage2: image2, threshold: 0.99)
// if isSimilar {
// currentGroup.append(assets[changeIndex].localIdentifier)
// }
// }
// }
// }
// }
// if currentGroup.count >= 2 {
// groupAssets.append(currentGroup)
// processHandler(groupAssets)
// }
// semaphore.signal()
// }
//
// }
//
// completeHandler(groupAssets)
// }
let
imageCache
=
NSCache
<
NSString
,
UIImage
>
()
func
getCacheImage
(
asset
:
PHAsset
)
->
UIImage
{
let
imageTemp
=
imageCache
.
object
(
forKey
:
asset
.
localIdentifier
as
NSString
)
if
imageTemp
!=
nil
{
return
imageTemp
!
}
let
options
=
PHImageRequestOptions
()
options
.
isSynchronous
=
true
options
.
deliveryMode
=
.
fastFormat
options
.
resizeMode
=
.
exact
PHImageManager
.
default
()
.
requestImage
(
for
:
asset
,
targetSize
:
CGSizeMake
(
100
,
100
),
contentMode
:
.
aspectFit
,
options
:
options
)
{
(
image
,
_
)
in
if
image
!=
nil
{
self
.
imageCache
.
setObject
(
image
!
,
forKey
:
asset
.
localIdentifier
as
NSString
)
}
}
return
getCacheImage
(
asset
:
asset
)
}
func
doCompareSimilarPhotos
(
assets
:[
PHAsset
],
processHandler
:
@escaping
([[
String
]])
->
Void
,
completeHandler
:
@escaping
([[
String
]])
->
Void
){
let
options
=
PHImageRequestOptions
()
options
.
isSynchronous
=
true
options
.
deliveryMode
=
.
highQualityFormat
let
customQueue
=
OperationQueue
()
// 设置最大并发操作数,可根据实际情况调整
customQueue
.
maxConcurrentOperationCount
=
3
DispatchQueue
.
global
()
.
async
{
// 双重循环
Print
(
"开始时间
\(
Date
()
)
"
)
var
groupAssets
:
[[
String
]]
=
[]
for
index
in
0
..<
assets
.
count
{
var
currentGroup
:
[
String
]
=
[]
//获取第一张图片
currentGroup
.
append
(
assets
[
index
]
.
localIdentifier
)
let
nextIndex
=
index
+
1
let
operation
=
BlockOperation
{
if
(
nextIndex
<
assets
.
count
-
1
){
for
changeIndex
in
nextIndex
..<
assets
.
count
{
let
isSimilar
=
OpenCVWrapper
.
areImagesSimilar
(
self
.
getCacheImage
(
asset
:
assets
[
index
]),
withImage2
:
self
.
getCacheImage
(
asset
:
assets
[
changeIndex
]),
threshold
:
0.999
)
if
isSimilar
{
currentGroup
.
append
(
assets
[
changeIndex
]
.
localIdentifier
)
}
}
}
if
currentGroup
.
count
>=
2
{
groupAssets
.
append
(
currentGroup
)
processHandler
(
groupAssets
)
Print
(
"执行一次时间
\(
Date
()
)
,当前序列号:
\(
index
)
"
)
}
}
customQueue
.
addOperation
(
operation
)
// 一次执行完成需要将这个image从cache中移除
self
.
imageCache
.
removeObject
(
forKey
:
assets
[
index
]
.
localIdentifier
as
NSString
)
}
Print
(
"完成时间
\(
Date
()
)
"
)
completeHandler
(
groupAssets
)
}
}
func
setAssets
()
{
let
fetchOptions
=
PHFetchOptions
()
...
...
PhoneManager/Class/Tool/Singleton/Singleton.swift
0 → 100644
View file @
409eabe4
//
// Singleton.swift
// PhoneManager
//
// Created by 赵前 on 2025/4/6.
//
import
Foundation
class
Singleton
{
// 使用静态常量来保存单例实例
static
let
shared
=
Singleton
()
// 私有化初始化方法,防止外部创建新实例
private
init
()
{}
var
resourceModel
:
[
ResourceModel
]
=
[]
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment