EasyMakePhotoPicker 0.1.2

EasyMakePhotoPicker 0.1.2

EasyMakePhotoPicker 维护。



 
依赖项
RxSwift>= 4.0
RxCocoa>= 4.0
 

  • 作者
  • Myung gi son

EasyMakePhotoPicker

Version License Platform Swift

如果您需要创建自己的 PhotoPicker,这并不容易实现,因为您需要实现许多实现 PhotoPicker 所需的功能(UI,业务逻辑)。因此,EasyMakePhotoPicker 提供了一个 PhotoPicker 的抽象层。EasyMakePhotoPicker 实现了所有 PhotoPicker 所需的业务逻辑,这样您就可以专注于 UI。

示例

EasyMakePhotoPicker 可以轻松实现类似 FacebookPhotoPicker 的功能。

alt text alt text alt text

三个组件(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协议。

注意:必须有一行符合PhotoCellableLivePhotoCellableVideoCellable中的一个。这是因为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)
}

单元

PhotoCollectionsViewConfigurePhotoCollectionsView提供要显示的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>

灵感

TLPhotoPicker

需求

iOS 9.1

安装

EasyMakePhotoPicker可通过CocoaPods使用。要安装它,只需将以下行添加到您的Podfile中:

platform :ios, '9.1'
pod "EasyMakePhotoPicker"

作者

Myung gi son, [email protected]

许可证

EasyMakePhotoPicker可在MIT许可证下使用。有关更多信息,请参阅LICENSE文件。