This commit is contained in:
DDIsFriend
2023-08-18 17:28:57 +08:00
commit f0e8a1709d
4282 changed files with 192396 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
//
// ZLAddPhotoCell.swift
// ZLPhotoBrowser
//
// Created by ruby109 on 2020/11/3.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import Foundation
class ZLAddPhotoCell: UICollectionViewCell {
private lazy var imageView: UIImageView = {
let view = UIImageView(image: .zl.getImage("zl_addPhoto"))
view.contentMode = .scaleAspectFit
view.clipsToBounds = true
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
deinit {
zl_debugPrint("ZLAddPhotoCell deinit")
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
imageView.frame = CGRect(x: 0, y: 0, width: bounds.width / 3, height: bounds.width / 3)
imageView.center = CGPoint(x: bounds.midX, y: bounds.midY)
}
func setupUI() {
if ZLPhotoUIConfiguration.default().cellCornerRadio > 0 {
layer.masksToBounds = true
layer.cornerRadius = ZLPhotoUIConfiguration.default().cellCornerRadio
}
backgroundColor = .zl.cameraCellBgColor
contentView.addSubview(imageView)
}
}

View File

@@ -0,0 +1,208 @@
//
// ZLAlbumListCell.swift
// ZLPhotoBrowser
//
// Created by long on 2020/8/19.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
class ZLAlbumListCell: UITableViewCell {
private lazy var coverImageView: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFill
view.clipsToBounds = true
if ZLPhotoUIConfiguration.default().cellCornerRadio > 0 {
view.layer.masksToBounds = true
view.layer.cornerRadius = ZLPhotoUIConfiguration.default().cellCornerRadio
}
return view
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .zl.font(ofSize: 17)
label.textColor = .zl.albumListTitleColor
return label
}()
private lazy var countLabel: UILabel = {
let label = UILabel()
label.font = .zl.font(ofSize: 16)
label.textColor = .zl.albumListCountColor
return label
}()
private var imageIdentifier: String?
private var model: ZLAlbumListModel!
private var style: ZLPhotoBrowserStyle = .embedAlbumList
private var indicator: UIImageView = {
var image = UIImage.zl.getImage("zl_ablumList_arrow")
if isRTL() {
image = image?.imageFlippedForRightToLeftLayoutDirection()
}
let view = UIImageView(image: image)
view.contentMode = .scaleAspectFit
return view
}()
lazy var selectBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.isUserInteractionEnabled = false
btn.isHidden = true
btn.setImage(.zl.getImage("zl_albumSelect"), for: .selected)
return btn
}()
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let width = contentView.zl.width
let height = contentView.zl.height
let coverImageW = height - 4
let maxTitleW = width - coverImageW - 80
var titleW: CGFloat = 0
var countW: CGFloat = 0
if let model = model {
titleW = min(
bounds.width / 3 * 2,
model.title.zl.boundingRect(
font: .zl.font(ofSize: 17),
limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 30)
).width
)
titleW = min(titleW, maxTitleW)
countW = ("(" + String(model.count) + ")").zl
.boundingRect(
font: .zl.font(ofSize: 16),
limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 30)
).width
}
if isRTL() {
let imageViewX: CGFloat
if style == .embedAlbumList {
imageViewX = width - coverImageW
} else {
imageViewX = width - coverImageW - 12
}
coverImageView.frame = CGRect(x: imageViewX, y: 2, width: coverImageW, height: coverImageW)
titleLabel.frame = CGRect(
x: coverImageView.zl.left - titleW - 10,
y: (height - 30) / 2,
width: titleW,
height: 30
)
countLabel.frame = CGRect(
x: titleLabel.zl.left - countW - 10,
y: (height - 30) / 2,
width: countW,
height: 30
)
selectBtn.frame = CGRect(x: 20, y: (height - 20) / 2, width: 20, height: 20)
indicator.frame = CGRect(x: 20, y: (bounds.height - 15) / 2, width: 15, height: 15)
return
}
let imageViewX: CGFloat
if style == .embedAlbumList {
imageViewX = 0
} else {
imageViewX = 12
}
coverImageView.frame = CGRect(x: imageViewX, y: 2, width: coverImageW, height: coverImageW)
titleLabel.frame = CGRect(
x: coverImageView.zl.right + 10,
y: (bounds.height - 30) / 2,
width: titleW,
height: 30
)
countLabel.frame = CGRect(x: titleLabel.zl.right + 10, y: (height - 30) / 2, width: countW, height: 30)
selectBtn.frame = CGRect(x: width - 20 - 20, y: (height - 20) / 2, width: 20, height: 20)
indicator.frame = CGRect(x: width - 20 - 15, y: (height - 15) / 2, width: 15, height: 15)
}
func setupUI() {
backgroundColor = .zl.albumListBgColor
selectionStyle = .none
accessoryType = .none
contentView.addSubview(coverImageView)
contentView.addSubview(titleLabel)
contentView.addSubview(countLabel)
contentView.addSubview(selectBtn)
contentView.addSubview(indicator)
}
func configureCell(model: ZLAlbumListModel, style: ZLPhotoBrowserStyle) {
self.model = model
self.style = style
titleLabel.text = self.model.title
countLabel.text = "(" + String(self.model.count) + ")"
if style == .embedAlbumList {
selectBtn.isHidden = false
indicator.isHidden = true
} else {
indicator.isHidden = false
selectBtn.isHidden = true
}
imageIdentifier = self.model.headImageAsset?.localIdentifier
if let asset = self.model.headImageAsset {
let w = bounds.height * 2.5
ZLPhotoManager.fetchImage(for: asset, size: CGSize(width: w, height: w)) { [weak self] image, _ in
if self?.imageIdentifier == self?.model.headImageAsset?.localIdentifier {
self?.coverImageView.image = image ?? .zl.getImage("zl_defaultphoto")
}
}
}
}
}

View File

@@ -0,0 +1,158 @@
//
// ZLAlbumListController.swift
// ZLPhotoBrowser
//
// Created by long on 2020/8/18.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import Photos
class ZLAlbumListController: UIViewController {
private lazy var navView = ZLExternalAlbumListNavView(title: localLanguageTextValue(.photo))
private var navBlurView: UIVisualEffectView?
private lazy var tableView: UITableView = {
let view = UITableView(frame: .zero, style: .plain)
view.backgroundColor = .zl.albumListBgColor
view.tableFooterView = UIView()
view.rowHeight = 65
view.separatorInset = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 0)
view.separatorColor = .zl.separatorLineColor
view.delegate = self
view.dataSource = self
if #available(iOS 11.0, *) {
view.contentInsetAdjustmentBehavior = .always
}
ZLAlbumListCell.zl.register(view)
return view
}()
private var arrDataSource: [ZLAlbumListModel] = []
private var shouldReloadAlbumList = true
override var preferredStatusBarStyle: UIStatusBarStyle {
return ZLPhotoUIConfiguration.default().statusBarStyle
}
deinit {
zl_debugPrint("ZLAlbumListController deinit")
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
PHPhotoLibrary.shared().register(self)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.isHidden = true
guard shouldReloadAlbumList else {
return
}
DispatchQueue.global().async {
ZLPhotoManager.getPhotoAlbumList(ascending: ZLPhotoConfiguration.default().sortAscending, allowSelectImage: ZLPhotoConfiguration.default().allowSelectImage, allowSelectVideo: ZLPhotoConfiguration.default().allowSelectVideo) { [weak self] albumList in
self?.arrDataSource.removeAll()
self?.arrDataSource.append(contentsOf: albumList)
self?.shouldReloadAlbumList = false
ZLMainAsync {
self?.tableView.reloadData()
}
}
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let navViewNormalH: CGFloat = 44
var insets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
var collectionViewInsetTop: CGFloat = 20
if #available(iOS 11.0, *) {
insets = view.safeAreaInsets
collectionViewInsetTop = navViewNormalH
} else {
collectionViewInsetTop += navViewNormalH
}
navView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: insets.top + navViewNormalH)
tableView.frame = CGRect(x: insets.left, y: 0, width: view.frame.width - insets.left - insets.right, height: view.frame.height)
tableView.contentInset = UIEdgeInsets(top: collectionViewInsetTop, left: 0, bottom: 0, right: 0)
tableView.scrollIndicatorInsets = UIEdgeInsets(top: 44, left: 0, bottom: 0, right: 0)
}
private func setupUI() {
view.backgroundColor = .zl.albumListBgColor
view.addSubview(tableView)
navView.backBtn.isHidden = true
navView.cancelBlock = { [weak self] in
let nav = self?.navigationController as? ZLImageNavController
nav?.cancelBlock?()
nav?.dismiss(animated: true, completion: nil)
}
view.addSubview(navView)
}
}
extension ZLAlbumListController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrDataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ZLAlbumListCell.zl.identifier, for: indexPath) as! ZLAlbumListCell
cell.configureCell(model: arrDataSource[indexPath.row], style: .externalAlbumList)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let vc = ZLThumbnailViewController(albumList: arrDataSource[indexPath.row])
show(vc, sender: nil)
}
}
extension ZLAlbumListController: PHPhotoLibraryChangeObserver {
func photoLibraryDidChange(_ changeInstance: PHChange) {
shouldReloadAlbumList = true
}
}

View File

@@ -0,0 +1,96 @@
//
// ZLAlbumListModel.swift
// ZLPhotoBrowser
//
// Created by long on 2020/8/11.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import Photos
public class ZLAlbumListModel: NSObject {
public let title: String
public var count: Int {
return result.count
}
public var result: PHFetchResult<PHAsset>
public let collection: PHAssetCollection
public let option: PHFetchOptions
public let isCameraRoll: Bool
public var headImageAsset: PHAsset? {
return result.lastObject
}
public var models: [ZLPhotoModel] = []
//
private var selectedModels: [ZLPhotoModel] = []
//
private var selectedCount: Int = 0
public init(
title: String,
result: PHFetchResult<PHAsset>,
collection: PHAssetCollection,
option: PHFetchOptions,
isCameraRoll: Bool
) {
self.title = title
self.result = result
self.collection = collection
self.option = option
self.isCameraRoll = isCameraRoll
}
public func refetchPhotos() {
let models = ZLPhotoManager.fetchPhoto(
in: result,
ascending: ZLPhotoConfiguration.default().sortAscending,
allowSelectImage: ZLPhotoConfiguration.default().allowSelectImage,
allowSelectVideo: ZLPhotoConfiguration.default().allowSelectVideo
)
self.models.removeAll()
self.models.append(contentsOf: models)
}
func refreshResult() {
result = PHAsset.fetchAssets(in: collection, options: option)
}
}
extension ZLAlbumListModel {
static func ==(lhs: ZLAlbumListModel, rhs: ZLAlbumListModel) -> Bool {
return lhs.title == rhs.title &&
lhs.count == rhs.count &&
lhs.headImageAsset?.localIdentifier == rhs.headImageAsset?.localIdentifier
}
}

View File

@@ -0,0 +1,65 @@
//
// ZLAnimationUtils.swift
// ZLPhotoBrowser
//
// Created by long on 2023/1/13.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
class ZLAnimationUtils: NSObject {
enum AnimationType: String {
case fade = "opacity"
case scale = "transform.scale"
case rotate = "transform.rotation"
}
class func animation(
type: ZLAnimationUtils.AnimationType,
fromValue: CGFloat,
toValue: CGFloat,
duration: TimeInterval
) -> CAAnimation {
let animation = CABasicAnimation(keyPath: type.rawValue)
animation.fromValue = fromValue
animation.toValue = toValue
animation.duration = duration
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
return animation
}
class func springAnimation() -> CAKeyframeAnimation {
let animate = CAKeyframeAnimation(keyPath: "transform")
animate.duration = ZLPhotoConfiguration.default().selectBtnAnimationDuration
animate.isRemovedOnCompletion = true
animate.fillMode = .forwards
animate.values = [
CATransform3DMakeScale(0.7, 0.7, 1),
CATransform3DMakeScale(1.2, 1.2, 1),
CATransform3DMakeScale(0.8, 0.8, 1),
CATransform3DMakeScale(1, 1, 1),
]
return animate
}
}

View File

@@ -0,0 +1,157 @@
//
// ZLCameraCell.swift
// ZLPhotoBrowser
//
// Created by long on 2020/8/19.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import AVFoundation
class ZLCameraCell: UICollectionViewCell {
private lazy var imageView: UIImageView = {
let view = UIImageView(image: .zl.getImage("zl_takePhoto"))
view.contentMode = .scaleAspectFit
view.clipsToBounds = true
return view
}()
private var session: AVCaptureSession?
private var videoInput: AVCaptureDeviceInput?
private var photoOutput: AVCapturePhotoOutput?
private var previewLayer: AVCaptureVideoPreviewLayer?
var isEnable: Bool = true {
didSet {
contentView.alpha = isEnable ? 1 : 0.3
}
}
deinit {
session?.stopRunning()
session = nil
zl_debugPrint("ZLCameraCell deinit")
}
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
imageView.center = CGPoint(x: bounds.midX, y: bounds.midY)
previewLayer?.frame = contentView.layer.bounds
}
private func setupUI() {
layer.masksToBounds = true
layer.cornerRadius = ZLPhotoUIConfiguration.default().cellCornerRadio
contentView.addSubview(imageView)
backgroundColor = .zl.cameraCellBgColor
}
private func setupSession() {
guard session == nil, (session?.isRunning ?? false) == false else {
return
}
session?.stopRunning()
if let input = videoInput {
session?.removeInput(input)
}
if let output = photoOutput {
session?.removeOutput(output)
}
session = nil
previewLayer?.removeFromSuperlayer()
previewLayer = nil
guard let camera = backCamera() else {
return
}
guard let input = try? AVCaptureDeviceInput(device: camera) else {
return
}
videoInput = input
photoOutput = AVCapturePhotoOutput()
session = AVCaptureSession()
if session?.canAddInput(input) == true {
session?.addInput(input)
}
if session?.canAddOutput(photoOutput!) == true {
session?.addOutput(photoOutput!)
}
previewLayer = AVCaptureVideoPreviewLayer(session: session!)
contentView.layer.masksToBounds = true
previewLayer?.frame = contentView.layer.bounds
previewLayer?.videoGravity = .resizeAspectFill
contentView.layer.insertSublayer(previewLayer!, at: 0)
session?.startRunning()
}
private func backCamera() -> AVCaptureDevice? {
let devices = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .back).devices
for device in devices {
if device.position == .back {
return device
}
}
return nil
}
func startCapture() {
let status = AVCaptureDevice.authorizationStatus(for: .video)
if !UIImagePickerController.isSourceTypeAvailable(.camera) || status == .denied {
return
}
if status == .notDetermined {
AVCaptureDevice.requestAccess(for: .video) { granted in
if granted {
ZLMainAsync {
self.setupSession()
}
}
}
} else {
setupSession()
}
}
}

View File

@@ -0,0 +1,286 @@
//
// ZLCameraConfiguration.swift
// ZLPhotoBrowser
//
// Created by long on 2021/11/10.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import AVFoundation
@objcMembers
public class ZLCameraConfiguration: NSObject {
private var pri_allowTakePhoto = true
/// Allow taking photos in the camera (Need allowSelectImage to be true). Defaults to true.
public var allowTakePhoto: Bool {
get {
pri_allowTakePhoto && ZLPhotoConfiguration.default().allowSelectImage
}
set {
pri_allowTakePhoto = newValue
}
}
private var pri_allowRecordVideo = true
/// Allow recording in the camera (Need allowSelectVideo to be true). Defaults to true.
public var allowRecordVideo: Bool {
get {
pri_allowRecordVideo && ZLPhotoConfiguration.default().allowSelectVideo
}
set {
pri_allowRecordVideo = newValue
}
}
private var pri_minRecordDuration: ZLPhotoConfiguration.Second = 0
/// Minimum recording duration. Defaults to 0.
public var minRecordDuration: ZLPhotoConfiguration.Second {
get {
pri_minRecordDuration
}
set {
pri_minRecordDuration = max(0, newValue)
}
}
private var pri_maxRecordDuration: ZLPhotoConfiguration.Second = 20
/// Maximum recording duration. Defaults to 20, minimum is 1.
public var maxRecordDuration: ZLPhotoConfiguration.Second {
get {
pri_maxRecordDuration
}
set {
pri_maxRecordDuration = max(1, newValue)
}
}
/// Video resolution. Defaults to hd1920x1080.
public var sessionPreset: ZLCameraConfiguration.CaptureSessionPreset = .hd1920x1080
/// Camera focus mode. Defaults to continuousAutoFocus
public var focusMode: ZLCameraConfiguration.FocusMode = .continuousAutoFocus
/// Camera exposure mode. Defaults to continuousAutoExposure
public var exposureMode: ZLCameraConfiguration.ExposureMode = .continuousAutoExposure
/// Camera flahs switch. Defaults to true.
public var showFlashSwitch = true
/// Whether to support switch camera. Defaults to true.
public var allowSwitchCamera = true
/// Video export format for recording video and editing video. Defaults to mov.
public var videoExportType: ZLCameraConfiguration.VideoExportType = .mov
/// The default camera position after entering the camera. Defaults to back.
public var devicePosition: ZLCameraConfiguration.DevicePosition = .back
private var pri_videoCodecType: Any?
/// The codecs for video capture. Defaults to .h264
@available(iOS 11.0, *)
public var videoCodecType: AVVideoCodecType {
get {
(pri_videoCodecType as? AVVideoCodecType) ?? .h264
}
set {
pri_videoCodecType = newValue
}
}
}
public extension ZLCameraConfiguration {
@objc enum CaptureSessionPreset: Int {
var avSessionPreset: AVCaptureSession.Preset {
switch self {
case .cif352x288:
return .cif352x288
case .vga640x480:
return .vga640x480
case .hd1280x720:
return .hd1280x720
case .hd1920x1080:
return .hd1920x1080
case .photo:
return .photo
}
}
case cif352x288
case vga640x480
case hd1280x720
case hd1920x1080
case photo
}
@objc enum FocusMode: Int {
var avFocusMode: AVCaptureDevice.FocusMode {
switch self {
case .autoFocus:
return .autoFocus
case .continuousAutoFocus:
return .continuousAutoFocus
}
}
case autoFocus
case continuousAutoFocus
}
@objc enum ExposureMode: Int {
var avFocusMode: AVCaptureDevice.ExposureMode {
switch self {
case .autoExpose:
return .autoExpose
case .continuousAutoExposure:
return .continuousAutoExposure
}
}
case autoExpose
case continuousAutoExposure
}
@objc enum VideoExportType: Int {
var format: String {
switch self {
case .mov:
return "mov"
case .mp4:
return "mp4"
}
}
var avFileType: AVFileType {
switch self {
case .mov:
return .mov
case .mp4:
return .mp4
}
}
case mov
case mp4
}
@objc enum DevicePosition: Int {
case back
case front
/// For custom camera
var avDevicePosition: AVCaptureDevice.Position {
switch self {
case .back:
return .back
case .front:
return .front
}
}
/// For system camera
var cameraDevice: UIImagePickerController.CameraDevice {
switch self {
case .back:
return .rear
case .front:
return .front
}
}
}
}
// MARK: chaining
public extension ZLCameraConfiguration {
@discardableResult
func allowTakePhoto(_ value: Bool) -> ZLCameraConfiguration {
allowTakePhoto = value
return self
}
@discardableResult
func allowRecordVideo(_ value: Bool) -> ZLCameraConfiguration {
allowRecordVideo = value
return self
}
@discardableResult
func minRecordDuration(_ duration: ZLPhotoConfiguration.Second) -> ZLCameraConfiguration {
minRecordDuration = duration
return self
}
@discardableResult
func maxRecordDuration(_ duration: ZLPhotoConfiguration.Second) -> ZLCameraConfiguration {
maxRecordDuration = duration
return self
}
@discardableResult
func sessionPreset(_ sessionPreset: ZLCameraConfiguration.CaptureSessionPreset) -> ZLCameraConfiguration {
self.sessionPreset = sessionPreset
return self
}
@discardableResult
func focusMode(_ mode: ZLCameraConfiguration.FocusMode) -> ZLCameraConfiguration {
focusMode = mode
return self
}
@discardableResult
func exposureMode(_ mode: ZLCameraConfiguration.ExposureMode) -> ZLCameraConfiguration {
exposureMode = mode
return self
}
@discardableResult
func showFlashSwitch(_ value: Bool) -> ZLCameraConfiguration {
showFlashSwitch = value
return self
}
@discardableResult
func allowSwitchCamera(_ value: Bool) -> ZLCameraConfiguration {
allowSwitchCamera = value
return self
}
@discardableResult
func videoExportType(_ type: ZLCameraConfiguration.VideoExportType) -> ZLCameraConfiguration {
videoExportType = type
return self
}
@discardableResult
func devicePosition(_ position: ZLCameraConfiguration.DevicePosition) -> ZLCameraConfiguration {
devicePosition = position
return self
}
@available(iOS 11.0, *)
@discardableResult
func videoCodecType(_ type: AVVideoCodecType) -> ZLCameraConfiguration {
videoCodecType = type
return self
}
}

View File

@@ -0,0 +1,31 @@
//
// ZLCollectionViewFlowLayout.swift
// ZLPhotoBrowser
//
// Created by long on 2023/4/20.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
class ZLCollectionViewFlowLayout: UICollectionViewFlowLayout {
override var flipsHorizontallyInOppositeLayoutDirection: Bool { isRTL() }
}

View File

@@ -0,0 +1,83 @@
//
// ZLCustomAlertProtocol.swift
// ZLPhotoBrowser
//
// Created by long on 2022/6/29.
//
import UIKit
public enum ZLCustomAlertStyle {
case alert
case actionSheet
}
public protocol ZLCustomAlertProtocol: AnyObject {
/// Should return an instance of ZLCustomAlertProtocol
static func alert(title: String?, message: String, style: ZLCustomAlertStyle) -> ZLCustomAlertProtocol
func addAction(_ action: ZLCustomAlertAction)
func show(with parentVC: UIViewController?)
}
public class ZLCustomAlertAction: NSObject {
public enum Style {
case `default`
case tint
case cancel
case destructive
}
public let title: String
public let style: ZLCustomAlertAction.Style
public let handler: ((ZLCustomAlertAction) -> Void)?
deinit {
zl_debugPrint("ZLCustomAlertAction deinit")
}
public init(title: String, style: ZLCustomAlertAction.Style, handler: ((ZLCustomAlertAction) -> Void)?) {
self.title = title
self.style = style
self.handler = handler
super.init()
}
}
/// internal
extension ZLCustomAlertStyle {
var toSystemAlertStyle: UIAlertController.Style {
switch self {
case .alert:
return .alert
case .actionSheet:
return .actionSheet
}
}
}
/// internal
extension ZLCustomAlertAction.Style {
var toSystemAlertActionStyle: UIAlertAction.Style {
switch self {
case .default, .tint:
return .default
case .cancel:
return .cancel
case .destructive:
return .destructive
}
}
}
/// internal
extension ZLCustomAlertAction {
func toSystemAlertAction() -> UIAlertAction {
return UIAlertAction(title: title, style: style.toSystemAlertActionStyle) { _ in
self.handler?(self)
}
}
}

View File

@@ -0,0 +1,360 @@
//
// ZLEditImageConfiguration.swift
// ZLPhotoBrowser
//
// Created by long on 2021/12/17.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
/// Provide an image sticker container view that conform to this protocol must be a subclass of UIView
/// UIView
@objc public protocol ZLImageStickerContainerDelegate {
@objc var selectImageBlock: ((UIImage) -> Void)? { get set }
@objc var hideBlock: (() -> Void)? { get set }
@objc func show(in view: UIView)
}
@objcMembers
public class ZLEditImageConfiguration: NSObject {
private var pri_tools: [ZLEditImageConfiguration.EditTool] = ZLEditImageConfiguration.EditTool.allCases
/// Edit image tools. (Default order is draw, clip, imageSticker, textSticker, mosaic, filtter)
/// Because Objective-C Array can't contain Enum styles, so this property is invalid in Objective-C.
/// - warning: If you want to use the image sticker feature, you must provide a view that implements ZLImageStickerContainerDelegate.
public var tools: [ZLEditImageConfiguration.EditTool] {
get {
if pri_tools.isEmpty {
return ZLEditImageConfiguration.EditTool.allCases
} else {
return pri_tools
}
}
set {
pri_tools = newValue
}
}
/// Edit image tools. (This property is only for objc).
/// - warning: If you want to use the image sticker feature, you must provide a view that implements ZLImageStickerContainerDelegate.
public var tools_objc: [Int] = [] {
didSet {
tools = tools_objc.compactMap { ZLEditImageConfiguration.EditTool(rawValue: $0) }
}
}
private static let defaultDrawColors: [UIColor] = [
.white,
.black,
.zl.rgba(249, 80, 81),
.zl.rgba(248, 156, 59),
.zl.rgba(255, 195, 0),
.zl.rgba(145, 211, 0),
.zl.rgba(0, 193, 94),
.zl.rgba(16, 173, 254),
.zl.rgba(16, 132, 236),
.zl.rgba(99, 103, 240),
.zl.rgba(127, 127, 127)
]
private var pri_drawColors = ZLEditImageConfiguration.defaultDrawColors
/// Draw colors for image editor.
public var drawColors: [UIColor] {
get {
if pri_drawColors.isEmpty {
return ZLEditImageConfiguration.defaultDrawColors
} else {
return pri_drawColors
}
}
set {
pri_drawColors = newValue
}
}
/// The default draw color. If this color not in editImageDrawColors, will pick the first color in editImageDrawColors as the default.
public var defaultDrawColor: UIColor = .zl.rgba(249, 80, 81)
private var pri_clipRatios: [ZLImageClipRatio] = [.custom]
/// Edit ratios for image editor.
public var clipRatios: [ZLImageClipRatio] {
get {
if pri_clipRatios.isEmpty {
return [.custom]
} else {
return pri_clipRatios
}
}
set {
pri_clipRatios = newValue
}
}
private static let defaultTextStickerTextColors: [UIColor] = [
.white,
.black,
.zl.rgba(249, 80, 81),
.zl.rgba(248, 156, 59),
.zl.rgba(255, 195, 0),
.zl.rgba(145, 211, 0),
.zl.rgba(0, 193, 94),
.zl.rgba(16, 173, 254),
.zl.rgba(16, 132, 236),
.zl.rgba(99, 103, 240),
.zl.rgba(127, 127, 127)
]
private var pri_textStickerTextColors: [UIColor] = ZLEditImageConfiguration.defaultTextStickerTextColors
/// Text sticker colors for image editor.
public var textStickerTextColors: [UIColor] {
get {
if pri_textStickerTextColors.isEmpty {
return ZLEditImageConfiguration.defaultTextStickerTextColors
} else {
return pri_textStickerTextColors
}
}
set {
pri_textStickerTextColors = newValue
}
}
/// The default text sticker color. If this color not in textStickerTextColors, will pick the first color in textStickerTextColors as the default.
public var textStickerDefaultTextColor = UIColor.white
private var pri_filters: [ZLFilter] = ZLFilter.all
/// Filters for image editor.
public var filters: [ZLFilter] {
get {
if pri_filters.isEmpty {
return ZLFilter.all
} else {
return pri_filters
}
}
set {
pri_filters = newValue
}
}
public var imageStickerContainerView: (UIView & ZLImageStickerContainerDelegate)?
private var pri_adjustTools: [ZLEditImageConfiguration.AdjustTool] = ZLEditImageConfiguration.AdjustTool.allCases
/// Adjust image tools. (Default order is brightness, contrast, saturation)
/// Valid when the tools contain EditTool.adjust
/// Because Objective-C Array can't contain Enum styles, so this property is invalid in Objective-C.
public var adjustTools: [ZLEditImageConfiguration.AdjustTool] {
get {
if pri_adjustTools.isEmpty {
return ZLEditImageConfiguration.AdjustTool.allCases
} else {
return pri_adjustTools
}
}
set {
pri_adjustTools = newValue
}
}
/// Adjust image tools. (This property is only for objc).
/// Valid when the tools contain EditTool.adjust
public var adjustTools_objc: [Int] = [] {
didSet {
adjustTools = adjustTools_objc.compactMap { ZLEditImageConfiguration.AdjustTool(rawValue: $0) }
}
}
/// Give an impact feedback when the adjust slider value is zero. Defaults to true.
public var impactFeedbackWhenAdjustSliderValueIsZero = true
/// Impact feedback style. Defaults to .medium
public var impactFeedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle = .medium
/// Whether to support redo in graffiti and mosaic tools. Defaults to false
public var canRedo = false
}
public extension ZLEditImageConfiguration {
@objc enum EditTool: Int, CaseIterable {
case draw
case clip
case imageSticker
case textSticker
case mosaic
case filter
case adjust
}
@objc enum AdjustTool: Int, CaseIterable {
case brightness
case contrast
case saturation
var key: String {
switch self {
case .brightness:
return kCIInputBrightnessKey
case .contrast:
return kCIInputContrastKey
case .saturation:
return kCIInputSaturationKey
}
}
func filterValue(_ value: Float) -> Float {
switch self {
case .brightness:
// -1---103 -0.33---0.33
return value / 3
case .contrast:
// 0---410.5---2.5
let v: Float
if value < 0 {
v = 1 + value * (1 / 2)
} else {
v = 1 + value * (3 / 2)
}
return v
case .saturation:
// 0---21
return value + 1
}
}
}
}
// MARK: chaining
public extension ZLEditImageConfiguration {
@discardableResult
func tools(_ tools: [ZLEditImageConfiguration.EditTool]) -> ZLEditImageConfiguration {
self.tools = tools
return self
}
@discardableResult
func drawColors(_ colors: [UIColor]) -> ZLEditImageConfiguration {
drawColors = colors
return self
}
func defaultDrawColor(_ color: UIColor) -> ZLEditImageConfiguration {
defaultDrawColor = color
return self
}
@discardableResult
func clipRatios(_ ratios: [ZLImageClipRatio]) -> ZLEditImageConfiguration {
clipRatios = ratios
return self
}
@discardableResult
func textStickerTextColors(_ colors: [UIColor]) -> ZLEditImageConfiguration {
textStickerTextColors = colors
return self
}
@discardableResult
func textStickerDefaultTextColor(_ color: UIColor) -> ZLEditImageConfiguration {
textStickerDefaultTextColor = color
return self
}
@discardableResult
func filters(_ filters: [ZLFilter]) -> ZLEditImageConfiguration {
self.filters = filters
return self
}
@discardableResult
func imageStickerContainerView(_ view: (UIView & ZLImageStickerContainerDelegate)?) -> ZLEditImageConfiguration {
imageStickerContainerView = view
return self
}
@discardableResult
func adjustTools(_ tools: [ZLEditImageConfiguration.AdjustTool]) -> ZLEditImageConfiguration {
adjustTools = tools
return self
}
@discardableResult
func impactFeedbackWhenAdjustSliderValueIsZero(_ value: Bool) -> ZLEditImageConfiguration {
impactFeedbackWhenAdjustSliderValueIsZero = value
return self
}
@discardableResult
func impactFeedbackStyle(_ style: UIImpactFeedbackGenerator.FeedbackStyle) -> ZLEditImageConfiguration {
impactFeedbackStyle = style
return self
}
@discardableResult
func canRedo(_ value: Bool) -> ZLEditImageConfiguration {
canRedo = value
return self
}
}
// MARK:
public class ZLImageClipRatio: NSObject {
@objc public var title: String
@objc public let whRatio: CGFloat
@objc public let isCircle: Bool
@objc public init(title: String, whRatio: CGFloat, isCircle: Bool = false) {
self.title = title
self.whRatio = isCircle ? 1 : whRatio
self.isCircle = isCircle
super.init()
}
}
extension ZLImageClipRatio {
static func == (lhs: ZLImageClipRatio, rhs: ZLImageClipRatio) -> Bool {
return lhs.whRatio == rhs.whRatio && lhs.title == rhs.title
}
}
public extension ZLImageClipRatio {
@objc static let custom = ZLImageClipRatio(title: "custom", whRatio: 0)
@objc static let circle = ZLImageClipRatio(title: "circle", whRatio: 1, isCircle: true)
@objc static let wh1x1 = ZLImageClipRatio(title: "1 : 1", whRatio: 1)
@objc static let wh3x4 = ZLImageClipRatio(title: "3 : 4", whRatio: 3.0 / 4.0)
@objc static let wh4x3 = ZLImageClipRatio(title: "4 : 3", whRatio: 4.0 / 3.0)
@objc static let wh2x3 = ZLImageClipRatio(title: "2 : 3", whRatio: 2.0 / 3.0)
@objc static let wh3x2 = ZLImageClipRatio(title: "3 : 2", whRatio: 3.0 / 2.0)
@objc static let wh9x16 = ZLImageClipRatio(title: "9 : 16", whRatio: 9.0 / 16.0)
@objc static let wh16x9 = ZLImageClipRatio(title: "16 : 9", whRatio: 16.0 / 9.0)
}

View File

@@ -0,0 +1,232 @@
//
// ZLEmbedAlbumListView.swift
// ZLPhotoBrowser
//
// Created by long on 2020/9/7.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import Photos
class ZLEmbedAlbumListView: UIView {
static let rowH: CGFloat = 60
private var selectedAlbum: ZLAlbumListModel
private lazy var tableBgView = UIView()
private lazy var tableView: UITableView = {
let view = UITableView(frame: .zero, style: .plain)
view.backgroundColor = .zl.albumListBgColor
view.tableFooterView = UIView()
view.rowHeight = ZLEmbedAlbumListView.rowH
view.separatorInset = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 0)
view.separatorColor = .zl.separatorLineColor
view.delegate = self
view.dataSource = self
ZLAlbumListCell.zl.register(view)
return view
}()
private var arrDataSource: [ZLAlbumListModel] = []
var selectAlbumBlock: ((ZLAlbumListModel) -> Void)?
var hideBlock: (() -> Void)?
private var orientation: UIInterfaceOrientation = UIApplication.shared.statusBarOrientation
init(selectedAlbum: ZLAlbumListModel) {
self.selectedAlbum = selectedAlbum
super.init(frame: .zero)
setupUI()
loadAlbumList()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let currOri = UIApplication.shared.statusBarOrientation
guard currOri != orientation else {
return
}
orientation = currOri
guard !isHidden else {
return
}
let bgFrame = calculateBgViewBounds()
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: frame.width, height: bgFrame.height), byRoundingCorners: [.bottomLeft, .bottomRight], cornerRadii: CGSize(width: 8, height: 8))
tableBgView.layer.mask = nil
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
tableBgView.layer.mask = maskLayer
tableBgView.frame = bgFrame
tableView.frame = tableBgView.bounds
}
private func setupUI() {
clipsToBounds = true
backgroundColor = .zl.embedAlbumListTranslucentColor
addSubview(tableBgView)
tableBgView.addSubview(tableView)
let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction(_:)))
tap.delegate = self
addGestureRecognizer(tap)
}
private func loadAlbumList(completion: (() -> Void)? = nil) {
DispatchQueue.global().async {
ZLPhotoManager.getPhotoAlbumList(ascending: ZLPhotoConfiguration.default().sortAscending, allowSelectImage: ZLPhotoConfiguration.default().allowSelectImage, allowSelectVideo: ZLPhotoConfiguration.default().allowSelectVideo) { [weak self] albumList in
self?.arrDataSource.removeAll()
self?.arrDataSource.append(contentsOf: albumList)
ZLMainAsync {
completion?()
self?.tableView.reloadData()
}
}
}
}
private func calculateBgViewBounds() -> CGRect {
let contentH = CGFloat(arrDataSource.count) * ZLEmbedAlbumListView.rowH
let maxH: CGFloat
if UIApplication.shared.statusBarOrientation.isPortrait {
maxH = min(frame.height * 0.7, contentH)
} else {
maxH = min(frame.height * 0.8, contentH)
}
return CGRect(x: 0, y: 0, width: frame.width, height: maxH)
}
@objc private func tapAction(_ tap: UITapGestureRecognizer) {
hide()
hideBlock?()
}
///
func show(reloadAlbumList: Bool) {
guard reloadAlbumList else {
animateShow()
return
}
if #available(iOS 14.0, *), PHPhotoLibrary.authorizationStatus(for: .readWrite) == .limited {
loadAlbumList { [weak self] in
self?.animateShow()
}
} else {
loadAlbumList()
animateShow()
}
}
func hide() {
var toFrame = tableBgView.frame
toFrame.origin.y = -toFrame.height
UIView.animate(withDuration: 0.25, animations: {
self.alpha = 0
self.tableBgView.frame = toFrame
}) { _ in
self.isHidden = true
self.alpha = 1
}
}
private func animateShow() {
let toFrame = calculateBgViewBounds()
isHidden = false
alpha = 0
var newFrame = toFrame
newFrame.origin.y -= newFrame.height
if newFrame != tableBgView.frame {
let path = UIBezierPath(
roundedRect: CGRect(x: 0, y: 0, width: newFrame.width, height: newFrame.height),
byRoundingCorners: [.bottomLeft, .bottomRight],
cornerRadii: CGSize(width: 8, height: 8)
)
tableBgView.layer.mask = nil
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
tableBgView.layer.mask = maskLayer
}
tableBgView.frame = newFrame
tableView.frame = tableBgView.bounds
UIView.animate(withDuration: 0.25) {
self.alpha = 1
self.tableBgView.frame = toFrame
}
}
}
extension ZLEmbedAlbumListView: UIGestureRecognizerDelegate {
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let point = gestureRecognizer.location(in: self)
return !tableBgView.frame.contains(point)
}
}
extension ZLEmbedAlbumListView: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrDataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ZLAlbumListCell.zl.identifier, for: indexPath) as! ZLAlbumListCell
let m = arrDataSource[indexPath.row]
cell.configureCell(model: m, style: .embedAlbumList)
cell.selectBtn.isSelected = m == selectedAlbum
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let m = arrDataSource[indexPath.row]
selectedAlbum = m
selectAlbumBlock?(m)
hide()
if let indexPaths = tableView.indexPathsForVisibleRows {
tableView.reloadRows(at: indexPaths, with: .none)
}
}
}

View File

@@ -0,0 +1,68 @@
//
// ZLEnlargeButton.swift
// ZLPhotoBrowser
//
// Created by long on 2022/4/24.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
public class ZLEnlargeButton: UIButton {
///
public var enlargeInsets: UIEdgeInsets = .zero
///
public var enlargeInset: CGFloat = 0 {
didSet {
let inset = max(0, enlargeInset)
enlargeInsets = UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset)
}
}
override public func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
guard !isHidden, alpha != 0 else {
return false
}
let rect = enlargeRect()
if rect.equalTo(bounds) {
return super.point(inside: point, with: event)
}
return rect.contains(point) ? true : false
}
private func enlargeRect() -> CGRect {
guard enlargeInsets != .zero else {
return bounds
}
let rect = CGRect(
x: bounds.minX - enlargeInsets.left,
y: bounds.minY - enlargeInsets.top,
width: bounds.width + enlargeInsets.left + enlargeInsets.right,
height: bounds.height + enlargeInsets.top + enlargeInsets.bottom
)
return rect
}
}

View File

@@ -0,0 +1,181 @@
//
// ZLFetchImageOperation.swift
// ZLPhotoBrowser
//
// Created by long on 2020/8/18.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import Photos
class ZLFetchImageOperation: Operation {
private let model: ZLPhotoModel
private let isOriginal: Bool
private let progress: ((CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable: Any]?) -> Void)?
private let completion: (UIImage?, PHAsset?) -> Void
private var pri_isExecuting = false {
willSet {
self.willChangeValue(forKey: "isExecuting")
}
didSet {
self.didChangeValue(forKey: "isExecuting")
}
}
override var isExecuting: Bool {
return pri_isExecuting
}
private var pri_isFinished = false {
willSet {
self.willChangeValue(forKey: "isFinished")
}
didSet {
self.didChangeValue(forKey: "isFinished")
}
}
override var isFinished: Bool {
return pri_isFinished
}
private var pri_isCancelled = false {
willSet {
willChangeValue(forKey: "isCancelled")
}
didSet {
didChangeValue(forKey: "isCancelled")
}
}
private var requestImageID: PHImageRequestID = PHInvalidImageRequestID
override var isCancelled: Bool {
return pri_isCancelled
}
init(
model: ZLPhotoModel,
isOriginal: Bool,
progress: ((CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable: Any]?) -> Void)? = nil,
completion: @escaping ((UIImage?, PHAsset?) -> Void)
) {
self.model = model
self.isOriginal = isOriginal
self.progress = progress
self.completion = completion
super.init()
}
override func start() {
if isCancelled {
fetchFinish()
return
}
zl_debugPrint("---- start fetch")
pri_isExecuting = true
//
if let editImage = model.editImage {
if ZLPhotoConfiguration.default().saveNewImageAfterEdit {
ZLPhotoManager.saveImageToAlbum(image: editImage) { [weak self] _, asset in
self?.completion(editImage, asset)
self?.fetchFinish()
}
} else {
ZLMainAsync {
self.completion(editImage, nil)
self.fetchFinish()
}
}
return
}
if ZLPhotoConfiguration.default().allowSelectGif, model.type == .gif {
requestImageID = ZLPhotoManager.fetchOriginalImageData(for: model.asset) { [weak self] data, _, isDegraded in
if !isDegraded {
let image = UIImage.zl.animateGifImage(data: data)
self?.completion(image, nil)
self?.fetchFinish()
}
}
return
}
if isOriginal {
requestImageID = ZLPhotoManager.fetchOriginalImage(for: model.asset, progress: progress) { [weak self] image, isDegraded in
if !isDegraded {
zl_debugPrint("---- 原图加载完成 \(String(describing: self?.isCancelled))")
self?.completion(image?.zl.fixOrientation(), nil)
self?.fetchFinish()
}
}
} else {
requestImageID = ZLPhotoManager.fetchImage(for: model.asset, size: model.previewSize, progress: progress) { [weak self] image, isDegraded in
if !isDegraded {
zl_debugPrint("---- 加载完成 isCancelled: \(String(describing: self?.isCancelled))")
self?.completion(self?.scaleImage(image?.zl.fixOrientation()), nil)
self?.fetchFinish()
}
}
}
}
override func cancel() {
super.cancel()
zl_debugPrint("---- cancel \(isExecuting) \(requestImageID)")
PHImageManager.default().cancelImageRequest(requestImageID)
pri_isCancelled = true
if isExecuting {
fetchFinish()
}
}
private func scaleImage(_ image: UIImage?) -> UIImage? {
guard let i = image else {
return nil
}
guard let data = i.jpegData(compressionQuality: 1) else {
return i
}
let mUnit: CGFloat = 1024 * 1024
if data.count < Int(0.2 * mUnit) {
return i
}
let scale: CGFloat = (data.count > Int(mUnit) ? 0.6 : 0.8)
guard let d = i.jpegData(compressionQuality: scale) else {
return i
}
return UIImage(data: d)
}
private func fetchFinish() {
pri_isExecuting = false
pri_isFinished = true
}
}

View File

@@ -0,0 +1,254 @@
//
// ZLGeneralDefine.swift
// ZLPhotoBrowser
//
// Created by long on 2020/8/11.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
let ZLMaxImageWidth: CGFloat = 500
enum ZLLayout {
static let navTitleFont: UIFont = .zl.font(ofSize: 17)
static let bottomToolViewH: CGFloat = 55
static let bottomToolBtnH: CGFloat = 34
static let bottomToolBtnY: CGFloat = 10
static let bottomToolTitleFont: UIFont = .zl.font(ofSize: 17)
static let bottomToolBtnCornerRadius: CGFloat = 5
}
func markSelected(source: inout [ZLPhotoModel], selected: inout [ZLPhotoModel]) {
guard selected.count > 0 else {
return
}
var selIds: [String: Bool] = [:]
var selEditImage: [String: UIImage] = [:]
var selEditModel: [String: ZLEditImageModel] = [:]
var selIdAndIndex: [String: Int] = [:]
for (index, m) in selected.enumerated() {
selIds[m.ident] = true
selEditImage[m.ident] = m.editImage
selEditModel[m.ident] = m.editImageModel
selIdAndIndex[m.ident] = index
}
source.forEach { m in
if selIds[m.ident] == true {
m.isSelected = true
m.editImage = selEditImage[m.ident]
m.editImageModel = selEditModel[m.ident]
selected[selIdAndIndex[m.ident]!] = m
} else {
m.isSelected = false
}
}
}
func getAppName() -> String {
if let name = Bundle.main.localizedInfoDictionary?["CFBundleDisplayName"] as? String {
return name
}
if let name = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String {
return name
}
if let name = Bundle.main.infoDictionary?["CFBundleName"] as? String {
return name
}
return "App"
}
func deviceIsiPhone() -> Bool {
return UIDevice.current.userInterfaceIdiom == .phone
}
func deviceIsiPad() -> Bool {
return UIDevice.current.userInterfaceIdiom == .pad
}
func deviceSafeAreaInsets() -> UIEdgeInsets {
var insets: UIEdgeInsets = .zero
if #available(iOS 11, *) {
insets = UIApplication.shared.keyWindow?.safeAreaInsets ?? .zero
}
return insets
}
func deviceIsFringeScreen() -> Bool {
return deviceSafeAreaInsets().top > 0
}
func isSmallScreen() -> Bool {
return UIScreen.main.bounds.height <= 812
}
func isRTL() -> Bool {
return UIView.userInterfaceLayoutDirection(for: UIView.appearance().semanticContentAttribute) == .rightToLeft
}
func showAlertView(_ message: String, _ sender: UIViewController?) {
ZLMainAsync {
let action = ZLCustomAlertAction(title: localLanguageTextValue(.ok), style: .default, handler: nil)
showAlertController(title: nil, message: message, style: .alert, actions: [action], sender: sender)
}
}
func showAlertController(title: String?, message: String?, style: ZLCustomAlertStyle, actions: [ZLCustomAlertAction], sender: UIViewController?) {
if let alertClass = ZLPhotoUIConfiguration.default().customAlertClass {
let alert = alertClass.alert(title: title, message: message ?? "", style: style)
actions.forEach { alert.addAction($0) }
alert.show(with: sender)
return
}
let alert = UIAlertController(title: title, message: message, preferredStyle: style.toSystemAlertStyle)
actions
.map { $0.toSystemAlertAction() }
.forEach { alert.addAction($0) }
if deviceIsiPad() {
alert.popoverPresentationController?.sourceView = sender?.view
}
(sender ?? UIApplication.shared.keyWindow?.rootViewController)?.zl.showAlertController(alert)
}
func canAddModel(_ model: ZLPhotoModel, currentSelectCount: Int, sender: UIViewController?, showAlert: Bool = true) -> Bool {
let config = ZLPhotoConfiguration.default()
guard config.canSelectAsset?(model.asset) ?? true else {
return false
}
if currentSelectCount >= config.maxSelectCount {
if showAlert {
let message = String(format: localLanguageTextValue(.exceededMaxSelectCount), config.maxSelectCount)
showAlertView(message, sender)
}
return false
}
if currentSelectCount > 0,
!config.allowMixSelect,
model.type == .video{
return false
}
guard model.type == .video else {
return true
}
if model.second > config.maxSelectVideoDuration {
if showAlert {
let message = String(format: localLanguageTextValue(.longerThanMaxVideoDuration), config.maxSelectVideoDuration)
showAlertView(message, sender)
}
return false
}
if model.second < config.minSelectVideoDuration {
if showAlert {
let message = String(format: localLanguageTextValue(.shorterThanMinVideoDuration), config.minSelectVideoDuration)
showAlertView(message, sender)
}
return false
}
guard (config.minSelectVideoDataSize > 0 || config.maxSelectVideoDataSize != .greatestFiniteMagnitude),
let size = model.dataSize else {
return true
}
if size > config.maxSelectVideoDataSize {
if showAlert {
let value = Int(round(config.maxSelectVideoDataSize / 1024))
let message = String(format: localLanguageTextValue(.largerThanMaxVideoDataSize), String(value))
showAlertView(message, sender)
}
return false
}
if size < config.minSelectVideoDataSize {
if showAlert {
let value = Int(round(config.minSelectVideoDataSize / 1024))
let message = String(format: localLanguageTextValue(.smallerThanMinVideoDataSize), String(value))
showAlertView(message, sender)
}
return false
}
return true
}
/// Check if the video duration and size meet the requirements
func videoIsMeetRequirements(model: ZLPhotoModel) -> Bool {
guard model.type == .video else {
return true
}
let config = ZLPhotoConfiguration.default()
guard config.minSelectVideoDuration...config.maxSelectVideoDuration ~= model.second else {
return false
}
if (config.minSelectVideoDataSize > 0 || config.maxSelectVideoDataSize != .greatestFiniteMagnitude),
let dataSize = model.dataSize,
!(config.minSelectVideoDataSize...config.maxSelectVideoDataSize ~= dataSize) {
return false
}
return true
}
func ZLMainAsync(after: TimeInterval = 0, handler: @escaping (() -> Void)) {
if after > 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + after) {
handler()
}
} else {
if Thread.isMainThread {
handler()
} else {
DispatchQueue.main.async {
handler()
}
}
}
}
func zl_debugPrint(_ message: Any...) {
// message.forEach { debugPrint($0) }
}
func zlLoggerInDebug(_ lastMessage: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) {
#if DEBUG
print("\(file):\(line): \(lastMessage())")
#endif
}

View File

@@ -0,0 +1,71 @@
//
// ZLImageNavController.swift
// ZLPhotoBrowser
//
// Created by long on 2020/8/18.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import Photos
class ZLImageNavController: UINavigationController {
var isSelectedOriginal: Bool = false
var arrSelectedModels: [ZLPhotoModel] = []
var selectImageBlock: (() -> Void)?
var cancelBlock: (() -> Void)?
deinit {
zl_debugPrint("ZLImageNavController deinit")
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return ZLPhotoUIConfiguration.default().statusBarStyle
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nil, bundle: nil)
}
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
navigationBar.barStyle = .black
navigationBar.isTranslucent = true
modalPresentationStyle = .fullScreen
isNavigationBarHidden = true
}
@available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}

View File

@@ -0,0 +1,582 @@
//
// ZLImagePreviewController.swift
// ZLPhotoBrowser
//
// Created by long on 2020/10/22.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import Photos
@objc public enum ZLURLType: Int {
case image
case video
}
public typealias ZLImageLoaderBlock = (_ url: URL, _ imageView: UIImageView, _ progress: @escaping (CGFloat) -> Void, _ complete: @escaping () -> Void) -> Void
public class ZLImagePreviewController: UIViewController {
static let colItemSpacing: CGFloat = 40
static let selPhotoPreviewH: CGFloat = 100
private let datas: [Any]
private var selectStatus: [Bool]
private let urlType: ((URL) -> ZLURLType)?
private let urlImageLoader: ZLImageLoaderBlock?
private let showSelectBtn: Bool
private let showBottomView: Bool
private var currentIndex: Int
private var indexBeforOrientationChanged: Int
private lazy var collectionView: UICollectionView = {
let layout = ZLCollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.backgroundColor = .clear
view.dataSource = self
view.delegate = self
view.isPagingEnabled = true
view.showsHorizontalScrollIndicator = false
ZLPhotoPreviewCell.zl.register(view)
ZLGifPreviewCell.zl.register(view)
ZLLivePhotoPreviewCell.zl.register(view)
ZLVideoPreviewCell.zl.register(view)
ZLLocalImagePreviewCell.zl.register(view)
ZLNetImagePreviewCell.zl.register(view)
ZLNetVideoPreviewCell.zl.register(view)
return view
}()
private lazy var navView: UIView = {
let view = UIView()
view.backgroundColor = .zl.navBarColorOfPreviewVC
return view
}()
private var navBlurView: UIVisualEffectView?
private lazy var backBtn: UIButton = {
let btn = UIButton(type: .custom)
var image = UIImage.zl.getImage("zl_navBack")
if isRTL() {
image = image?.imageFlippedForRightToLeftLayoutDirection()
btn.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: -10)
} else {
btn.imageEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: 0, right: 0)
}
btn.setImage(image, for: .normal)
btn.addTarget(self, action: #selector(backBtnClick), for: .touchUpInside)
return btn
}()
private lazy var indexLabel: UILabel = {
let label = UILabel()
label.textColor = .zl.indexLabelTextColor
label.font = ZLLayout.navTitleFont
label.textAlignment = .center
return label
}()
private lazy var selectBtn: ZLEnlargeButton = {
let btn = ZLEnlargeButton(type: .custom)
btn.setImage(.zl.getImage("zl_btn_circle"), for: .normal)
btn.setImage(.zl.getImage("zl_btn_selected"), for: .selected)
btn.enlargeInset = 10
btn.addTarget(self, action: #selector(selectBtnClick), for: .touchUpInside)
return btn
}()
private lazy var bottomView: UIView = {
let view = UIView()
view.backgroundColor = .zl.bottomToolViewBgColorOfPreviewVC
return view
}()
private var bottomBlurView: UIVisualEffectView?
private lazy var doneBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.titleLabel?.font = ZLLayout.bottomToolTitleFont
btn.setTitle(title, for: .normal)
btn.setTitleColor(.zl.bottomToolViewDoneBtnNormalTitleColorOfPreviewVC, for: .normal)
btn.setTitleColor(.zl.bottomToolViewDoneBtnDisableTitleColorOfPreviewVC, for: .disabled)
btn.addTarget(self, action: #selector(doneBtnClick), for: .touchUpInside)
btn.backgroundColor = .zl.bottomToolViewBtnNormalBgColorOfPreviewVC
btn.layer.masksToBounds = true
btn.layer.cornerRadius = ZLLayout.bottomToolBtnCornerRadius
return btn
}()
private var isFirstAppear = true
private var hideNavView = false
private var orientation: UIInterfaceOrientation = .unknown
@objc public var longPressBlock: ((ZLImagePreviewController?, UIImage?, Int) -> Void)?
@objc public var doneBlock: (([Any]) -> Void)?
@objc public var videoHttpHeader: [String: Any]?
override public var prefersStatusBarHidden: Bool {
return !ZLPhotoUIConfiguration.default().showStatusBarInPreviewInterface
}
override public var preferredStatusBarStyle: UIStatusBarStyle {
return ZLPhotoUIConfiguration.default().statusBarStyle
}
deinit {
zl_debugPrint("ZLImagePreviewController deinit")
}
/// - Parameters:
/// - datas: Must be one of PHAsset, UIImage and URL, will filter others in init function.
/// - showBottomView: If showSelectBtn is true, showBottomView is always true.
/// - index: Index for first display.
/// - urlType: Tell me the url is image or video.
/// - urlImageLoader: Called when cell will display, cell will layout after callback when image load finish. The first block is progress callback, second is load finish callback.
@objc public init(
datas: [Any],
index: Int = 0,
showSelectBtn: Bool = true,
showBottomView: Bool = true,
urlType: ((URL) -> ZLURLType)? = nil,
urlImageLoader: ZLImageLoaderBlock? = nil
) {
let filterDatas = datas.filter { $0 is PHAsset || $0 is UIImage || $0 is URL }
self.datas = filterDatas
selectStatus = Array(repeating: true, count: filterDatas.count)
currentIndex = min(index, filterDatas.count - 1)
indexBeforOrientationChanged = currentIndex
self.showSelectBtn = showSelectBtn
self.showBottomView = showSelectBtn ? true : showBottomView
self.urlType = urlType
self.urlImageLoader = urlImageLoader
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func viewDidLoad() {
super.viewDidLoad()
setupUI()
resetSubViewStatus()
}
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.isHidden = true
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard isFirstAppear else {
return
}
isFirstAppear = false
reloadCurrentCell()
}
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
var insets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
if #available(iOS 11.0, *) {
insets = view.safeAreaInsets
}
insets.top = max(20, insets.top)
collectionView.frame = CGRect(
x: -ZLPhotoPreviewController.colItemSpacing / 2,
y: 0,
width: view.zl.width + ZLPhotoPreviewController.colItemSpacing,
height: view.zl.height
)
let navH = insets.top + 44
navView.frame = CGRect(x: 0, y: 0, width: view.zl.width, height: navH)
navBlurView?.frame = navView.bounds
indexLabel.frame = CGRect(x: (view.zl.width - 80) / 2, y: insets.top, width: 80, height: 44)
if isRTL() {
backBtn.frame = CGRect(x: view.zl.width - insets.right - 60, y: insets.top, width: 60, height: 44)
selectBtn.frame = CGRect(x: insets.left + 15, y: insets.top + (44 - 25) / 2, width: 25, height: 25)
} else {
backBtn.frame = CGRect(x: insets.left, y: insets.top, width: 60, height: 44)
selectBtn.frame = CGRect(x: view.zl.width - 40 - insets.right, y: insets.top + (44 - 25) / 2, width: 25, height: 25)
}
let bottomViewH = ZLLayout.bottomToolViewH
bottomView.frame = CGRect(x: 0, y: view.zl.height - insets.bottom - bottomViewH, width: view.zl.width, height: bottomViewH + insets.bottom)
bottomBlurView?.frame = bottomView.bounds
resetBottomViewFrame()
let ori = UIApplication.shared.statusBarOrientation
if ori != orientation {
orientation = ori
collectionView.setContentOffset(
CGPoint(
x: (view.zl.width + ZLPhotoPreviewController.colItemSpacing) * CGFloat(indexBeforOrientationChanged),
y: 0
),
animated: false
)
collectionView.performBatchUpdates({
self.collectionView.setContentOffset(
CGPoint(
x: (self.view.frame.width + ZLPhotoPreviewController.colItemSpacing) * CGFloat(self.indexBeforOrientationChanged),
y: 0
),
animated: false
)
})
}
}
private func reloadCurrentCell() {
guard let cell = collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)) else {
return
}
if let cell = cell as? ZLGifPreviewCell {
cell.loadGifWhenCellDisplaying()
} else if let cell = cell as? ZLLivePhotoPreviewCell {
cell.loadLivePhotoData()
}
}
private func setupUI() {
view.backgroundColor = .zl.previewVCBgColor
automaticallyAdjustsScrollViewInsets = false
view.addSubview(navView)
if let effect = ZLPhotoUIConfiguration.default().navViewBlurEffectOfPreview {
navBlurView = UIVisualEffectView(effect: effect)
navView.addSubview(navBlurView!)
}
navView.addSubview(backBtn)
navView.addSubview(indexLabel)
navView.addSubview(selectBtn)
view.addSubview(collectionView)
view.addSubview(bottomView)
if let effect = ZLPhotoUIConfiguration.default().bottomViewBlurEffectOfPreview {
bottomBlurView = UIVisualEffectView(effect: effect)
bottomView.addSubview(bottomBlurView!)
}
bottomView.addSubview(doneBtn)
view.bringSubviewToFront(navView)
}
private func resetSubViewStatus() {
indexLabel.text = String(currentIndex + 1) + " / " + String(datas.count)
if showSelectBtn {
selectBtn.isSelected = selectStatus[currentIndex]
} else {
selectBtn.isHidden = true
}
resetBottomViewFrame()
}
private func resetBottomViewFrame() {
guard showBottomView else {
bottomView.isHidden = true
return
}
let btnY = ZLLayout.bottomToolBtnY
var doneTitle = localLanguageTextValue(.done)
let selCount = selectStatus.filter { $0 }.count
if showSelectBtn,
ZLPhotoConfiguration.default().showSelectCountOnDoneBtn,
selCount > 0 {
doneTitle += "(" + String(selCount) + ")"
}
let doneBtnW = doneTitle.zl.boundingRect(font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 30)).width + 20
doneBtn.frame = CGRect(x: bottomView.bounds.width - doneBtnW - 15, y: btnY, width: doneBtnW, height: ZLLayout.bottomToolBtnH)
doneBtn.setTitle(doneTitle, for: .normal)
}
private func dismiss() {
if let nav = navigationController {
let vc = nav.popViewController(animated: true)
if vc == nil {
nav.dismiss(animated: true, completion: nil)
}
} else {
dismiss(animated: true, completion: nil)
}
}
// MARK: btn actions
@objc private func backBtnClick() {
dismiss()
}
@objc private func selectBtnClick() {
var isSelected = selectStatus[currentIndex]
selectBtn.layer.removeAllAnimations()
if isSelected {
isSelected = false
} else {
if ZLPhotoConfiguration.default().animateSelectBtnWhenSelect {
selectBtn.layer.add(ZLAnimationUtils.springAnimation(), forKey: nil)
}
isSelected = true
}
selectStatus[currentIndex] = isSelected
resetSubViewStatus()
}
@objc private func doneBtnClick() {
if showSelectBtn {
let res = datas.enumerated()
.filter { self.selectStatus[$0.offset] }
.map { $0.element }
doneBlock?(res)
} else {
doneBlock?(datas)
}
dismiss()
}
private func tapPreviewCell() {
hideNavView.toggle()
let cell = collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0))
if let cell = cell as? ZLVideoPreviewCell, cell.isPlaying {
hideNavView = true
}
navView.isHidden = hideNavView
if showBottomView {
bottomView.isHidden = hideNavView
}
}
}
// scroll view delegate
public extension ZLImagePreviewController {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView == collectionView else {
return
}
NotificationCenter.default.post(name: ZLPhotoPreviewController.previewVCScrollNotification, object: nil)
let offset = scrollView.contentOffset
var page = Int(round(offset.x / (view.bounds.width + ZLPhotoPreviewController.colItemSpacing)))
page = max(0, min(page, datas.count - 1))
if page == currentIndex {
return
}
currentIndex = page
resetSubViewStatus()
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
indexBeforOrientationChanged = currentIndex
let cell = collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0))
if let cell = cell as? ZLGifPreviewCell {
cell.loadGifWhenCellDisplaying()
} else if let cell = cell as? ZLLivePhotoPreviewCell {
cell.loadLivePhotoData()
}
}
}
extension ZLImagePreviewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return ZLImagePreviewController.colItemSpacing
}
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return ZLImagePreviewController.colItemSpacing
}
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: ZLImagePreviewController.colItemSpacing / 2, bottom: 0, right: ZLImagePreviewController.colItemSpacing / 2)
}
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.zl.width, height: view.zl.height)
}
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return datas.count
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let config = ZLPhotoConfiguration.default()
let obj = datas[indexPath.row]
let baseCell: ZLPreviewBaseCell
if let asset = obj as? PHAsset {
let model = ZLPhotoModel(asset: asset)
if config.allowSelectGif, model.type == .gif {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLGifPreviewCell.zl.identifier, for: indexPath) as! ZLGifPreviewCell
cell.singleTapBlock = { [weak self] in
self?.tapPreviewCell()
}
cell.model = model
baseCell = cell
} else if config.allowSelectLivePhoto, model.type == .livePhoto {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLLivePhotoPreviewCell.zl.identifier, for: indexPath) as! ZLLivePhotoPreviewCell
cell.model = model
baseCell = cell
} else if config.allowSelectVideo, model.type == .video {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLVideoPreviewCell.zl.identifier, for: indexPath) as! ZLVideoPreviewCell
cell.model = model
baseCell = cell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLPhotoPreviewCell.zl.identifier, for: indexPath) as! ZLPhotoPreviewCell
cell.singleTapBlock = { [weak self] in
self?.tapPreviewCell()
}
cell.model = model
baseCell = cell
}
return baseCell
} else if let image = obj as? UIImage {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLLocalImagePreviewCell.zl.identifier, for: indexPath) as! ZLLocalImagePreviewCell
cell.image = image
baseCell = cell
} else if let url = obj as? URL {
let type: ZLURLType = urlType?(url) ?? .image
if type == .image {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLNetImagePreviewCell.zl.identifier, for: indexPath) as! ZLNetImagePreviewCell
cell.image = nil
urlImageLoader?(url, cell.preview.imageView, { [weak cell] progress in
ZLMainAsync {
cell?.progress = progress
}
}, { [weak cell] in
ZLMainAsync {
cell?.preview.resetSubViewSize()
}
})
baseCell = cell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLNetVideoPreviewCell.zl.identifier, for: indexPath) as! ZLNetVideoPreviewCell
cell.configureCell(videoUrl: url, httpHeader: videoHttpHeader)
baseCell = cell
}
} else {
#if DEBUG
fatalError("Preview obj must one of PHAsset, UIImage, URL")
#else
return UICollectionViewCell()
#endif
}
baseCell.singleTapBlock = { [weak self] in
self?.tapPreviewCell()
}
(baseCell as? ZLLocalImagePreviewCell)?.longPressBlock = { [weak self, weak baseCell] in
if let callback = self?.longPressBlock {
callback(self, baseCell?.currentImage, indexPath.row)
} else {
self?.showSaveImageAlert()
}
}
return baseCell
}
public func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if let cell = cell as? ZLPreviewBaseCell {
cell.resetSubViewStatusWhenCellEndDisplay()
}
}
private func showSaveImageAlert() {
func saveImage() {
guard let cell = collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)) as? ZLLocalImagePreviewCell, let image = cell.currentImage else {
return
}
let hud = ZLProgressHUD.show()
ZLPhotoManager.saveImageToAlbum(image: image) { [weak self] suc, _ in
hud.hide()
if !suc {
showAlertView(localLanguageTextValue(.saveImageError), self)
}
}
}
let saveAction = ZLCustomAlertAction(title: localLanguageTextValue(.save), style: .default) { _ in
saveImage()
}
let cancelAction = ZLCustomAlertAction(title: localLanguageTextValue(.cancel), style: .cancel, handler: nil)
showAlertController(title: nil, message: "", style: .actionSheet, actions: [saveAction, cancelAction], sender: self)
}
}

View File

@@ -0,0 +1,322 @@
//
// ZLLanguageDefine.swift
// ZLPhotoBrowser
//
// Created by long on 2020/8/17.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
@objc public enum ZLLanguageType: Int {
case system
case chineseSimplified
case chineseTraditional
case english
case japanese
case french
case german
case russian
case vietnamese
case korean
case malay
case italian
case indonesian
case portuguese
case spanish
case turkish
case arabic
var key: String {
var key = "en"
switch self {
case .system:
key = Locale.preferredLanguages.first ?? "en"
if key.hasPrefix("zh") {
if key.range(of: "Hans") != nil {
key = "zh-Hans"
} else {
key = "zh-Hant"
}
} else if key.hasPrefix("ja") {
key = "ja-US"
} else if key.hasPrefix("fr") {
key = "fr"
} else if key.hasPrefix("de") {
key = "de"
} else if key.hasPrefix("ru") {
key = "ru"
} else if key.hasPrefix("vi") {
key = "vi"
} else if key.hasPrefix("ko") {
key = "ko"
} else if key.hasPrefix("ms") {
key = "ms"
} else if key.hasPrefix("it") {
key = "it"
} else if key.hasPrefix("id") {
key = "id"
} else if key.hasPrefix("pt") {
key = "pt-BR"
} else if key.hasPrefix("es") {
key = "es-419"
} else if key.hasPrefix("tr") {
key = "tr"
} else if key.hasPrefix("ar") {
key = "ar"
} else {
key = "en"
}
case .chineseSimplified:
key = "zh-Hans"
case .chineseTraditional:
key = "zh-Hant"
case .english:
key = "en"
case .japanese:
key = "ja-US"
case .french:
key = "fr"
case .german:
key = "de"
case .russian:
key = "ru"
case .vietnamese:
key = "vi"
case .korean:
key = "ko"
case .malay:
key = "ms"
case .italian:
key = "it"
case .indonesian:
key = "id"
case .portuguese:
key = "pt-BR"
case .spanish:
key = "es-419"
case .turkish:
key = "tr"
case .arabic:
key = "ar"
}
return key
}
}
public struct ZLLocalLanguageKey: Hashable {
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
/// Camera ()
public static let previewCamera = ZLLocalLanguageKey(rawValue: "previewCamera")
/// Record ()
public static let previewCameraRecord = ZLLocalLanguageKey(rawValue: "previewCameraRecord")
/// Album ()
public static let previewAlbum = ZLLocalLanguageKey(rawValue: "previewAlbum")
/// Cancel ()
public static let cancel = ZLLocalLanguageKey(rawValue: "cancel")
/// No Photo ()
public static let noPhotoTips = ZLLocalLanguageKey(rawValue: "noPhotoTips")
/// waiting... (...)
public static let hudLoading = ZLLocalLanguageKey(rawValue: "hudLoading")
/// Done ()
public static let done = ZLLocalLanguageKey(rawValue: "done")
/// OK ()
public static let ok = ZLLocalLanguageKey(rawValue: "ok")
/// Request timed out ()
public static let timeout = ZLLocalLanguageKey(rawValue: "timeout")
/// Please Allow %@ to access your album in \"Settings\"->\"Privacy\"->\"Photos\"
/// (iPhone\"--\"%@访)
public static let noPhotoLibratyAuthority = ZLLocalLanguageKey(rawValue: "noPhotoLibratyAuthority")
/// Please allow %@ to access your device's camera in \"Settings\"->\"Privacy\"->\"Camera\"
/// (iPhone\"--\"%@访)
public static let noCameraAuthority = ZLLocalLanguageKey(rawValue: "noCameraAuthority")
/// Unable to record audio. Go to \"Settings\" > \"%@\" and enable microphone access.
/// (\" > %@\")
public static let noMicrophoneAuthority = ZLLocalLanguageKey(rawValue: "noMicrophoneAuthority")
/// Camera is unavailable ()
public static let cameraUnavailable = ZLLocalLanguageKey(rawValue: "cameraUnavailable")
/// Keep Recording ()
public static let keepRecording = ZLLocalLanguageKey(rawValue: "keepRecording")
/// Go to Settings ()
public static let gotoSettings = ZLLocalLanguageKey(rawValue: "gotoSettings")
/// Photos ()
public static let photo = ZLLocalLanguageKey(rawValue: "photo")
/// Full Image ()
public static let originalPhoto = ZLLocalLanguageKey(rawValue: "originalPhoto")
/// Back ()
public static let back = ZLLocalLanguageKey(rawValue: "back")
/// Edit ()
public static let edit = ZLLocalLanguageKey(rawValue: "edit")
/// Done ()
public static let editFinish = ZLLocalLanguageKey(rawValue: "editFinish")
/// Undo ()
public static let revert = ZLLocalLanguageKey(rawValue: "revert")
/// Brightness ()
public static let brightness = ZLLocalLanguageKey(rawValue: "brightness")
/// Contrast ()
public static let contrast = ZLLocalLanguageKey(rawValue: "contrast")
/// Saturation ()
public static let saturation = ZLLocalLanguageKey(rawValue: "saturation")
/// Preview ()
public static let preview = ZLLocalLanguageKey(rawValue: "preview")
/// Save ()
public static let save = ZLLocalLanguageKey(rawValue: "save")
/// Failed to save the image ()
public static let saveImageError = ZLLocalLanguageKey(rawValue: "saveImageError")
/// Failed to save the video ()
public static let saveVideoError = ZLLocalLanguageKey(rawValue: "saveVideoError")
/// Max select count: %ld (%ld)
public static let exceededMaxSelectCount = ZLLocalLanguageKey(rawValue: "exceededMaxSelectCount")
/// Max count for video selection: %ld (%ld)
public static let exceededMaxVideoSelectCount = ZLLocalLanguageKey(rawValue: "exceededMaxVideoSelectCount")
/// Min count for video selection: %ld (%ld)
public static let lessThanMinVideoSelectCount = ZLLocalLanguageKey(rawValue: "lessThanMinVideoSelectCount")
/// Can't select videos longer than %lds
/// (%ld)
public static let longerThanMaxVideoDuration = ZLLocalLanguageKey(rawValue: "longerThanMaxVideoDuration")
/// Can't select videos shorter than %lds
/// (%ld)
public static let shorterThanMinVideoDuration = ZLLocalLanguageKey(rawValue: "shorterThanMinVideoDuration")
/// Can't select videos larger than %@MB
/// (%@MB)
public static let largerThanMaxVideoDataSize = ZLLocalLanguageKey(rawValue: "largerThanMaxVideoDataSize")
/// Can't select videos smaller than %@MB
/// (%@MB)
public static let smallerThanMinVideoDataSize = ZLLocalLanguageKey(rawValue: "smallerThanMinVideoDataSize")
/// Unable to sync from iCloud (iCloud)
public static let iCloudVideoLoadFaild = ZLLocalLanguageKey(rawValue: "iCloudVideoLoadFaild")
/// loading failed ()
public static let imageLoadFailed = ZLLocalLanguageKey(rawValue: "imageLoadFailed")
/// Tap to take photo and hold to record video ()
public static let customCameraTips = ZLLocalLanguageKey(rawValue: "customCameraTips")
/// Tap to take photo ()
public static let customCameraTakePhotoTips = ZLLocalLanguageKey(rawValue: "customCameraTakePhotoTips")
/// hold to record video ()
public static let customCameraRecordVideoTips = ZLLocalLanguageKey(rawValue: "customCameraRecordVideoTips")
/// Record at least %lds (%ld)
public static let minRecordTimeTips = ZLLocalLanguageKey(rawValue: "minRecordTimeTips")
/// Recents ()
public static let cameraRoll = ZLLocalLanguageKey(rawValue: "cameraRoll")
/// Panoramas ()
public static let panoramas = ZLLocalLanguageKey(rawValue: "panoramas")
/// Videos ()
public static let videos = ZLLocalLanguageKey(rawValue: "videos")
/// Favorites ()
public static let favorites = ZLLocalLanguageKey(rawValue: "favorites")
/// Time-Lapse ()
public static let timelapses = ZLLocalLanguageKey(rawValue: "timelapses")
/// Recently Added ()
public static let recentlyAdded = ZLLocalLanguageKey(rawValue: "recentlyAdded")
/// Bursts ()
public static let bursts = ZLLocalLanguageKey(rawValue: "bursts")
/// Slo-mo ()
public static let slomoVideos = ZLLocalLanguageKey(rawValue: "slomoVideos")
/// Selfies ()
public static let selfPortraits = ZLLocalLanguageKey(rawValue: "selfPortraits")
/// Screenshots ()
public static let screenshots = ZLLocalLanguageKey(rawValue: "screenshots")
/// Portrait ()
public static let depthEffect = ZLLocalLanguageKey(rawValue: "depthEffect")
/// Live Photo
public static let livePhotos = ZLLocalLanguageKey(rawValue: "livePhotos")
/// Animated ()
public static let animated = ZLLocalLanguageKey(rawValue: "animated")
/// My Photo Stream ()
public static let myPhotoStream = ZLLocalLanguageKey(rawValue: "myPhotoStream")
/// All Photos ()
public static let noTitleAlbumListPlaceholder = ZLLocalLanguageKey(rawValue: "noTitleAlbumListPlaceholder")
/// Unable to access all photos, go to settings (访)
public static let unableToAccessAllPhotos = ZLLocalLanguageKey(rawValue: "unableToAccessAllPhotos")
/// Drag here to remove ()
public static let textStickerRemoveTips = ZLLocalLanguageKey(rawValue: "textStickerRemoveTips")
}
func localLanguageTextValue(_ key: ZLLocalLanguageKey) -> String {
if let value = ZLCustomLanguageDeploy.deploy[key] {
return value
}
return Bundle.zlLocalizedString(key.rawValue)
}

View File

@@ -0,0 +1,75 @@
//
// ZLPhotoBrowser.swift
// ZLPhotoBrowser
//
// Created by long on 2020/9/2.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import Foundation
import Photos
let version = "4.4.3.2"
public struct ZLPhotoBrowserWrapper<Base> {
public let base: Base
public init(_ base: Base) {
self.base = base
}
}
public protocol ZLPhotoBrowserCompatible: AnyObject { }
public protocol ZLPhotoBrowserCompatibleValue { }
extension ZLPhotoBrowserCompatible {
public var zl: ZLPhotoBrowserWrapper<Self> {
get { ZLPhotoBrowserWrapper(self) }
set { }
}
public static var zl: ZLPhotoBrowserWrapper<Self>.Type {
get { ZLPhotoBrowserWrapper<Self>.self }
set { }
}
}
extension ZLPhotoBrowserCompatibleValue {
public var zl: ZLPhotoBrowserWrapper<Self> {
get { ZLPhotoBrowserWrapper(self) }
set { }
}
}
extension UIViewController: ZLPhotoBrowserCompatible { }
extension UIColor: ZLPhotoBrowserCompatible { }
extension UIImage: ZLPhotoBrowserCompatible { }
extension CIImage: ZLPhotoBrowserCompatible { }
extension PHAsset: ZLPhotoBrowserCompatible { }
extension UIFont: ZLPhotoBrowserCompatible { }
extension UIView: ZLPhotoBrowserCompatible { }
extension Array: ZLPhotoBrowserCompatibleValue { }
extension String: ZLPhotoBrowserCompatibleValue { }
extension CGFloat: ZLPhotoBrowserCompatibleValue { }
extension Bool: ZLPhotoBrowserCompatibleValue { }

View File

@@ -0,0 +1,366 @@
//
// ZLPhotoConfiguration+Chaining.swift
// ZLPhotoBrowser
//
// Created by long on 2021/11/1.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import Photos
public extension ZLPhotoConfiguration {
@discardableResult
func sortAscending(_ ascending: Bool) -> ZLPhotoConfiguration {
sortAscending = ascending
return self
}
@discardableResult
func maxSelectCount(_ count: Int) -> ZLPhotoConfiguration {
maxSelectCount = count
return self
}
@discardableResult
func maxVideoSelectCount(_ count: Int) -> ZLPhotoConfiguration {
maxVideoSelectCount = count
return self
}
@discardableResult
func minVideoSelectCount(_ count: Int) -> ZLPhotoConfiguration {
minVideoSelectCount = count
return self
}
@discardableResult
func allowMixSelect(_ value: Bool) -> ZLPhotoConfiguration {
allowMixSelect = value
return self
}
@discardableResult
func maxPreviewCount(_ count: Int) -> ZLPhotoConfiguration {
maxPreviewCount = count
return self
}
@discardableResult
func allowSelectImage(_ value: Bool) -> ZLPhotoConfiguration {
allowSelectImage = value
return self
}
@discardableResult
@objc func allowSelectVideo(_ value: Bool) -> ZLPhotoConfiguration {
allowSelectVideo = value
return self
}
@discardableResult
func allowSelectGif(_ value: Bool) -> ZLPhotoConfiguration {
allowSelectGif = value
return self
}
@discardableResult
func allowSelectLivePhoto(_ value: Bool) -> ZLPhotoConfiguration {
allowSelectLivePhoto = value
return self
}
@discardableResult
func allowTakePhotoInLibrary(_ value: Bool) -> ZLPhotoConfiguration {
allowTakePhotoInLibrary = value
return self
}
@discardableResult
func callbackDirectlyAfterTakingPhoto(_ value: Bool) -> ZLPhotoConfiguration {
callbackDirectlyAfterTakingPhoto = value
return self
}
@discardableResult
func allowEditImage(_ value: Bool) -> ZLPhotoConfiguration {
allowEditImage = value
return self
}
@discardableResult
func allowEditVideo(_ value: Bool) -> ZLPhotoConfiguration {
allowEditVideo = value
return self
}
@discardableResult
func animateSelectBtnWhenSelect(_ animate: Bool) -> ZLPhotoConfiguration {
animateSelectBtnWhenSelect = animate
return self
}
@discardableResult
func selectBtnAnimationDuration(_ duration: CFTimeInterval) -> ZLPhotoConfiguration {
selectBtnAnimationDuration = duration
return self
}
@discardableResult
func editAfterSelectThumbnailImage(_ value: Bool) -> ZLPhotoConfiguration {
editAfterSelectThumbnailImage = value
return self
}
@discardableResult
func cropVideoAfterSelectThumbnail(_ value: Bool) -> ZLPhotoConfiguration {
cropVideoAfterSelectThumbnail = value
return self
}
@discardableResult
func showClipDirectlyIfOnlyHasClipTool(_ value: Bool) -> ZLPhotoConfiguration {
showClipDirectlyIfOnlyHasClipTool = value
return self
}
@discardableResult
func saveNewImageAfterEdit(_ value: Bool) -> ZLPhotoConfiguration {
saveNewImageAfterEdit = value
return self
}
@discardableResult
func allowSlideSelect(_ value: Bool) -> ZLPhotoConfiguration {
allowSlideSelect = value
return self
}
@discardableResult
func autoScrollWhenSlideSelectIsActive(_ value: Bool) -> ZLPhotoConfiguration {
autoScrollWhenSlideSelectIsActive = value
return self
}
@discardableResult
func autoScrollMaxSpeed(_ speed: CGFloat) -> ZLPhotoConfiguration {
autoScrollMaxSpeed = speed
return self
}
@discardableResult
func allowDragSelect(_ value: Bool) -> ZLPhotoConfiguration {
allowDragSelect = value
return self
}
@discardableResult
func allowSelectOriginal(_ value: Bool) -> ZLPhotoConfiguration {
allowSelectOriginal = value
return self
}
@discardableResult
func alwaysRequestOriginal(_ value: Bool) -> ZLPhotoConfiguration {
alwaysRequestOriginal = value
return self
}
@discardableResult
func allowPreviewPhotos(_ value: Bool) -> ZLPhotoConfiguration {
allowPreviewPhotos = value
return self
}
@discardableResult
func showPreviewButtonInAlbum(_ value: Bool) -> ZLPhotoConfiguration {
showPreviewButtonInAlbum = value
return self
}
@discardableResult
func showSelectCountOnDoneBtn(_ value: Bool) -> ZLPhotoConfiguration {
showSelectCountOnDoneBtn = value
return self
}
@discardableResult
func maxEditVideoTime(_ second: Second) -> ZLPhotoConfiguration {
maxEditVideoTime = second
return self
}
@discardableResult
func maxSelectVideoDuration(_ duration: Second) -> ZLPhotoConfiguration {
maxSelectVideoDuration = duration
return self
}
@discardableResult
func minSelectVideoDuration(_ duration: Second) -> ZLPhotoConfiguration {
minSelectVideoDuration = duration
return self
}
@discardableResult
func maxSelectVideoDataSize(_ size: ZLPhotoConfiguration.KBUnit) -> ZLPhotoConfiguration {
maxSelectVideoDataSize = size
return self
}
@discardableResult
func minSelectVideoDataSize(_ size: ZLPhotoConfiguration.KBUnit) -> ZLPhotoConfiguration {
minSelectVideoDataSize = size
return self
}
@discardableResult
func editImageConfiguration(_ configuration: ZLEditImageConfiguration) -> ZLPhotoConfiguration {
editImageConfiguration = configuration
return self
}
@discardableResult
func showCaptureImageOnTakePhotoBtn(_ value: Bool) -> ZLPhotoConfiguration {
showCaptureImageOnTakePhotoBtn = value
return self
}
@discardableResult
func showSelectBtnWhenSingleSelect(_ value: Bool) -> ZLPhotoConfiguration {
showSelectBtnWhenSingleSelect = value
return self
}
@discardableResult
func showSelectedMask(_ value: Bool) -> ZLPhotoConfiguration {
showSelectedMask = value
return self
}
@discardableResult
func showSelectedBorder(_ value: Bool) -> ZLPhotoConfiguration {
showSelectedBorder = value
return self
}
@discardableResult
func showInvalidMask(_ value: Bool) -> ZLPhotoConfiguration {
showInvalidMask = value
return self
}
@discardableResult
func showSelectedIndex(_ value: Bool) -> ZLPhotoConfiguration {
showSelectedIndex = value
return self
}
@discardableResult
func showSelectedPhotoPreview(_ value: Bool) -> ZLPhotoConfiguration {
showSelectedPhotoPreview = value
return self
}
@discardableResult
func timeout(_ timeout: TimeInterval) -> ZLPhotoConfiguration {
self.timeout = timeout
return self
}
@discardableResult
func useCustomCamera(_ value: Bool) -> ZLPhotoConfiguration {
useCustomCamera = value
return self
}
@discardableResult
func cameraConfiguration(_ configuration: ZLCameraConfiguration) -> ZLPhotoConfiguration {
cameraConfiguration = configuration
return self
}
@discardableResult
func canSelectAsset(_ block: ((PHAsset) -> Bool)?) -> ZLPhotoConfiguration {
canSelectAsset = block
return self
}
@discardableResult
func didSelectAsset(_ block: ((PHAsset) -> Void)?) -> ZLPhotoConfiguration {
didSelectAsset = block
return self
}
@discardableResult
func didDeselectAsset(_ block: ((PHAsset) -> Void)?) -> ZLPhotoConfiguration {
didDeselectAsset = block
return self
}
@discardableResult
func showAddPhotoButton(_ value: Bool) -> ZLPhotoConfiguration {
showAddPhotoButton = value
return self
}
@discardableResult
func showEnterSettingTips(_ value: Bool) -> ZLPhotoConfiguration {
showEnterSettingTips = value
return self
}
@discardableResult
func maxFrameCountForGIF(_ frameCount: Int) -> ZLPhotoConfiguration {
maxFrameCountForGIF = frameCount
return self
}
@discardableResult
func gifPlayBlock(_ block: ((UIImageView, Data, [AnyHashable: Any]?) -> Void)?) -> ZLPhotoConfiguration {
gifPlayBlock = block
return self
}
@discardableResult
func pauseGIFBlock(_ block: ((UIImageView) -> Void)?) -> ZLPhotoConfiguration {
pauseGIFBlock = block
return self
}
@discardableResult
func resumeGIFBlock(_ block: ((UIImageView) -> Void)?) -> ZLPhotoConfiguration {
resumeGIFBlock = block
return self
}
@discardableResult
func noAuthorityCallback(_ callback: ((ZLNoAuthorityType) -> Void)?) -> ZLPhotoConfiguration {
noAuthorityCallback = callback
return self
}
@discardableResult
func operateBeforeDoneAction(_ block: ((UIViewController, @escaping () -> Void) -> Void)?) -> ZLPhotoConfiguration {
operateBeforeDoneAction = block
return self
}
}

View File

@@ -0,0 +1,286 @@
//
// ZLPhotoConfiguration.swift
// ZLPhotoBrowser
//
// Created by long on 2020/8/11.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import Photos
@objcMembers
public class ZLPhotoConfiguration: NSObject {
public typealias Second = Int
public typealias KBUnit = CGFloat
private static var single = ZLPhotoConfiguration()
public class func `default`() -> ZLPhotoConfiguration {
ZLPhotoConfiguration.single
}
public class func resetConfiguration() {
ZLPhotoConfiguration.single = ZLPhotoConfiguration()
}
/// Photo sorting method, the preview interface is not affected by this parameter. Defaults to true.
public var sortAscending = true
private var pri_maxSelectCount = 9
/// Anything superior than 1 will enable the multiple selection feature. Defaults to 9.
public var maxSelectCount: Int {
get {
pri_maxSelectCount
}
set {
pri_maxSelectCount = max(1, newValue)
}
}
private var pri_maxVideoSelectCount = 0
/// A count for video max selection. Defaults to 0.
/// - warning: Only valid in mix selection mode. (i.e. allowMixSelect = true)
public var maxVideoSelectCount: Int {
get {
if pri_maxVideoSelectCount <= 0 {
return maxSelectCount
} else {
return max(minVideoSelectCount, min(pri_maxVideoSelectCount, maxSelectCount))
}
}
set {
pri_maxVideoSelectCount = newValue
}
}
private var pri_minVideoSelectCount = 0
/// A count for video min selection. Defaults to 0.
/// - warning: Only valid in mix selection mode. (i.e. allowMixSelect = true)
public var minVideoSelectCount: Int {
get {
min(maxSelectCount, max(pri_minVideoSelectCount, 0))
}
set {
pri_minVideoSelectCount = newValue
}
}
/// Whether photos and videos can be selected together. Defaults to true.
/// If set to false, only one video can be selected. Defaults to true.
public var allowMixSelect = true
/// Preview selection max preview count, if the value is zero, only show `Camera`, `Album`, `Cancel` buttons. Defaults to 20.
public var maxPreviewCount = 20
/// If set to false, gif and livephoto cannot be selected either. Defaults to true.
public var allowSelectImage = true
public var allowSelectVideo = true
/// Allow select Gif, it only controls whether it is displayed in Gif form.
/// If value is false, the Gif logo is not displayed. Defaults to true.
public var allowSelectGif = true
/// Allow select LivePhoto, it only controls whether it is displayed in LivePhoto form.
/// If value is false, the LivePhoto logo is not displayed. Defaults to false.
public var allowSelectLivePhoto = false
private var pri_allowTakePhotoInLibrary = true
/// Allow take photos in the album. Defaults to true.
/// - warning: If allowTakePhoto and allowRecordVideo are both false, it will not be displayed.
public var allowTakePhotoInLibrary: Bool {
get {
pri_allowTakePhotoInLibrary && (cameraConfiguration.allowTakePhoto || cameraConfiguration.allowRecordVideo)
}
set {
pri_allowTakePhotoInLibrary = newValue
}
}
/// Whether to callback directly after taking a photo. Defaults to false.
public var callbackDirectlyAfterTakingPhoto = false
private var pri_allowEditImage = true
public var allowEditImage: Bool {
get {
pri_allowEditImage
}
set {
pri_allowEditImage = newValue
}
}
/// - warning: The video can only be edited when no photos are selected, or only one video is selected, and the selection callback is executed immediately after editing is completed.
private var pri_allowEditVideo = false
public var allowEditVideo: Bool {
get {
pri_allowEditVideo
}
set {
pri_allowEditVideo = newValue
}
}
/// Control whether to display the selection button animation when selecting. Defaults to true.
public var animateSelectBtnWhenSelect = true
/// Animation duration for select button
public var selectBtnAnimationDuration: CFTimeInterval = 0.4
/// After selecting a image/video in the thumbnail interface, enter the editing interface directly. Defaults to false.
/// - discussion: Editing image is only valid when allowEditImage is true and maxSelectCount is 1.
/// Editing video is only valid when allowEditVideo is true and maxSelectCount is 1.
public var editAfterSelectThumbnailImage = false
/// Only valid when allowMixSelect is false and allowEditVideo is true. Defaults to true.
/// Just like the Wechat-Timeline selection style. If you want to crop the video after select thumbnail under allowMixSelect = true, please use **editAfterSelectThumbnailImage**.
public var cropVideoAfterSelectThumbnail = true
/// If image edit tools only has clip and this property is true. When you click edit, the cropping interface (i.e. ZLClipImageViewController) will be displayed. Defaults to false.
public var showClipDirectlyIfOnlyHasClipTool = false
/// Save the edited image to the album after editing. Defaults to true.
public var saveNewImageAfterEdit = true
/// If true, you can slide select photos in album. Defaults to true.
public var allowSlideSelect = true
/// When slide select is active, will auto scroll to top or bottom when your finger at the top or bottom. Defaults to true.
public var autoScrollWhenSlideSelectIsActive = true
/// The max speed (pt/s) of auto scroll. Defaults to 600.
public var autoScrollMaxSpeed: CGFloat = 600
/// If true, you can drag select photo when preview selection style. Defaults to false.
public var allowDragSelect = false
/// Allow select full image. Defaults to true.
public var allowSelectOriginal = true
/// Always return the original photo.
/// - warning: Only valid when `allowSelectOriginal = false`, Defaults to false.
public var alwaysRequestOriginal = false
/// Allow access to the preview large image interface (That is, whether to allow access to the large image interface after clicking the thumbnail image). Defaults to true.
public var allowPreviewPhotos = true
/// Whether to show the preview button (i.e. the preview button in the lower left corner of the thumbnail interface). Defaults to true.
public var showPreviewButtonInAlbum = true
/// Whether to display the selected count on the button. Defaults to true.
public var showSelectCountOnDoneBtn = true
/// Maximum cropping time when editing video, unit: second. Defaults to 10.
public var maxEditVideoTime: ZLPhotoConfiguration.Second = 10
/// Allow to choose the maximum duration of the video. Defaults to 120.
public var maxSelectVideoDuration: ZLPhotoConfiguration.Second = 120
/// Allow to choose the minimum duration of the video. Defaults to 0.
public var minSelectVideoDuration: ZLPhotoConfiguration.Second = 0
/// Allow to choose the maximum data size of the video. Defaults to infinite.
public var maxSelectVideoDataSize: ZLPhotoConfiguration.KBUnit = .greatestFiniteMagnitude
/// Allow to choose the minimum data size of the video. Defaults to 0 KB.
public var minSelectVideoDataSize: ZLPhotoConfiguration.KBUnit = 0
/// Image editor configuration.
public var editImageConfiguration = ZLEditImageConfiguration()
/// Show the image captured by the camera is displayed on the camera button inside the album. Defaults to false.
public var showCaptureImageOnTakePhotoBtn = false
/// In single selection mode, whether to display the selection button. Defaults to false.
public var showSelectBtnWhenSingleSelect = false
/// Overlay a mask layer on top of the selected photos. Defaults to true.
public var showSelectedMask = true
/// Display a border on the selected photos cell. Defaults to false.
public var showSelectedBorder = false
/// Overlay a mask layer above the cells that cannot be selected. Defaults to true.
public var showInvalidMask = true
/// Display the index of the selected photos. Defaults to true.
public var showSelectedIndex = true
/// Display the selected photos at the bottom of the preview large photos interface. Defaults to true.
public var showSelectedPhotoPreview = true
/// Timeout for image parsing. Defaults to 20.
public var timeout: TimeInterval = 20
/// Whether to use custom camera. Defaults to true.
public var useCustomCamera = true
/// The configuration for camera.
public var cameraConfiguration = ZLCameraConfiguration()
/// This block will be called before selecting an image, the developer can first determine whether the asset is allowed to be selected.
/// Only control whether it is allowed to be selected, and will not affect the selection logic in the framework.
/// - Tips: If the choice is not allowed, the developer can toast prompt the user for relevant information.
public var canSelectAsset: ((PHAsset) -> Bool)?
/// This block will be called when selecting an asset.
public var didSelectAsset: ((PHAsset) -> Void)?
/// This block will be called when cancel selecting an asset.
public var didDeselectAsset: ((PHAsset) -> Void)?
/// If user choose limited Photo mode, a button with '+' will be added to the ZLThumbnailViewController. It will call PHPhotoLibrary.shared().presentLimitedLibraryPicker(from:) to add photo. Defaults to true.
/// E.g., Sina Weibo's ImagePicker
public var showAddPhotoButton = true
/// iOS14 limited Photo mode, will show collection footer view in ZLThumbnailViewController.
/// Will go to system setting if clicked. Defaults to true.
public var showEnterSettingTips = true
/// The maximum number of frames for GIF images. To avoid crashes due to memory spikes caused by loading GIF images with too many frames, it is recommended that this value is not too large. Defaults to 50.
public var maxFrameCountForGIF = 50
/// You can use this block to customize the playback of GIF images to achieve better results. For example, use FLAnimatedImage to play GIFs. Defaults to nil.
public var gifPlayBlock: ((UIImageView, Data, [AnyHashable: Any]?) -> Void)?
/// Pause GIF image playback, used together with gifPlayBlock. Defaults to nil.
public var pauseGIFBlock: ((UIImageView) -> Void)?
/// Resume GIF image playback, used together with gifPlayBlock. Defaults to nil.
public var resumeGIFBlock: ((UIImageView) -> Void)?
/// Callback after the no authority alert dismiss.
public var noAuthorityCallback: ((ZLNoAuthorityType) -> Void)?
/// Allow user to do something before select photo result callback.
/// And you must call the second parameter of this block to continue the photos selection.
/// The first parameter is the current controller.
/// The second parameter is the block that needs to be called after the user completes the operation.
public var operateBeforeDoneAction: ((UIViewController, @escaping () -> Void) -> Void)?
}
@objc public enum ZLNoAuthorityType: Int {
case library
case camera
case microphone
}

View File

@@ -0,0 +1,462 @@
//
// ZLPhotoManager.swift
// ZLPhotoBrowser
//
// Created by long on 2020/8/11.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import Photos
@objcMembers
public class ZLPhotoManager: NSObject {
/// Save image to album.
public class func saveImageToAlbum(image: UIImage, completion: ((Bool, PHAsset?) -> Void)?) {
let status = PHPhotoLibrary.authorizationStatus()
if status == .denied || status == .restricted {
completion?(false, nil)
return
}
var placeholderAsset: PHObjectPlaceholder?
let completionHandler: ((Bool, Error?) -> Void) = { suc, _ in
ZLMainAsync {
if suc {
let asset = self.getAsset(from: placeholderAsset?.localIdentifier)
completion?(suc, asset)
} else {
completion?(false, nil)
}
}
}
if image.zl.hasAlphaChannel(), let data = image.pngData() {
PHPhotoLibrary.shared().performChanges({
let newAssetRequest = PHAssetCreationRequest.forAsset()
newAssetRequest.addResource(with: .photo, data: data, options: nil)
placeholderAsset = newAssetRequest.placeholderForCreatedAsset
}, completionHandler: completionHandler)
} else {
PHPhotoLibrary.shared().performChanges({
let newAssetRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
placeholderAsset = newAssetRequest.placeholderForCreatedAsset
}, completionHandler: completionHandler)
}
}
/// Save video to album.
public class func saveVideoToAlbum(url: URL, completion: ((Bool, PHAsset?) -> Void)?) {
let status = PHPhotoLibrary.authorizationStatus()
if status == .denied || status == .restricted {
completion?(false, nil)
return
}
var placeholderAsset: PHObjectPlaceholder?
PHPhotoLibrary.shared().performChanges({
let newAssetRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
placeholderAsset = newAssetRequest?.placeholderForCreatedAsset
}) { suc, _ in
ZLMainAsync {
if suc {
let asset = self.getAsset(from: placeholderAsset?.localIdentifier)
completion?(suc, asset)
} else {
completion?(false, nil)
}
}
}
}
private class func getAsset(from localIdentifier: String?) -> PHAsset? {
guard let id = localIdentifier else {
return nil
}
let result = PHAsset.fetchAssets(withLocalIdentifiers: [id], options: nil)
return result.firstObject
}
/// Fetch photos from result.
public class func fetchPhoto(in result: PHFetchResult<PHAsset>, ascending: Bool, allowSelectImage: Bool, allowSelectVideo: Bool, limitCount: Int = .max) -> [ZLPhotoModel] {
var models: [ZLPhotoModel] = []
let option: NSEnumerationOptions = ascending ? .init(rawValue: 0) : .reverse
var count = 1
result.enumerateObjects(options: option) { asset, _, stop in
let m = ZLPhotoModel(asset: asset)
if m.type == .image, !allowSelectImage {
return
}
if m.type == .video, !allowSelectVideo {
return
}
if count == limitCount {
stop.pointee = true
}
models.append(m)
count += 1
}
return models
}
/// Fetch all album list.
public class func getPhotoAlbumList(ascending: Bool, allowSelectImage: Bool, allowSelectVideo: Bool, completion: ([ZLAlbumListModel]) -> Void) {
let option = PHFetchOptions()
if !allowSelectImage {
option.predicate = NSPredicate(format: "mediaType == %ld", PHAssetMediaType.video.rawValue)
}
if !allowSelectVideo {
option.predicate = NSPredicate(format: "mediaType == %ld", PHAssetMediaType.image.rawValue)
}
let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: nil) as! PHFetchResult<PHCollection>
let albums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumRegular, options: nil) as! PHFetchResult<PHCollection>
let streamAlbums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumMyPhotoStream, options: nil) as! PHFetchResult<PHCollection>
let syncedAlbums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumSyncedAlbum, options: nil) as! PHFetchResult<PHCollection>
let sharedAlbums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumCloudShared, options: nil) as! PHFetchResult<PHCollection>
let arr = [smartAlbums, albums, streamAlbums, syncedAlbums, sharedAlbums]
var albumList: [ZLAlbumListModel] = []
arr.forEach { album in
album.enumerateObjects { collection, _, _ in
guard let collection = collection as? PHAssetCollection else { return }
if collection.assetCollectionSubtype == .smartAlbumAllHidden {
return
}
if #available(iOS 11.0, *), collection.assetCollectionSubtype.rawValue > PHAssetCollectionSubtype.smartAlbumLongExposures.rawValue {
return
}
let result = PHAsset.fetchAssets(in: collection, options: option)
if result.count == 0 {
return
}
let title = self.getCollectionTitle(collection)
if collection.assetCollectionSubtype == .smartAlbumUserLibrary {
// Album of all photos.
let m = ZLAlbumListModel(title: title, result: result, collection: collection, option: option, isCameraRoll: true)
albumList.insert(m, at: 0)
} else {
let m = ZLAlbumListModel(title: title, result: result, collection: collection, option: option, isCameraRoll: false)
albumList.append(m)
}
}
}
completion(albumList)
}
/// Fetch camera roll album.
public class func getCameraRollAlbum(allowSelectImage: Bool, allowSelectVideo: Bool, completion: @escaping (ZLAlbumListModel) -> Void) {
DispatchQueue.global().async {
let option = PHFetchOptions()
if !allowSelectImage {
option.predicate = NSPredicate(format: "mediaType == %ld", PHAssetMediaType.video.rawValue)
}
if !allowSelectVideo {
option.predicate = NSPredicate(format: "mediaType == %ld", PHAssetMediaType.image.rawValue)
}
let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: nil)
smartAlbums.enumerateObjects { collection, _, stop in
if collection.assetCollectionSubtype == .smartAlbumUserLibrary {
stop.pointee = true
let result = PHAsset.fetchAssets(in: collection, options: option)
let albumModel = ZLAlbumListModel(title: self.getCollectionTitle(collection), result: result, collection: collection, option: option, isCameraRoll: true)
ZLMainAsync {
completion(albumModel)
}
}
}
}
}
/// Conversion collection title.
private class func getCollectionTitle(_ collection: PHAssetCollection) -> String {
if collection.assetCollectionType == .album {
// Albums created by user.
var title: String?
if ZLCustomLanguageDeploy.language == .system {
title = collection.localizedTitle
} else {
switch collection.assetCollectionSubtype {
case .albumMyPhotoStream:
title = localLanguageTextValue(.myPhotoStream)
default:
title = collection.localizedTitle
}
}
return title ?? localLanguageTextValue(.noTitleAlbumListPlaceholder)
}
var title: String?
if ZLCustomLanguageDeploy.language == .system {
title = collection.localizedTitle
} else {
switch collection.assetCollectionSubtype {
case .smartAlbumUserLibrary:
title = localLanguageTextValue(.cameraRoll)
case .smartAlbumPanoramas:
title = localLanguageTextValue(.panoramas)
case .smartAlbumVideos:
title = localLanguageTextValue(.videos)
case .smartAlbumFavorites:
title = localLanguageTextValue(.favorites)
case .smartAlbumTimelapses:
title = localLanguageTextValue(.timelapses)
case .smartAlbumRecentlyAdded:
title = localLanguageTextValue(.recentlyAdded)
case .smartAlbumBursts:
title = localLanguageTextValue(.bursts)
case .smartAlbumSlomoVideos:
title = localLanguageTextValue(.slomoVideos)
case .smartAlbumSelfPortraits:
title = localLanguageTextValue(.selfPortraits)
case .smartAlbumScreenshots:
title = localLanguageTextValue(.screenshots)
case .smartAlbumDepthEffect:
title = localLanguageTextValue(.depthEffect)
case .smartAlbumLivePhotos:
title = localLanguageTextValue(.livePhotos)
default:
title = collection.localizedTitle
}
if #available(iOS 11.0, *) {
if collection.assetCollectionSubtype == PHAssetCollectionSubtype.smartAlbumAnimated {
title = localLanguageTextValue(.animated)
}
}
}
return title ?? localLanguageTextValue(.noTitleAlbumListPlaceholder)
}
@discardableResult
public class func fetchImage(for asset: PHAsset, size: CGSize, progress: ((CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable: Any]?) -> Void)? = nil, completion: @escaping (UIImage?, Bool) -> Void) -> PHImageRequestID {
return fetchImage(for: asset, size: size, resizeMode: .fast, progress: progress, completion: completion)
}
@discardableResult
public class func fetchOriginalImage(for asset: PHAsset, progress: ((CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable: Any]?) -> Void)? = nil, completion: @escaping (UIImage?, Bool) -> Void) -> PHImageRequestID {
return fetchImage(for: asset, size: PHImageManagerMaximumSize, resizeMode: .fast, progress: progress, completion: completion)
}
/// Fetch asset data.
@discardableResult
public class func fetchOriginalImageData(for asset: PHAsset, progress: ((CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable: Any]?) -> Void)? = nil, completion: @escaping (Data, [AnyHashable: Any]?, Bool) -> Void) -> PHImageRequestID {
let option = PHImageRequestOptions()
if asset.zl.isGif {
option.version = .original
}
option.isNetworkAccessAllowed = true
option.resizeMode = .fast
option.deliveryMode = .highQualityFormat
option.progressHandler = { pro, error, stop, info in
ZLMainAsync {
progress?(CGFloat(pro), error, stop, info)
}
}
return PHImageManager.default().requestImageData(for: asset, options: option) { data, _, _, info in
let cancel = info?[PHImageCancelledKey] as? Bool ?? false
let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
if !cancel, let data = data {
completion(data, info, isDegraded)
}
}
}
/// Fetch image for asset.
private class func fetchImage(for asset: PHAsset, size: CGSize, resizeMode: PHImageRequestOptionsResizeMode, progress: ((CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable: Any]?) -> Void)? = nil, completion: @escaping (UIImage?, Bool) -> Void) -> PHImageRequestID {
let option = PHImageRequestOptions()
option.resizeMode = resizeMode
option.isNetworkAccessAllowed = true
option.progressHandler = { pro, error, stop, info in
ZLMainAsync {
progress?(CGFloat(pro), error, stop, info)
}
}
return PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: option) { image, info in
var downloadFinished = false
if let info = info {
downloadFinished = !(info[PHImageCancelledKey] as? Bool ?? false) && (info[PHImageErrorKey] == nil)
}
let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
if downloadFinished {
ZLMainAsync {
completion(image, isDegraded)
}
}
}
}
public class func fetchLivePhoto(for asset: PHAsset, completion: @escaping (PHLivePhoto?, [AnyHashable: Any]?, Bool) -> Void) -> PHImageRequestID {
let option = PHLivePhotoRequestOptions()
option.version = .current
option.deliveryMode = .opportunistic
option.isNetworkAccessAllowed = true
return PHImageManager.default().requestLivePhoto(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFit, options: option) { livePhoto, info in
let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
completion(livePhoto, info, isDegraded)
}
}
public class func fetchVideo(for asset: PHAsset, progress: ((CGFloat, Error?, UnsafeMutablePointer<ObjCBool>, [AnyHashable: Any]?) -> Void)? = nil, completion: @escaping (AVPlayerItem?, [AnyHashable: Any]?, Bool) -> Void) -> PHImageRequestID {
let option = PHVideoRequestOptions()
option.isNetworkAccessAllowed = true
option.progressHandler = { pro, error, stop, info in
ZLMainAsync {
progress?(CGFloat(pro), error, stop, info)
}
}
// https://github.com/longitachi/ZLPhotoBrowser/issues/369#issuecomment-728679135
if asset.zl.isInCloud {
return PHImageManager.default().requestExportSession(forVideo: asset, options: option, exportPreset: AVAssetExportPresetHighestQuality, resultHandler: { session, info in
// iOS11 and earlier, callback is not on the main thread.
ZLMainAsync {
let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
if let avAsset = session?.asset {
let item = AVPlayerItem(asset: avAsset)
completion(item, info, isDegraded)
} else {
completion(nil, nil, true)
}
}
})
} else {
return PHImageManager.default().requestPlayerItem(forVideo: asset, options: option) { item, info in
// iOS11 and earlier, callback is not on the main thread.
ZLMainAsync {
let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
completion(item, info, isDegraded)
}
}
}
}
class func isFetchImageError(_ error: Error?) -> Bool {
guard let e = error as NSError? else {
return false
}
if e.domain == "CKErrorDomain" || e.domain == "CloudPhotoLibraryErrorDomain" {
return true
}
return false
}
public class func fetchAVAsset(forVideo asset: PHAsset, completion: @escaping (AVAsset?, [AnyHashable: Any]?) -> Void) -> PHImageRequestID {
let options = PHVideoRequestOptions()
options.deliveryMode = .automatic
options.isNetworkAccessAllowed = true
if asset.zl.isInCloud {
return PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { session, info in
// iOS11 and earlier, callback is not on the main thread.
ZLMainAsync {
if let avAsset = session?.asset {
completion(avAsset, info)
} else {
completion(nil, info)
}
}
}
} else {
return PHImageManager.default().requestAVAsset(forVideo: asset, options: options) { avAsset, _, info in
ZLMainAsync {
completion(avAsset, info)
}
}
}
}
/// Fetch the size of asset. Unit is KB.
public class func fetchAssetSize(for asset: PHAsset) -> ZLPhotoConfiguration.KBUnit? {
guard let resource = PHAssetResource.assetResources(for: asset).first,
let size = resource.value(forKey: "fileSize") as? CGFloat else {
return nil
}
return size / 1024
}
/// Fetch asset local file path.
/// - Note: Asynchronously to fetch the file path. calls completionHandler block on the main queue.
public class func fetchAssetFilePath(for asset: PHAsset, completion: @escaping (String?) -> Void) {
asset.requestContentEditingInput(with: nil) { input, _ in
var path = input?.fullSizeImageURL?.absoluteString
if path == nil,
let dir = asset.value(forKey: "directory") as? String,
let name = asset.zl.filename {
path = String(format: "file:///var/mobile/Media/%@/%@", dir, name)
}
completion(path)
}
}
/// Save asset original data to file url. Support save image and video.
/// - Note: Asynchronously write to a local file. Calls completionHandler block on the main queue.
public class func saveAsset(_ asset: PHAsset, toFile fileUrl: URL, completion: @escaping ((Error?) -> Void)) {
guard let resource = asset.zl.resource else {
completion(NSError.assetSaveError)
return
}
PHAssetResourceManager.default().writeData(for: resource, toFile: fileUrl, options: nil) { error in
ZLMainAsync {
completion(error)
}
}
}
}
/// Authority related.
public extension ZLPhotoManager {
class func hasPhotoLibratyAuthority() -> Bool {
return PHPhotoLibrary.authorizationStatus() == .authorized
}
class func hasCameraAuthority() -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: .video)
if status == .restricted || status == .denied {
return false
}
return true
}
class func hasMicrophoneAuthority() -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: .audio)
if status == .restricted || status == .denied {
return false
}
return true
}
}

View File

@@ -0,0 +1,165 @@
//
// ZLPhotoModel.swift
// ZLPhotoBrowser
//
// Created by long on 2020/8/11.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import Photos
public extension ZLPhotoModel {
enum MediaType: Int {
case unknown = 0
case image
case gif
case livePhoto
case video
}
}
public class ZLPhotoModel: NSObject {
public let ident: String
public let asset: PHAsset
public var type: ZLPhotoModel.MediaType = .unknown
public var duration: String = ""
public var isSelected: Bool = false
private var pri_dataSize: ZLPhotoConfiguration.KBUnit?
public var dataSize: ZLPhotoConfiguration.KBUnit? {
if let pri_dataSize = pri_dataSize {
return pri_dataSize
}
let size = ZLPhotoManager.fetchAssetSize(for: asset)
pri_dataSize = size
return size
}
private var pri_editImage: UIImage?
public var editImage: UIImage? {
set {
pri_editImage = newValue
}
get {
if let _ = editImageModel {
return pri_editImage
} else {
return nil
}
}
}
public var second: ZLPhotoConfiguration.Second {
guard type == .video else {
return 0
}
return Int(round(asset.duration))
}
public var whRatio: CGFloat {
return CGFloat(asset.pixelWidth) / CGFloat(asset.pixelHeight)
}
public var previewSize: CGSize {
let scale: CGFloat = UIScreen.main.scale
if whRatio > 1 {
let h = min(UIScreen.main.bounds.height, ZLMaxImageWidth) * scale
let w = h * whRatio
return CGSize(width: w, height: h)
} else {
let w = min(UIScreen.main.bounds.width, ZLMaxImageWidth) * scale
let h = w / whRatio
return CGSize(width: w, height: h)
}
}
// Content of the last edit.
public var editImageModel: ZLEditImageModel?
public init(asset: PHAsset) {
ident = asset.localIdentifier
self.asset = asset
super.init()
type = transformAssetType(for: asset)
if type == .video {
duration = transformDuration(for: asset)
}
}
public func transformAssetType(for asset: PHAsset) -> ZLPhotoModel.MediaType {
switch asset.mediaType {
case .video:
return .video
case .image:
if asset.zl.isGif {
return .gif
}
if asset.mediaSubtypes.contains(.photoLive) {
return .livePhoto
}
return .image
default:
return .unknown
}
}
public func transformDuration(for asset: PHAsset) -> String {
let dur = Int(round(asset.duration))
switch dur {
case 0..<60:
return String(format: "00:%02d", dur)
case 60..<3600:
let m = dur / 60
let s = dur % 60
return String(format: "%02d:%02d", m, s)
case 3600...:
let h = dur / 3600
let m = (dur % 3600) / 60
let s = dur % 60
return String(format: "%02d:%02d:%02d", h, m, s)
default:
return ""
}
}
}
public extension ZLPhotoModel {
static func ==(lhs: ZLPhotoModel, rhs: ZLPhotoModel) -> Bool {
return lhs.ident == rhs.ident
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,434 @@
//
// ZLPhotoUIConfiguration+Chaining.swift
// ZLPhotoBrowser
//
// Created by long on 2022/4/19.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
// MARK: chaining
public extension ZLPhotoUIConfiguration {
@discardableResult
func style(_ style: ZLPhotoBrowserStyle) -> ZLPhotoUIConfiguration {
self.style = style
return self
}
@discardableResult
func statusBarStyle(_ statusBarStyle: UIStatusBarStyle) -> ZLPhotoUIConfiguration {
self.statusBarStyle = statusBarStyle
return self
}
@discardableResult
func navCancelButtonStyle(_ style: ZLPhotoUIConfiguration.CancelButtonStyle) -> ZLPhotoUIConfiguration {
navCancelButtonStyle = style
return self
}
@discardableResult
func showStatusBarInPreviewInterface(_ value: Bool) -> ZLPhotoUIConfiguration {
showStatusBarInPreviewInterface = value
return self
}
@discardableResult
func hudStyle(_ style: ZLProgressHUD.HUDStyle) -> ZLPhotoUIConfiguration {
hudStyle = style
return self
}
@discardableResult
func adjustSliderType(_ type: ZLAdjustSliderType) -> ZLPhotoUIConfiguration {
adjustSliderType = type
return self
}
@discardableResult
func cellCornerRadio(_ cornerRadio: CGFloat) -> ZLPhotoUIConfiguration {
cellCornerRadio = cornerRadio
return self
}
@discardableResult
func customAlertClass(_ alertClass: ZLCustomAlertProtocol.Type?) -> ZLPhotoUIConfiguration {
customAlertClass = alertClass
return self
}
/// - Note: This property is ignored when using columnCountBlock.
@discardableResult
func columnCount(_ count: Int) -> ZLPhotoUIConfiguration {
columnCount = count
return self
}
@discardableResult
func columnCountBlock(_ block: ((_ collectionViewWidth: CGFloat) -> Int)?) -> ZLPhotoUIConfiguration {
columnCountBlock = block
return self
}
@discardableResult
func minimumInteritemSpacing(_ value: CGFloat) -> ZLPhotoUIConfiguration {
minimumInteritemSpacing = value
return self
}
@discardableResult
func minimumLineSpacing(_ value: CGFloat) -> ZLPhotoUIConfiguration {
minimumLineSpacing = value
return self
}
@discardableResult
func navViewBlurEffectOfAlbumList(_ effect: UIBlurEffect?) -> ZLPhotoUIConfiguration {
navViewBlurEffectOfAlbumList = effect
return self
}
@discardableResult
func navViewBlurEffectOfPreview(_ effect: UIBlurEffect?) -> ZLPhotoUIConfiguration {
navViewBlurEffectOfPreview = effect
return self
}
@discardableResult
func bottomViewBlurEffectOfAlbumList(_ effect: UIBlurEffect?) -> ZLPhotoUIConfiguration {
bottomViewBlurEffectOfAlbumList = effect
return self
}
@discardableResult
func bottomViewBlurEffectOfPreview(_ effect: UIBlurEffect?) -> ZLPhotoUIConfiguration {
bottomViewBlurEffectOfPreview = effect
return self
}
@discardableResult
func customImageNames(_ names: [String]) -> ZLPhotoUIConfiguration {
customImageNames = names
return self
}
@discardableResult
func customImageForKey(_ map: [String: UIImage?]) -> ZLPhotoUIConfiguration {
customImageForKey = map
return self
}
@discardableResult
func languageType(_ type: ZLLanguageType) -> ZLPhotoUIConfiguration {
languageType = type
return self
}
@discardableResult
func customLanguageKeyValue(_ map: [ZLLocalLanguageKey: String]) -> ZLPhotoUIConfiguration {
customLanguageKeyValue = map
return self
}
@discardableResult
func themeFontName(_ name: String) -> ZLPhotoUIConfiguration {
themeFontName = name
return self
}
@discardableResult
func themeColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
themeColor = color
return self
}
@discardableResult
func sheetTranslucentColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
sheetTranslucentColor = color
return self
}
@discardableResult
func sheetBtnBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
sheetBtnBgColor = color
return self
}
@discardableResult
func sheetBtnTitleColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
sheetBtnTitleColor = color
return self
}
@discardableResult
func sheetBtnTitleTintColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
sheetBtnTitleTintColor = color
return self
}
@discardableResult
func navBarColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
navBarColor = color
return self
}
@discardableResult
func navBarColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
navBarColorOfPreviewVC = color
return self
}
@discardableResult
func navTitleColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
navTitleColor = color
return self
}
@discardableResult
func navTitleColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
navTitleColorOfPreviewVC = color
return self
}
@discardableResult
func navEmbedTitleViewBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
navEmbedTitleViewBgColor = color
return self
}
@discardableResult
func albumListBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
albumListBgColor = color
return self
}
@discardableResult
func embedAlbumListTranslucentColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
embedAlbumListTranslucentColor = color
return self
}
@discardableResult
func albumListTitleColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
albumListTitleColor = color
return self
}
@discardableResult
func albumListCountColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
albumListCountColor = color
return self
}
@discardableResult
func separatorColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
separatorColor = color
return self
}
@discardableResult
func thumbnailBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
thumbnailBgColor = color
return self
}
@discardableResult
func previewVCBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
previewVCBgColor = color
return self
}
@discardableResult
func bottomToolViewBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
bottomToolViewBgColor = color
return self
}
@discardableResult
func bottomToolViewBgColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
bottomToolViewBgColorOfPreviewVC = color
return self
}
@discardableResult
func bottomToolViewBtnNormalTitleColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
bottomToolViewBtnNormalTitleColor = color
return self
}
@discardableResult
func bottomToolViewDoneBtnNormalTitleColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
bottomToolViewDoneBtnNormalTitleColor = color
return self
}
@discardableResult
func bottomToolViewBtnNormalTitleColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
bottomToolViewBtnNormalTitleColorOfPreviewVC = color
return self
}
@discardableResult
func bottomToolViewDoneBtnNormalTitleColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
bottomToolViewDoneBtnNormalTitleColorOfPreviewVC = color
return self
}
@discardableResult
func bottomToolViewBtnDisableTitleColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
bottomToolViewBtnDisableTitleColor = color
return self
}
@discardableResult
func bottomToolViewDoneBtnDisableTitleColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
bottomToolViewDoneBtnDisableTitleColor = color
return self
}
@discardableResult
func bottomToolViewBtnDisableTitleColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
bottomToolViewBtnDisableTitleColorOfPreviewVC = color
return self
}
@discardableResult
func bottomToolViewDoneBtnDisableTitleColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
bottomToolViewDoneBtnDisableTitleColorOfPreviewVC = color
return self
}
@discardableResult
func bottomToolViewBtnNormalBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
bottomToolViewBtnNormalBgColor = color
return self
}
@discardableResult
func bottomToolViewBtnNormalBgColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
bottomToolViewBtnNormalBgColorOfPreviewVC = color
return self
}
@discardableResult
func bottomToolViewBtnDisableBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
bottomToolViewBtnDisableBgColor = color
return self
}
@discardableResult
func bottomToolViewBtnDisableBgColorOfPreviewVC(_ color: UIColor) -> ZLPhotoUIConfiguration {
bottomToolViewBtnDisableBgColorOfPreviewVC = color
return self
}
@discardableResult
func limitedAuthorityTipsColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
limitedAuthorityTipsColor = color
return self
}
@discardableResult
func cameraRecodeProgressColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
cameraRecodeProgressColor = color
return self
}
@discardableResult
func selectedMaskColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
selectedMaskColor = color
return self
}
@discardableResult
func selectedBorderColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
selectedBorderColor = color
return self
}
@discardableResult
func invalidMaskColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
invalidMaskColor = color
return self
}
@discardableResult
func indexLabelTextColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
indexLabelTextColor = color
return self
}
@discardableResult
func indexLabelBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
indexLabelBgColor = color
return self
}
@discardableResult
func cameraCellBgColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
cameraCellBgColor = color
return self
}
@discardableResult
func adjustSliderNormalColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
adjustSliderNormalColor = color
return self
}
@discardableResult
func adjustSliderTintColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
adjustSliderTintColor = color
return self
}
@discardableResult
func imageEditorToolTitleNormalColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
imageEditorToolTitleNormalColor = color
return self
}
@discardableResult
func imageEditorToolTitleTintColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
imageEditorToolTitleTintColor = color
return self
}
@discardableResult
func imageEditorToolIconTintColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
imageEditorToolIconTintColor = color
return self
}
@discardableResult
func trashCanBackgroundNormalColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
trashCanBackgroundNormalColor = color
return self
}
@discardableResult
func trashCanBackgroundTintColor(_ color: UIColor) -> ZLPhotoUIConfiguration {
trashCanBackgroundTintColor = color
return self
}
}

View File

@@ -0,0 +1,460 @@
//
// ZLPhotoUIConfiguration.swift
// ZLPhotoBrowser
//
// Created by long on 2022/4/18.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
/// Custom UI configuration (include colors, images, text, font)
@objcMembers
public class ZLPhotoUIConfiguration: NSObject {
@objc public enum CancelButtonStyle: Int {
case text
case image
}
private static var single = ZLPhotoUIConfiguration()
public class func `default`() -> ZLPhotoUIConfiguration {
return ZLPhotoUIConfiguration.single
}
public class func resetConfiguration() {
ZLPhotoUIConfiguration.single = ZLPhotoUIConfiguration()
}
// MARK: Framework style.
public var style: ZLPhotoBrowserStyle = .embedAlbumList
public var statusBarStyle: UIStatusBarStyle = .lightContent
/// text: Cancel. image: 'x'. Defaults to image.
public var navCancelButtonStyle: ZLPhotoUIConfiguration.CancelButtonStyle = .image
/// Whether to show the status bar when previewing photos. Defaults to false.
public var showStatusBarInPreviewInterface = false
/// HUD style. Defaults to dark.
public var hudStyle: ZLProgressHUD.HUDStyle = .dark
/// Adjust Slider Type
public var adjustSliderType: ZLAdjustSliderType = .vertical
public var cellCornerRadio: CGFloat = 0
/// Custom alert class. Defaults to nil.
public var customAlertClass: ZLCustomAlertProtocol.Type?
private var pri_columnCount = 4
/// The column count when iPhone is in portait mode. Minimum is 2, maximum is 6. Defaults to 4.
/// ```
/// iPhone landscape mode: columnCount += 2.
/// iPad portait mode: columnCount += 2.
/// iPad landscape mode: columnCount += 4.
/// ```
///
/// - Note: This property is ignored when using columnCountBlock.
public var columnCount: Int {
get {
pri_columnCount
}
set {
pri_columnCount = min(6, max(newValue, 2))
}
}
/// Use this property to customize the column count for `ZLThumbnailViewController`.
/// This property is recommended.
public var columnCountBlock: ((_ collectionViewWidth: CGFloat) -> Int)?
/// The minimum spacing to use between items in the same row for `ZLThumbnailViewController`.
public var minimumInteritemSpacing: CGFloat = 2
/// The minimum spacing to use between lines of items in the grid for `ZLThumbnailViewController`.
public var minimumLineSpacing: CGFloat = 2
// MARK: Navigation and bottom tool bar
/// The blur effect of the navigation bar in the album list
public var navViewBlurEffectOfAlbumList: UIBlurEffect? = UIBlurEffect(style: .dark)
/// The blur effect of the navigation bar in the preview interface
public var navViewBlurEffectOfPreview: UIBlurEffect? = UIBlurEffect(style: .dark)
/// The blur effect of the bottom tool bar in the album list
public var bottomViewBlurEffectOfAlbumList: UIBlurEffect? = UIBlurEffect(style: .dark)
/// The blur effect of the bottom tool bar in the preview interface
public var bottomViewBlurEffectOfPreview: UIBlurEffect? = UIBlurEffect(style: .dark)
// MARK: Image properties
/// Developers can customize images, but the name of the custom image resource must be consistent with the image name in the replaced bundle.
/// - example: Developers need to replace the selected and unselected image resources, and the array that needs to be passed in is
/// ["zl_btn_selected", "zl_btn_unselected"].
public var customImageNames: [String] = [] {
didSet {
ZLCustomImageDeploy.imageNames = customImageNames
}
}
/// Developers can customize images, but the name of the custom image resource must be consistent with the image name in the replaced bundle.
/// - example: Developers need to replace the selected and unselected image resources, and the array that needs to be passed in is
/// ["zl_btn_selected": selectedImage, "zl_btn_unselected": unselectedImage].
public var customImageForKey: [String: UIImage?] = [:] {
didSet {
customImageForKey.forEach { ZLCustomImageDeploy.imageForKey[$0.key] = $0.value }
}
}
/// Developers can customize images, but the name of the custom image resource must be consistent with the image name in the replaced bundle.
/// - example: Developers need to replace the selected and unselected image resources, and the array that needs to be passed in is
/// ["zl_btn_selected": selectedImage, "zl_btn_unselected": unselectedImage].
public var customImageForKey_objc: [String: UIImage] = [:] {
didSet {
ZLCustomImageDeploy.imageForKey = customImageForKey_objc
}
}
// MARK: Language properties
/// Language for framework.
public var languageType: ZLLanguageType = .system {
didSet {
ZLCustomLanguageDeploy.language = languageType
Bundle.resetLanguage()
}
}
/// Developers can customize languages.
/// - example: If you needs to replace
/// key: .hudLoading, value: "loading, waiting please" language,
/// The dictionary that needs to be passed in is [.hudLoading: "text to be replaced"].
/// - warning: Please pay attention to the placeholders contained in languages when changing, such as %ld, %@.
public var customLanguageKeyValue: [ZLLocalLanguageKey: String] = [:] {
didSet {
ZLCustomLanguageDeploy.deploy = customLanguageKeyValue
}
}
/// Developers can customize languages (This property is only for objc).
/// - example: If you needs to replace
/// key: @"loading", value: @"loading, waiting please" language,
/// The dictionary that needs to be passed in is @[@"hudLoading": @"text to be replaced"].
/// - warning: Please pay attention to the placeholders contained in languages when changing, such as %ld, %@.
public var customLanguageKeyValue_objc: [String: String] = [:] {
didSet {
var swiftParams: [ZLLocalLanguageKey: String] = [:]
customLanguageKeyValue_objc.forEach { key, value in
swiftParams[ZLLocalLanguageKey(rawValue: key)] = value
}
customLanguageKeyValue = swiftParams
}
}
// MARK: Font
/// Font name.
public var themeFontName: String? {
didSet {
ZLCustomFontDeploy.fontName = themeFontName
}
}
// MARK: Color properties
/// The theme color of framework.
///
public var themeColor: UIColor = .zl.rgba(7, 213, 101)
/// Preview selection mode, translucent background color above.
///
public var sheetTranslucentColor: UIColor = .black.withAlphaComponent(0.1)
/// Preview selection mode, a background color for `Camera`, `Album`, `Cancel` buttons.
///
public var sheetBtnBgColor: UIColor = .white
/// Preview selection mode, a text color for `Camera`, `Album`, `Cancel` buttons.
///
public var sheetBtnTitleColor: UIColor = .black
private var pri_sheetBtnTitleTintColor: UIColor?
/// Preview selection mode, cancel button title color when the selection amount is superior than 0.
///
public var sheetBtnTitleTintColor: UIColor {
get {
pri_sheetBtnTitleTintColor ?? themeColor
}
set {
pri_sheetBtnTitleTintColor = newValue
}
}
/// A color for navigation bar.
///
public var navBarColor: UIColor = .zl.rgba(160, 160, 160, 0.65)
/// A color for navigation bar in preview interface.
///
public var navBarColorOfPreviewVC: UIColor = .zl.rgba(160, 160, 160, 0.65)
/// A color for Navigation bar text.
///
public var navTitleColor: UIColor = .white
/// A color for Navigation bar text of preview vc.
///
public var navTitleColorOfPreviewVC: UIColor = .white
/// The background color of the title view when the frame style is embedAlbumList.
///
public var navEmbedTitleViewBgColor: UIColor = .zl.rgba(80, 80, 80)
/// A color for background in album list.
///
public var albumListBgColor: UIColor = .zl.rgba(45, 45, 45)
/// A color of the translucent area below the embed album list.
///
public var embedAlbumListTranslucentColor: UIColor = .black.withAlphaComponent(0.8)
/// A color for album list title label.
///
public var albumListTitleColor: UIColor = .white
/// A color for album list count label.
/// label
public var albumListCountColor: UIColor = .zl.rgba(180, 180, 180)
/// A color for album list separator.
/// 线
public var separatorColor: UIColor = .zl.rgba(60, 60, 60)
/// A color for background in thumbnail interface.
///
public var thumbnailBgColor: UIColor = .zl.rgba(50, 50, 50)
/// A color for background in preview interface..
///
public var previewVCBgColor: UIColor = .black
/// A color for background in bottom tool view.
///
public var bottomToolViewBgColor: UIColor = .zl.rgba(35, 35, 35, 0.3)
/// A color for background in bottom tool view in preview interface.
///
public var bottomToolViewBgColorOfPreviewVC: UIColor = .zl.rgba(35, 35, 35, 0.3)
/// The normal state title color of bottom tool view buttons. Without done button.
/// ``
public var bottomToolViewBtnNormalTitleColor: UIColor = .white
/// The normal state title color of bottom tool view done button.
/// ``
public var bottomToolViewDoneBtnNormalTitleColor: UIColor = .white
/// The normal state title color of bottom tool view buttons in preview interface. Without done button.
/// ``
public var bottomToolViewBtnNormalTitleColorOfPreviewVC: UIColor = .white
/// The normal state title color of bottom tool view done button.
/// ``
public var bottomToolViewDoneBtnNormalTitleColorOfPreviewVC: UIColor = .white
/// The disable state title color of bottom tool view buttons. Without done button.
/// ``
public var bottomToolViewBtnDisableTitleColor: UIColor = .zl.rgba(168, 168, 168)
/// The disable state title color of bottom tool view done button.
/// ``
public var bottomToolViewDoneBtnDisableTitleColor: UIColor = .zl.rgba(168, 168, 168)
/// The disable state title color of bottom tool view buttons in preview interface. Without done button.
/// ``
public var bottomToolViewBtnDisableTitleColorOfPreviewVC: UIColor = .zl.rgba(168, 168, 168)
/// The disable state title color of bottom tool view done button in preview interface.
/// ``
public var bottomToolViewDoneBtnDisableTitleColorOfPreviewVC: UIColor = .zl.rgba(168, 168, 168)
private var pri_bottomToolViewBtnNormalBgColor: UIColor?
/// The normal state background color of bottom tool view buttons.
///
public var bottomToolViewBtnNormalBgColor: UIColor {
get {
pri_bottomToolViewBtnNormalBgColor ?? themeColor
}
set {
pri_bottomToolViewBtnNormalBgColor = newValue
}
}
private var pri_bottomToolViewBtnNormalBgColorOfPreviewVC: UIColor?
/// The normal state background color of bottom tool view buttons in preview interface.
///
public var bottomToolViewBtnNormalBgColorOfPreviewVC: UIColor {
get {
pri_bottomToolViewBtnNormalBgColorOfPreviewVC ?? themeColor
}
set {
pri_bottomToolViewBtnNormalBgColorOfPreviewVC = newValue
}
}
/// The disable state background color of bottom tool view buttons.
///
public var bottomToolViewBtnDisableBgColor: UIColor = .zl.rgba(50, 50, 50)
/// The disable state background color of bottom tool view buttons in preview interface.
///
public var bottomToolViewBtnDisableBgColorOfPreviewVC: UIColor = .zl.rgba(50, 50, 50)
/// With iOS14 limited authority, a color for select more photos at the bottom of the thumbnail interface.
/// iOS14 limited
public var limitedAuthorityTipsColor: UIColor = .white
private var pri_cameraRecodeProgressColor: UIColor?
/// The record progress color of custom camera.
///
public var cameraRecodeProgressColor: UIColor {
get {
pri_cameraRecodeProgressColor ?? themeColor
}
set {
pri_cameraRecodeProgressColor = newValue
}
}
/// Mask layer color of selected cell.
///
public var selectedMaskColor: UIColor = .black.withAlphaComponent(0.2)
private var pri_selectedBorderColor: UIColor?
/// Border color of selected cell.
/// border
public var selectedBorderColor: UIColor {
get {
pri_selectedBorderColor ?? themeColor
}
set {
pri_selectedBorderColor = newValue
}
}
/// Mask layer color of the cell that cannot be selected.
///
public var invalidMaskColor: UIColor = .white.withAlphaComponent(0.5)
/// The text color of selected cell index label.
/// label
public var indexLabelTextColor: UIColor = .white
private var pri_indexLabelBgColor: UIColor?
/// The background color of selected cell index label.
/// label
public var indexLabelBgColor: UIColor {
get {
pri_indexLabelBgColor ?? themeColor
}
set {
pri_indexLabelBgColor = newValue
}
}
/// The background color of camera cell inside album.
///
public var cameraCellBgColor: UIColor = .zl.rgba(76, 76, 76)
/// The normal color of adjust slider.
/// slider
public var adjustSliderNormalColor: UIColor = .white
private var pri_adjustSliderTintColor: UIColor?
/// The tint color of adjust slider.
/// slider
public var adjustSliderTintColor: UIColor {
get {
pri_adjustSliderTintColor ?? themeColor
}
set {
pri_adjustSliderTintColor = newValue
}
}
/// The normal color of the title below the various tools in the image editor.
///
public var imageEditorToolTitleNormalColor: UIColor = .zl.rgba(160, 160, 160)
/// The tint color of the title below the various tools in the image editor.
///
public var imageEditorToolTitleTintColor: UIColor = .white
/// The tint color of the image editor tool icons.
///
public var imageEditorToolIconTintColor: UIColor?
/// Background color of trash can in image editor.
///
public var trashCanBackgroundNormalColor: UIColor = .zl.rgba(40, 40, 40, 0.8)
/// Background tint color of trash can in image editor.
///
public var trashCanBackgroundTintColor: UIColor = .zl.rgba(241, 79, 79, 0.98)
}
/// Font deploy
enum ZLCustomFontDeploy {
static var fontName: String?
}
/// Image source deploy
enum ZLCustomImageDeploy {
static var imageNames: [String] = []
static var imageForKey: [String: UIImage] = [:]
}
@objc public enum ZLPhotoBrowserStyle: Int {
/// The album list is embedded in the navigation of the thumbnail interface, click the drop-down display.
case embedAlbumList
/// The display relationship between the album list and the thumbnail interface is push.
case externalAlbumList
}
/// Language deploy
enum ZLCustomLanguageDeploy {
static var language: ZLLanguageType = .system
static var deploy: [ZLLocalLanguageKey: String] = [:]
}
/// Adjust slider type
@objc public enum ZLAdjustSliderType: Int {
case vertical
case horizontal
}

View File

@@ -0,0 +1,185 @@
//
// ZLProgressHUD.swift
// ZLPhotoBrowser
//
// Created by long on 2020/8/17.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
public class ZLProgressHUD: UIView {
@objc public enum HUDStyle: Int {
case light
case lightBlur
case dark
case darkBlur
var bgColor: UIColor {
switch self {
case .light:
return .white
case .dark:
return .darkGray
case .lightBlur:
return UIColor.white.withAlphaComponent(0.8)
case .darkBlur:
return UIColor.darkGray.withAlphaComponent(0.8)
}
}
var icon: UIImage? {
switch self {
case .light, .lightBlur:
return .zl.getImage("zl_loading_dark")
case .dark, .darkBlur:
return .zl.getImage("zl_loading_light")
}
}
var textColor: UIColor {
switch self {
case .light, .lightBlur:
return .black
case .dark, .darkBlur:
return .white
}
}
var blurEffectStyle: UIBlurEffect.Style? {
switch self {
case .light, .dark:
return nil
case .lightBlur:
return .extraLight
case .darkBlur:
return .dark
}
}
}
private let style: ZLProgressHUD.HUDStyle
private lazy var loadingView = UIImageView(image: style.icon)
private var timer: Timer?
public var timeoutBlock: (() -> Void)?
deinit {
zl_debugPrint("ZLProgressHUD deinit")
cleanTimer()
}
public init(style: ZLProgressHUD.HUDStyle) {
self.style = style
super.init(frame: UIScreen.main.bounds)
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 135, height: 135))
view.layer.masksToBounds = true
view.layer.cornerRadius = 12
view.backgroundColor = style.bgColor
view.clipsToBounds = true
view.center = center
if let effectStyle = style.blurEffectStyle {
let effect = UIBlurEffect(style: effectStyle)
let effectView = UIVisualEffectView(effect: effect)
effectView.frame = view.bounds
view.addSubview(effectView)
}
loadingView.frame = CGRect(x: 135 / 2 - 20, y: 27, width: 40, height: 40)
view.addSubview(loadingView)
let label = UILabel(frame: CGRect(x: 0, y: 85, width: view.bounds.width, height: 30))
label.textAlignment = .center
label.textColor = style.textColor
label.font = .zl.font(ofSize: 16)
label.text = localLanguageTextValue(.hudLoading)
view.addSubview(label)
addSubview(view)
}
private func startAnimation() {
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.fromValue = 0
animation.toValue = CGFloat.pi * 2
animation.duration = 0.8
animation.repeatCount = .infinity
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
loadingView.layer.add(animation, forKey: nil)
}
public func show(
in view: UIView? = UIApplication.shared.keyWindow,
timeout: TimeInterval = 100
) {
ZLMainAsync {
self.startAnimation()
view?.addSubview(self)
}
if timeout > 0 {
cleanTimer()
timer = Timer.scheduledTimer(timeInterval: timeout, target: ZLWeakProxy(target: self), selector: #selector(timeout(_:)), userInfo: nil, repeats: false)
RunLoop.current.add(timer!, forMode: .default)
}
}
public func hide() {
cleanTimer()
ZLMainAsync {
self.loadingView.layer.removeAllAnimations()
self.removeFromSuperview()
}
}
@objc func timeout(_ timer: Timer) {
timeoutBlock?()
hide()
}
func cleanTimer() {
timer?.invalidate()
timer = nil
}
}
public extension ZLProgressHUD {
class func show(
in view: UIView? = UIApplication.shared.keyWindow,
timeout: TimeInterval = 100
) -> ZLProgressHUD {
let hud = ZLProgressHUD(style: ZLPhotoUIConfiguration.default().hudStyle)
hud.show(in: view, timeout: timeout)
return hud
}
}

View File

@@ -0,0 +1,67 @@
//
// ZLProgressView.swift
// ZLPhotoBrowser
//
// Created by long on 2020/8/13.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
class ZLProgressView: UIView {
private lazy var progressLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.fillColor = UIColor.clear.cgColor
layer.strokeColor = UIColor.white.cgColor
layer.lineCap = .round
layer.lineWidth = 4
return layer
}()
var progress: CGFloat = 0 {
didSet {
self.setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
layer.addSublayer(progressLayer)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
let radius = rect.width / 2
let end = -(.pi / 2) + (.pi * 2 * progress)
progressLayer.frame = bounds
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: -(.pi / 2), endAngle: end, clockwise: true)
progressLayer.path = path.cgPath
}
}

View File

@@ -0,0 +1,58 @@
//
// ZLResultModel.swift
// ZLPhotoBrowser
//
// Created by long on 2022/9/7.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import Photos
public class ZLResultModel: NSObject {
@objc public let asset: PHAsset
@objc public let image: UIImage
/// Whether the picture has been edited. Always false when `saveNewImageAfterEdit = true`.
@objc public let isEdited: Bool
/// Content of the last edit. Always nil when `saveNewImageAfterEdit = true`.
@objc public let editModel: ZLEditImageModel?
/// The order in which the user selects the models in the album. This index is not necessarily equal to the order of the model's index in the array, as some PHAssets requests may fail.
@objc public let index: Int
@objc public init(asset: PHAsset, image: UIImage, isEdited: Bool, editModel: ZLEditImageModel? = nil, index: Int) {
self.asset = asset
self.image = image
self.isEdited = isEdited
self.editModel = editModel
self.index = index
super.init()
}
}
extension ZLResultModel {
static func ==(lhs: ZLResultModel, rhs: ZLResultModel) -> Bool {
return lhs.asset == rhs.asset
}
}

View File

@@ -0,0 +1,300 @@
//
// ZLThumbnailPhotoCell.swift
// ZLPhotoBrowser
//
// Created by long on 2020/8/12.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import Photos
class ZLThumbnailPhotoCell: UICollectionViewCell {
private lazy var containerView = UIView()
private lazy var bottomShadowView = UIImageView(image: .zl.getImage("zl_shadow"))
private lazy var videoTag = UIImageView(image: .zl.getImage("zl_video"))
private lazy var livePhotoTag = UIImageView(image: .zl.getImage("zl_livePhoto"))
private lazy var editImageTag = UIImageView(image: .zl.getImage("zl_editImage_tag"))
private lazy var descLabel: UILabel = {
let label = UILabel()
label.font = .zl.font(ofSize: 13)
label.textAlignment = .right
label.textColor = .white
return label
}()
private lazy var progressView: ZLProgressView = {
let view = ZLProgressView()
view.isHidden = true
return view
}()
private var imageIdentifier: String = ""
private var smallImageRequestID: PHImageRequestID = PHInvalidImageRequestID
private var bigImageReqeustID: PHImageRequestID = PHInvalidImageRequestID
lazy var imageView: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFill
view.clipsToBounds = true
return view
}()
lazy var btnSelect: ZLEnlargeButton = {
let btn = ZLEnlargeButton(type: .custom)
btn.setBackgroundImage(.zl.getImage("zl_btn_unselected"), for: .normal)
btn.setBackgroundImage(.zl.getImage("zl_btn_selected"), for: .selected)
btn.addTarget(self, action: #selector(btnSelectClick), for: .touchUpInside)
btn.enlargeInsets = UIEdgeInsets(top: 5, left: 10, bottom: 10, right: 5)
return btn
}()
lazy var coverView: UIView = {
let view = UIView()
view.isUserInteractionEnabled = false
view.isHidden = true
return view
}()
lazy var indexLabel: UILabel = {
let label = UILabel()
label.layer.cornerRadius = 23.0 / 2
label.layer.masksToBounds = true
label.textColor = .zl.indexLabelTextColor
label.backgroundColor = .zl.indexLabelBgColor
label.font = .zl.font(ofSize: 14)
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.5
label.textAlignment = .center
return label
}()
var enableSelect = true
var selectedBlock: ((Bool) -> Void)?
var model: ZLPhotoModel! {
didSet {
configureCell()
}
}
var index: Int = 0 {
didSet {
indexLabel.text = String(index)
}
}
deinit {
zl_debugPrint("ZLThumbnailPhotoCell deinit")
}
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupUI() {
contentView.addSubview(imageView)
contentView.addSubview(containerView)
containerView.addSubview(coverView)
containerView.addSubview(btnSelect)
btnSelect.addSubview(indexLabel)
containerView.addSubview(bottomShadowView)
bottomShadowView.addSubview(videoTag)
bottomShadowView.addSubview(livePhotoTag)
bottomShadowView.addSubview(editImageTag)
bottomShadowView.addSubview(descLabel)
containerView.addSubview(progressView)
if ZLPhotoConfiguration.default().showSelectedBorder {
layer.borderColor = UIColor.zl.selectedBorderColor.cgColor
}
}
override func layoutSubviews() {
imageView.frame = bounds
containerView.frame = bounds
coverView.frame = bounds
btnSelect.frame = CGRect(x: bounds.width - 30, y: 8, width: 23, height: 23)
indexLabel.frame = btnSelect.bounds
bottomShadowView.frame = CGRect(x: 0, y: bounds.height - 25, width: bounds.width, height: 25)
videoTag.frame = CGRect(x: 5, y: 1, width: 20, height: 15)
livePhotoTag.frame = CGRect(x: 5, y: -1, width: 20, height: 20)
editImageTag.frame = CGRect(x: 5, y: -1, width: 20, height: 20)
descLabel.frame = CGRect(x: 30, y: 1, width: bounds.width - 35, height: 17)
progressView.frame = CGRect(x: (bounds.width - 20) / 2, y: (bounds.height - 20) / 2, width: 20, height: 20)
super.layoutSubviews()
}
@objc func btnSelectClick() {
btnSelect.layer.removeAllAnimations()
if !btnSelect.isSelected, ZLPhotoConfiguration.default().animateSelectBtnWhenSelect {
btnSelect.layer.add(ZLAnimationUtils.springAnimation(), forKey: nil)
}
selectedBlock?(btnSelect.isSelected)
if btnSelect.isSelected {
fetchBigImage()
} else {
progressView.isHidden = true
cancelFetchBigImage()
}
}
private func configureCell() {
if ZLPhotoUIConfiguration.default().cellCornerRadio > 0 {
layer.cornerRadius = ZLPhotoUIConfiguration.default().cellCornerRadio
layer.masksToBounds = true
}
if model.type == .video {
bottomShadowView.isHidden = false
videoTag.isHidden = false
livePhotoTag.isHidden = true
editImageTag.isHidden = true
descLabel.text = model.duration
} else if model.type == .gif {
bottomShadowView.isHidden = !ZLPhotoConfiguration.default().allowSelectGif
videoTag.isHidden = true
livePhotoTag.isHidden = true
editImageTag.isHidden = true
descLabel.text = "GIF"
} else if model.type == .livePhoto {
bottomShadowView.isHidden = !ZLPhotoConfiguration.default().allowSelectLivePhoto
videoTag.isHidden = true
livePhotoTag.isHidden = false
editImageTag.isHidden = true
descLabel.text = "Live"
} else {
if let _ = model.editImage {
bottomShadowView.isHidden = false
videoTag.isHidden = true
livePhotoTag.isHidden = true
editImageTag.isHidden = false
descLabel.text = ""
} else {
bottomShadowView.isHidden = true
}
}
let showSelBtn: Bool
if ZLPhotoConfiguration.default().maxSelectCount > 1 {
if !ZLPhotoConfiguration.default().allowMixSelect {
showSelBtn = model.type.rawValue < ZLPhotoModel.MediaType.video.rawValue
} else {
showSelBtn = true
}
} else {
showSelBtn = ZLPhotoConfiguration.default().showSelectBtnWhenSingleSelect
}
btnSelect.isHidden = !showSelBtn
btnSelect.isUserInteractionEnabled = showSelBtn
btnSelect.isSelected = model.isSelected
if model.isSelected {
fetchBigImage()
} else {
cancelFetchBigImage()
}
if let ei = model.editImage {
imageView.image = ei
} else {
fetchSmallImage()
}
}
private func fetchSmallImage() {
let size: CGSize
let maxSideLength = bounds.width * 2
if model.whRatio > 1 {
let w = maxSideLength * model.whRatio
size = CGSize(width: w, height: maxSideLength)
} else {
let h = maxSideLength / model.whRatio
size = CGSize(width: maxSideLength, height: h)
}
if smallImageRequestID > PHInvalidImageRequestID {
PHImageManager.default().cancelImageRequest(smallImageRequestID)
}
imageIdentifier = model.ident
imageView.image = nil
smallImageRequestID = ZLPhotoManager.fetchImage(for: model.asset, size: size, completion: { [weak self] image, isDegraded in
if self?.imageIdentifier == self?.model.ident {
self?.imageView.image = image
}
if !isDegraded {
self?.smallImageRequestID = PHInvalidImageRequestID
}
})
}
private func fetchBigImage() {
cancelFetchBigImage()
bigImageReqeustID = ZLPhotoManager.fetchOriginalImageData(for: model.asset, progress: { [weak self] progress, _, _, _ in
if self?.model.isSelected == true {
self?.progressView.isHidden = false
self?.progressView.progress = max(0.1, progress)
self?.imageView.alpha = 0.5
if progress >= 1 {
self?.resetProgressViewStatus()
}
} else {
self?.cancelFetchBigImage()
}
}, completion: { [weak self] _, _, _ in
self?.resetProgressViewStatus()
})
}
private func cancelFetchBigImage() {
if bigImageReqeustID > PHInvalidImageRequestID {
PHImageManager.default().cancelImageRequest(bigImageReqeustID)
}
resetProgressViewStatus()
}
private func resetProgressViewStatus() {
progressView.isHidden = true
imageView.alpha = 1
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,233 @@
//
// ZLVideoManager.swift
// ZLPhotoBrowser
//
// Created by long on 2020/9/23.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import AVFoundation
import Photos
public class ZLVideoManager: NSObject {
class func getVideoExportFilePath(format: String? = nil) -> String {
let format = format ?? ZLPhotoConfiguration.default().cameraConfiguration.videoExportType.format
return NSTemporaryDirectory().appendingFormat("%@.%@", UUID().uuidString, format)
}
class func exportEditVideo(for asset: AVAsset, range: CMTimeRange, complete: @escaping ((URL?, Error?) -> Void)) {
let type: ZLVideoManager.ExportType = ZLPhotoConfiguration.default().cameraConfiguration.videoExportType == .mov ? .mov : .mp4
exportVideo(for: asset, range: range, exportType: type, presetName: AVAssetExportPresetPassthrough) { url, error in
if url != nil {
complete(url!, error)
} else {
complete(nil, error)
}
}
}
///
@objc public class func mergeVideos(fileUrls: [URL], completion: @escaping ((URL?, Error?) -> Void)) {
let composition = AVMutableComposition()
let assets = fileUrls.map { AVURLAsset(url: $0) }
var insertTime: CMTime = .zero
var assetVideoTracks: [AVAssetTrack] = []
let compositionVideoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: CMPersistentTrackID())!
let compositionAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: CMPersistentTrackID())!
for asset in assets {
do {
let timeRange = CMTimeRangeMake(start: .zero, duration: asset.duration)
if let videoTrack = asset.tracks(withMediaType: .video).first {
try compositionVideoTrack.insertTimeRange(
timeRange,
of: videoTrack,
at: insertTime
)
assetVideoTracks.append(videoTrack)
}
if let audioTrack = asset.tracks(withMediaType: .audio).first {
try compositionAudioTrack.insertTimeRange(
timeRange,
of: audioTrack,
at: insertTime
)
}
insertTime = CMTimeAdd(insertTime, asset.duration)
} catch {
completion(nil, NSError.videoMergeError)
return
}
}
guard assetVideoTracks.count == assets.count else {
completion(nil, NSError.videoMergeError)
return
}
let renderSize = getNaturalSize(videoTrack: assetVideoTracks[0])
let videoComposition = AVMutableVideoComposition()
videoComposition.instructions = getInstructions(compositionTrack: compositionVideoTrack, assetVideoTracks: assetVideoTracks, assets: assets)
videoComposition.frameDuration = assetVideoTracks[0].minFrameDuration
videoComposition.renderSize = renderSize
videoComposition.renderScale = 1
guard let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPreset1280x720) else {
completion(nil, NSError.videoMergeError)
return
}
let outputUrl = URL(fileURLWithPath: ZLVideoManager.getVideoExportFilePath())
exportSession.outputURL = outputUrl
exportSession.shouldOptimizeForNetworkUse = true
exportSession.outputFileType = ZLPhotoConfiguration.default().cameraConfiguration.videoExportType.avFileType
exportSession.videoComposition = videoComposition
exportSession.exportAsynchronously(completionHandler: {
let suc = exportSession.status == .completed
if exportSession.status == .failed {
zl_debugPrint("ZLPhotoBrowser: video merge failed: \(exportSession.error?.localizedDescription ?? "")")
}
ZLMainAsync {
completion(suc ? outputUrl : nil, exportSession.error)
}
})
}
private static func getNaturalSize(videoTrack: AVAssetTrack) -> CGSize {
var size = videoTrack.naturalSize
if isPortraitVideoTrack(videoTrack) {
swap(&size.width, &size.height)
}
return size
}
private static func getInstructions(
compositionTrack: AVMutableCompositionTrack,
assetVideoTracks: [AVAssetTrack],
assets: [AVURLAsset]
) -> [AVMutableVideoCompositionInstruction] {
var instructions: [AVMutableVideoCompositionInstruction] = []
var start: CMTime = .zero
for (index, videoTrack) in assetVideoTracks.enumerated() {
let asset = assets[index]
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionTrack)
layerInstruction.setTransform(videoTrack.preferredTransform, at: .zero)
let instruction = AVMutableVideoCompositionInstruction()
instruction.timeRange = CMTimeRangeMake(start: start, duration: asset.duration)
instruction.layerInstructions = [layerInstruction]
instructions.append(instruction)
start = CMTimeAdd(start, asset.duration)
}
return instructions
}
private static func isPortraitVideoTrack(_ track: AVAssetTrack) -> Bool {
let transform = track.preferredTransform
let tfA = transform.a
let tfB = transform.b
let tfC = transform.c
let tfD = transform.d
if (tfA == 0 && tfB == 1 && tfC == -1 && tfD == 0) ||
(tfA == 0 && tfB == 1 && tfC == 1 && tfD == 0) ||
(tfA == 0 && tfB == -1 && tfC == 1 && tfD == 0) {
return true
} else {
return false
}
}
}
// MARK: export methods
public extension ZLVideoManager {
@objc class func exportVideo(for asset: PHAsset, exportType: ZLVideoManager.ExportType = .mov, presetName: String = AVAssetExportPresetMediumQuality, complete: @escaping ((URL?, Error?) -> Void)) {
guard asset.mediaType == .video else {
complete(nil, NSError.videoExportTypeError)
return
}
_ = ZLPhotoManager.fetchAVAsset(forVideo: asset) { avAsset, _ in
if let set = avAsset {
self.exportVideo(for: set, exportType: exportType, presetName: presetName, complete: complete)
} else {
complete(nil, NSError.videoExportError)
}
}
}
@objc class func exportVideo(for asset: AVAsset, range: CMTimeRange = CMTimeRange(start: .zero, duration: .positiveInfinity), exportType: ZLVideoManager.ExportType = .mov, presetName: String = AVAssetExportPresetMediumQuality, complete: @escaping ((URL?, Error?) -> Void)) {
let outputUrl = URL(fileURLWithPath: getVideoExportFilePath(format: exportType.format))
guard let exportSession = AVAssetExportSession(asset: asset, presetName: presetName) else {
complete(nil, NSError.videoExportError)
return
}
exportSession.outputURL = outputUrl
exportSession.outputFileType = exportType.avFileType
exportSession.timeRange = range
exportSession.exportAsynchronously(completionHandler: {
let suc = exportSession.status == .completed
if exportSession.status == .failed {
zl_debugPrint("ZLPhotoBrowser: video export failed: \(exportSession.error?.localizedDescription ?? "")")
}
ZLMainAsync {
complete(suc ? outputUrl : nil, exportSession.error)
}
})
}
}
public extension ZLVideoManager {
@objc enum ExportType: Int {
var format: String {
switch self {
case .mov:
return "mov"
case .mp4:
return "mp4"
}
}
var avFileType: AVFileType {
switch self {
case .mov:
return .mov
case .mp4:
return .mp4
}
}
case mov
case mp4
}
}

View File

@@ -0,0 +1,50 @@
//
// ZLWeakProxy.swift
// ZLPhotoBrowser
//
// Created by long on 2021/3/10.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
class ZLWeakProxy: NSObject {
private weak var target: NSObjectProtocol?
init(target: NSObjectProtocol) {
self.target = target
super.init()
}
class func proxy(withTarget target: NSObjectProtocol) -> ZLWeakProxy {
return ZLWeakProxy(target: target)
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
return target
}
override func responds(to aSelector: Selector!) -> Bool {
return target?.responds(to: aSelector) ?? false
}
}