EasyMakePhotoPicker
如果您需要创建自己的 PhotoPicker,这并不容易实现,因为您需要实现许多实现 PhotoPicker 所需的功能(UI,业务逻辑)。因此,EasyMakePhotoPicker 提供了一个 PhotoPicker 的抽象层。EasyMakePhotoPicker 实现了所有 PhotoPicker 所需的业务逻辑,这样您就可以专注于 UI。
示例
EasyMakePhotoPicker 可以轻松实现类似 FacebookPhotoPicker 的功能。
三个组件(PhotosView, PhotoCollectionsView, PhotoManager)
EasyMakePhotoPicker 提供三个组件(PhotosView, PhotoCollectionsView, PhotoManager)。
PhotosView
PhotosView
是从 photoLibrary 中显示的图片网格视图。
- 自定义布局
- 自定义单元格(相机,照片,LivePhoto,视频)
- 类似于 Facebook 的 PhotoPicker,当您停止滚动时,它将运行 livePhoto 和 video。当选择 LivePhotoCell 或 VideCell 时,它会播放。
- 滚动性能优化 - 自动缓存和销毁图片。
- 所选顺序索引。
- 多选。
- 相机选择。
- 当 photoLibrary 发生变化时(例如插入,删除,更新,移动图片),自动更新 UI。
初始化器
init(configure: PhotosViewConfigure, photoAssetCollection: PhotoAssetCollection)
init(configure: PhotosViewConfigure, collectionType: PHAssetCollectionSubtype)
输入
// Note: 'selectedPhotosDidComplete' reacts when the signal come from selectionDidComplete.
var selectionDidComplete: PublishSubject<Void>
输出
var photoDidSelected: PublishSubject<PhotoAsset>
// Note: 'selectedPhotosDidComplete' reacts when the signal come from selectionDidComplete.
// only support when PhotosViewConfigure`s 'allowsMultipleSelection' property is true.
var selectedPhotosDidComplete: PublishSubject<[PhotoAsset]>
// only support when PhotosViewConfigure`s 'allowsMultipleSelection' property is true.
var selectedPhotosCount: PublishSubject<Int>
// only support when PhotosViewConfigure`s 'allowsMultipleSelection' property is true.
var photoDidDeselected: PublishSubject<PhotoAsset>
// only support when PhotosViewConfigure`s 'allowsCameraSelection' property is true.
var cameraDidClick: PublishSubject<Void>
公开方法
func change(photoAssetCollection: PhotoAssetCollection)
PhotosViewConfigure
通过PhotosViewConfigure来配置PhotosView。
protocol PhotosViewConfigure {
var fetchOptions: PHFetchOptions { get }
var allowsMultipleSelection: Bool { get }
var allowsCameraSelection: Bool { get }
// .video, .livePhoto
var allowsPlayTypes: [AssetType] { get }
var messageWhenMaxCountSelectedPhotosIsExceeded: String { get }
var maxCountSelectedPhotos: Int { get }
// get item image from PHCachingImageManager
// based on the UICollectionViewFlowLayout`s itemSize,
// therefore must set well itemSize in UICollectionViewFlowLayout.
var layout: UICollectionViewFlowLayout { get }
var photoCellTypeConverter: PhotoCellTypeConverter { get }
var livePhotoCellTypeConverter: LivePhotoCellTypeConverter { get }
var videoCellTypeConverter: VideoCellTypeConverter { get }
var cameraCellTypeConverter: CameraCellTypeConverter { get }
}
// example
class FacebookPhotosViewConfigure: PhotosViewConfigure {
var fetchOptions: PHFetchOptions = PHFetchOptions()
var allowsMultipleSelection: Bool = true
var allowsCameraSelection: Bool = true
// .video, .livePhoto
var allowsPlayTypes: [AssetType] = [.video, .livePhoto]
var messageWhenMaxCountSelectedPhotosIsExceeded: String = "over!!!"
var maxCountSelectedPhotos: Int = 15
var layout: UICollectionViewFlowLayout = FacebookPhotosLayout()
var cameraCellTypeConverter = CameraCellTypeConverter(type: FacebookCameraCell.self)
var photoCellTypeConverter = PhotoCellTypeConverter(type: FacebookPhotoCell.self)
var livePhotoCellTypeConverter = LivePhotoCellTypeConverter(type: FacebookLivePhotoCell.self)
var videoCellTypeConverter = VideoCellTypeConverter(type: FacebookVideoCell.self)
}
单元格
PhotosViewConfigure提供单元格以在PhotosView中显示(PhotoCell,VideoCell,LivePhotoCell和CameraCell)。
- 要提供PhotoCell,必须使
UICollectionViewCell
遵守PhotoCellable
协议。 - 要提供LivePhotoCell,必须使
UICollectionViewCell
遵守LivePhotoCellable
协议。 - 要提供VideoCell,必须继承
VideoCellable
协议。 - 要提供CameraCell,必须使
UICollectionViewCell
遵守CameraCellable
协议。
注意:必须有一行符合
PhotoCellable
、LivePhotoCellable
或VideoCellable
中的一个。这是因为PhotosView
是在MVVM架构
中实现的,协议决定了它属于哪种类型的CellViewModel
。如果单元格符合PhotoCellable
协议,则提供PhotoViewModel
给单元格。如果单元格符合LivePhotoCellable
协议,则提供LivePhotoCellViewModel
给单元格。如果单元格符合VideoCellable
协议,则提供VideoCellViewModel
给单元格。感谢MVVM架构,您可以使用CellViewModel的状态值轻松创建所需的单元格UI。
协议
protocol PhotoCellable: class {
var viewModel: PhotoCellViewModel? { get set }
}
protocol LivePhotoCellable: PhotoCellable { }
protocol VideoCellable: PhotoCellable { }
protocol CameraCellable: class { }
CellViewModels
class PhotoCellViewModel {
var image: Variable<UIImage?>
var isSelect: BehaviorSubject<Bool>
var selectedOrder: BehaviorSubject<Int>
...
}
class LivePhotoCellViewModel: PhotoCellViewModel {
...
var livePhoto: PHLivePhoto?
var playEvent: PublishSubject<PlayEvent>
var badgeImage: UIImage
}
class VideoCellViewModel: PhotoCellViewModel {
...
var playerItem: AVPlayerItem?
var duration: TimeInterval
}
// example
class FacebookPhotoCell: UICollectionViewCell, PhotoCellable {
// MARK: - Properties
var selectedView = UIView()
var orderLabel = FacebookNumberLabel()
var imageView = UIImageView()
var disposeBag: DisposeBag = DisposeBag()
var viewModel: PhotoCellViewModel? {
didSet {
guard let viewModel = viewModel else { return }
bind(viewModel: viewModel)
}
}
// MARK: - Set up views
...
func addSubviews() {
...
}
func setupConstraints() {
...
}
// MARK: - Bind
func bind(viewModel: PhotoCellViewModel) {
viewModel.isSelect
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self, weak viewModel] isSelect in
guard let `self` = self,
let `viewModel` = viewModel else { return }
self.selectedView.isHidden = !isSelect
if viewModel.configure.allowsMultipleSelection {
self.orderLabel.isHidden = !isSelect
}
else {
self.orderLabel.isHidden = true
}
})
.disposed(by: disposeBag)
viewModel.isSelect
.skip(1)
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] isSelect in
guard let `self` = self else { return }
if isSelect {
self.cellAnimationWhenSelectedCell()
}
else {
self.cellAnimationWhenDeselectedCell()
}
})
.disposed(by: disposeBag)
viewModel.selectedOrder
.subscribe(onNext: { [weak self] selectedOrder in
guard let `self` = self else { return }
self.orderLabel.text = "\(selectedOrder)"
})
.disposed(by: disposeBag)
viewModel.image.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] image in
guard let `self` = self else { return }
self.imageView.image = image
})
.disposed(by: disposeBag)
}
}
class FacebookVideoCell: FacebookPhotoCell, VideoCellable {
var durationLabel: UILabel = DurationLabel()
var playerView = PlayerView()
fileprivate var player: AVPlayer? {
didSet {
if let player = player {
playerView.playerLayer.player = player
NotificationCenter.default.addObserver(
forName: .AVPlayerItemDidPlayToEndTime,
object: player.currentItem,
queue: nil) { _ in
DispatchQueue.main.async {
player.seek(to: kCMTimeZero)
player.play()
}
}
}
else {
playerView.playerLayer.player = nil
NotificationCenter.default.removeObserver(self)
}
}
}
var durationBackgroundView = UIView()
var videoIconImageView = UIImageView(image: #imageLiteral(resourceName: "video"))
var duration: TimeInterval = 0.0 {
didSet {
durationLabel.text = timeFormatted(timeInterval: duration)
}
}
// MARK: - Life Cycle
override func addSubviews() {
super.addSubviews()
...
}
override func setupConstraints() {
super.setupConstraints()
...
}
// MARK: - Bind
override func bind(viewModel: PhotoCellViewModel) {
super.bind(viewModel: viewModel)
if let viewModel = viewModel as? VideoCellViewModel {
duration = viewModel.duration
viewModel.playEvent.asObserver()
.subscribe(onNext: { [weak self] playEvent in
guard let `self` = self else { return }
switch playEvent {
case .play: self.play()
case .stop: self.stop()
}
})
.disposed(by: disposeBag)
viewModel.isSelect
.subscribe(onNext: { [weak self] isSelect in
guard let `self` = self else { return }
if isSelect {
self.durationBackgroundView.backgroundColor =
Color.selectedDurationBackgroundViewBGColor
}
else {
self.durationBackgroundView.backgroundColor =
Color.deselectedDurationBackgroundViewBGColor
}
})
.disposed(by: disposeBag)
}
}
fileprivate func play() {
guard let viewModel = viewModel as? VideoCellViewModel,
let playerItem = viewModel.playerItem else { return }
self.player = AVPlayer(playerItem: playerItem)
if let player = player {
playerView.isHidden = false
player.play()
}
}
fileprivate func stop() {
if let player = player {
player.pause();
self.player = nil
playerView.isHidden = true
}
}
}
...
布局
通过提供PhotosViewConfigure的布局(UICollectionViewFlowLayout),PhotosView将显示由布局提供的单元格。
// example
class FacebookPhotosLayout: UICollectionViewFlowLayout {
// MARK: - Constant
fileprivate struct Constant {
static let padding = CGFloat(5)
static let numberOfColumns = CGFloat(3)
}
override var itemSize: CGSize {
set { }
get {
guard let collectionView = collectionView else { return .zero }
let collectionViewWidth = (collectionView.bounds.width)
let columnWidth = (collectionViewWidth -
Constant.padding * (Constant.numberOfColumns - 1)) / Constant.numberOfColumns
return CGSize(width: columnWidth, height: columnWidth)
}
}
override init() {
super.init()
setupLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init()
setupLayout()
}
func setupLayout() {
minimumLineSpacing = Constant.padding
minimumInteritemSpacing = Constant.padding
}
}
用法
class FacebookPhotoPickerVC: UIViewController {
...
var photosViewConfigure = FacebookPhotosViewConfigure()
lazy var photosView: PhotosView = { [unowned] self
let pv = PhotosView(
configure: self.photosViewConfigure,
collectionType: .smartAlbumUserLibrary)
return pv
}()
override func viewDidLoad() {
super.viewDidLoad()
// MARK: - add view
...
// MARK: - bind
...
doneButton.rx.tap
.observeOn(MainScheduler.instance)
.subscribe(onNext: { _ in
self.photosView.selectionDidComplete.onNext()
self.dismiss(animated: true, completion: nil)
})
.disposed(by: disposeBag)
photosView.selectedPhotosDidComplete
.subscribe(onNext: { [weak self] photoAssets in
guard let `self` = self else { return }
self.selectedPhotoAssetsDidComplete.onNext(photoAssets)
})
.disposed(by: disposeBag)
photosView.outputs.cameraDidClick
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] in
guard let `self` = self else { return }
self.showCamera()
})
.disposed(by: disposeBag)
...
}
....
}
PhotoCollectionsView
PhotoCollectionsView
是一个视图,用于显示从照片库中获取的相册列表。
- 自定义单元格(PhotoCollection)
- 自定义布局
- 当照片库发生变化时,自动更新UI。
初始化器
init(frame: CGRect, configure: PhotoCollectionsViewConfigure)
init(configure: PhotoCollectionsViewConfigure)
输入
// force cell selection.
var cellDidSelect: PublishSubject<IndexPath>
输出
var selectedPhotoCollectionWhenCellDidSelect: PublishSubject<(IndexPath, PhotoAssetCollection)>
PhotoCollectionsViewConfigure
PhotoCollectionsView通过PhotoCollectionsViewConfigure进行配置。
protocol PhotoCollectionsViewConfigure {
var fetchOptions: PHFetchOptions { get }
// to show collection types.
var showsCollectionTypes: [PHAssetCollectionSubtype] { get }
// size of view to show thumbnailImage in your cell and
// photoCollectionThumbnailSize must be the same
// because get photo collection thumbnail image from PHCachingImageManager
// based on the 'photoCollectionThumbnailSize'
var photoCollectionThumbnailSize: CGSize { get }
var layout: UICollectionViewFlowLayout { get }
var photoCollectionCellTypeConverter: PhotoCollectionCellTypeConverter { get }
}
// example
struct FacebookPhotoCollectionsViewConfigure: PhotoCollectionsViewConfigure {
var fetchOptions = PHFetchOptions()
// to show collection types.
var showsCollectionTypes: [PHAssetCollectionSubtype] = [
.smartAlbumUserLibrary,
.smartAlbumGeneric,
.smartAlbumFavorites,
.smartAlbumRecentlyAdded,
.smartAlbumVideos,
.smartAlbumPanoramas,
.smartAlbumBursts,
.smartAlbumScreenshots
]
var photoCollectionThumbnailSize = CGSize(width: 54, height: 54)
var layout: UICollectionViewFlowLayout = FacebookPhotoCollectionsLayout()
var photoCollectionCellTypeConverter =
PhotoCollectionCellTypeConverter(type: FacebookPhotoCollectionCell.self)
}
单元
PhotoCollectionsViewConfigure
为PhotoCollectionsView
提供要显示的Cell(PhotoCollectionCell
)。
- 为了提供PhotoCollectionCell,
UICollectionViewCell
必须继承PhotoCollectionCellable
协议。
注意:单元格必须符合
PhotoCollectionCellable
协议。这是因为PhotoCollectionsView
是在MVVM架构
下实现的,协议决定了它是什么样的CellViewModel
。多亏了MVVM架构,你可以轻松地使用CellViewModel的状态值创建所需的单元格UI。
协议
protocol PhotoCollectionCellable {
var viewModel: PhotoCollectionCellViewModel? { get set }
}
ViewModel
class PhotoCollectionCellViewModel {
var count: BehaviorSubject<Int>
var thumbnail = BehaviorSubject<UIImage?>
var title: BehaviorSubject<String>
var isSelect: Variable<Bool>
}
// example
class FacebookPhotoCollectionCell: BaseCollectionViewCell, PhotoCollectionCellable {
var checkView: UIView = CheckImageView()
var thumbnailImageView = UIImageView()
var titleLabel = UILabel()
var countLabel = UILabel()
var lineView = UIView()
var disposeBag = DisposeBag()
var viewModel: PhotoCollectionCellViewModel? {
didSet {
guard let viewModel = viewModel else { return }
bind(viewModel: viewModel)
}
}
// MARK: - Life Cycle
override func setupViews() {
...
}
override func setupConstraints() {
...
}
// MARK: - Bind
func bind(viewModel: PhotoCollectionCellViewModel) {
viewModel.isSelect.asObservable()
.subscribe(onNext: { [weak self] isSelect in
guard let`self` = self else { return }
if isSelect {
self.checkView.isHidden = false
}
else {
self.checkView.isHidden = true
}
})
.disposed(by: disposeBag)
viewModel.count
.subscribe(onNext: { [weak self] count in
guard let `self` = self else { return }
self.countLabel.text = "\(count)"
})
.disposed(by: disposeBag)
viewModel.thumbnail
.subscribe(onNext: { [weak self] thumbnail in
guard let `self` = self else { return }
self.thumbnailImageView.image = thumbnail
})
.disposed(by: disposeBag)
viewModel.title
.subscribe(onNext: { [weak self] title in
guard let `self` = self else { return }
self.titleLabel.text = title
})
.disposed(by: disposeBag)
}
}
布局
提供PhotoCollectionsViewConfigure的布局(UICollectionViewFlowLayout),PhotoCollectionsView将显示提供的布局的单元格。
// example
class FacebookPhotoCollectionsLayout: UICollectionViewFlowLayout {
override var itemSize: CGSize {
set { }
get {
guard let collectionView = collectionView else { return .zero }
return CGSize(width: collectionView.frame.width, height: 80)
}
}
override init() {
super.init()
setupLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init()
setupLayout()
}
func setupLayout() {
minimumInteritemSpacing = 0
minimumLineSpacing = 0
scrollDirection = .vertical
}
}
使用说明
class FacebookPhotoPickerVC: UIViewController {
...
var photoCollectionsViewConfigure = FacebookPhotoCollectionsViewConfigure()
lazy var photoCollectionsView: PhotoCollectionsView = { [unowned] self
let pv = PhotoCollectionsView(
configure: self.photoCollectionsViewConfigure)
return pv
}()
override func viewDidLoad() {
super.viewDidLoad()
// MARK: - set up views
...
// MARK: - bind
...
photoCollectionsView.selectedPhotoCollectionWhenCellDidSelect
.subscribe(onNext: { [weak self] (selectedIndexPath, selectedPhotoAssetCollection) in
guard let `self` = self else { return }
...
self.photosView.change(photoAssetCollection: selectedPhotoAssetCollection)
})
.disposed(by: disposeBag)
....
}
PhotoManager
PhotoManager
是一个对PhotoCacheImageManager的封装类,它将PhotoCacheImageManager
(获取照片,获取相册,缓存等)的功能作为Observable提供。
func startCaching(assets: [PHAsset], targetSize: CGSize, contentMode: PHImageContentMode, options: PHImageRequestOptions?)
func stopCaching(assets: [PHAsset], targetSize: CGSize, contentMode: PHImageContentMode, options: PHImageRequestOptions?)
func stopCachingForAllAssets()
func cancel(imageRequest requestID: PHImageRequestID)
func photoLibraryDidChange(_ changeInstance: PHChange)
func performChanges(changeBlock: @escaping () -> Void) -> Observable<PerformChangesEvent>
func fetchCollections(assetCollectionTypes: [PHAssetCollectionSubtype], thumbnailImageSize: CGSize, options: PHFetchOptions? = nil) -> Observable<[PhotoAssetCollection]>
func image(for asset: PHAsset, size: CGSize = CGSize(width: 720, height: 1280), options: PHImageRequestOptions? = nil) -> Observable<UIImage>
func livePhoto(for asset: PHAsset, size: CGSize = CGSize(width: 720, height: 1280)) -> Observable<LivePhotoDownloadEvent>
func video(for asset: PHAsset, size: CGSize = CGSize(width: 720, height: 1280)) -> Observable<VideoDownloadEvent>
func cloudImage(for asset: PHAsset, size: CGSize = PHImageManagerMaximumSize) -> Observable<CloudPhotoDownLoadEvent>
func fullResolutionImage(for asset: PHAsset) -> Observable<UIImage>
func checkPhotoLibraryPermission() -> Observable<Bool>
func checkCameraPermission() -> Observable<Bool>
灵感
需求
iOS 9.1
安装
EasyMakePhotoPicker可通过CocoaPods使用。要安装它,只需将以下行添加到您的Podfile中:
platform :ios, '9.1'
pod "EasyMakePhotoPicker"
作者
Myung gi son, [email protected]
许可证
EasyMakePhotoPicker可在MIT许可证下使用。有关更多信息,请参阅LICENSE文件。