Sketch out the photo collection picker.

This commit is contained in:
Matthew Chen 2018-11-14 10:14:39 -05:00
parent 9641edbfd2
commit ea080eda72
14 changed files with 564 additions and 197 deletions

View File

@ -154,6 +154,9 @@
3491D9A121022DB7001EF5A1 /* CDSSigningCertificateTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 3491D9A021022DB7001EF5A1 /* CDSSigningCertificateTest.m */; };
3496744D2076768700080B5F /* OWSMessageBubbleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496744C2076768700080B5F /* OWSMessageBubbleView.m */; };
3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496744E2076ACCE00080B5F /* LongTextViewController.swift */; };
3496955C219B605E00DCFE74 /* ImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34969559219B605E00DCFE74 /* ImagePickerController.swift */; };
3496955D219B605E00DCFE74 /* PhotoCollectionPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */; };
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496955B219B605E00DCFE74 /* PhotoLibrary.swift */; };
349EA07C2162AEA800F7B17F /* OWS111UDAttributesMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349EA07B2162AEA700F7B17F /* OWS111UDAttributesMigration.swift */; };
34A55F3720485465002CC6DE /* OWS2FARegistrationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A55F3520485464002CC6DE /* OWS2FARegistrationViewController.m */; };
34A8B3512190A40E00218A25 /* MediaAlbumCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A8B3502190A40E00218A25 /* MediaAlbumCellView.swift */; };
@ -434,7 +437,6 @@
4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */; };
4C11AA5020FD59C700351FBD /* MessageStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C11AA4F20FD59C700351FBD /* MessageStatusView.swift */; };
4C13C9F620E57BA30089A98B /* ColorPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */; };
4C1885D0218D0EA800B67051 /* ImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1885CF218D0EA800B67051 /* ImagePickerController.swift */; };
4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */; };
4C20B2B720CA0034001BAC90 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542DF51208B82E9007B4E76 /* ThreadViewModel.swift */; };
4C20B2B920CA10DE001BAC90 /* ConversationSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */; };
@ -807,6 +809,9 @@
3496744B2076768600080B5F /* OWSMessageBubbleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageBubbleView.h; sourceTree = "<group>"; };
3496744C2076768700080B5F /* OWSMessageBubbleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageBubbleView.m; sourceTree = "<group>"; };
3496744E2076ACCE00080B5F /* LongTextViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LongTextViewController.swift; sourceTree = "<group>"; };
34969559219B605E00DCFE74 /* ImagePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerController.swift; sourceTree = "<group>"; };
3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionPickerController.swift; sourceTree = "<group>"; };
3496955B219B605E00DCFE74 /* PhotoLibrary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoLibrary.swift; sourceTree = "<group>"; };
349EA07B2162AEA700F7B17F /* OWS111UDAttributesMigration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWS111UDAttributesMigration.swift; sourceTree = "<group>"; };
34A55F3520485464002CC6DE /* OWS2FARegistrationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWS2FARegistrationViewController.m; sourceTree = "<group>"; };
34A55F3620485464002CC6DE /* OWS2FARegistrationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWS2FARegistrationViewController.h; sourceTree = "<group>"; };
@ -1137,7 +1142,6 @@
4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HapticFeedback.swift; path = UserInterface/HapticFeedback.swift; sourceTree = "<group>"; };
4C11AA4F20FD59C700351FBD /* MessageStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStatusView.swift; sourceTree = "<group>"; };
4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerViewController.swift; sourceTree = "<group>"; };
4C1885CF218D0EA800B67051 /* ImagePickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerController.swift; sourceTree = "<group>"; };
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoGridViewCell.swift; sourceTree = "<group>"; };
4C1D2333218B692800A0598F /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = translations/ko.lproj/Localizable.strings; sourceTree = "<group>"; };
4C1D2334218B6A1100A0598F /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = translations/az.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -1735,13 +1739,25 @@
path = mocks;
sourceTree = "<group>";
};
34969558219B605E00DCFE74 /* PhotoLibrary */ = {
isa = PBXGroup;
children = (
34969559219B605E00DCFE74 /* ImagePickerController.swift */,
3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */,
3496955B219B605E00DCFE74 /* PhotoLibrary.swift */,
);
path = PhotoLibrary;
sourceTree = "<group>";
};
34B3F8331E8DF1700035BE1A /* ViewControllers */ = {
isa = PBXGroup;
children = (
452B998F20A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift */,
340FC87A204DAC8C007AEB0F /* AppSettings */,
34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */,
34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */,
34B3F83B1E8DF1700035BE1A /* CallViewController.swift */,
4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */,
348BB25C20A0C5530047AEC2 /* ContactShareViewHelper.swift */,
34B3F83E1E8DF1700035BE1A /* ContactsPicker.swift */,
34E88D252098C5AE00A608F4 /* ContactViewController.swift */,
@ -1770,6 +1786,7 @@
45D2AC01204885170033C692 /* OWS2FAReminderViewController.swift */,
345BC30A2047030600257B7C /* OWS2FASettingsViewController.h */,
345BC30B2047030600257B7C /* OWS2FASettingsViewController.m */,
34969558219B605E00DCFE74 /* PhotoLibrary */,
34CE88E51F2FB9A10098030F /* ProfileViewController.h */,
34CE88E61F2FB9A10098030F /* ProfileViewController.m */,
340FC875204DAC8C007AEB0F /* Registration */,
@ -1778,9 +1795,6 @@
34B3F86E1E8DF1700035BE1A /* SignalsNavigationController.m */,
340FC897204DAC8D007AEB0F /* ThreadSettings */,
34D1F0BE1F8EC1760066283D /* Utils */,
452B998F20A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift */,
4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */,
4C1885CF218D0EA800B67051 /* ImagePickerController.swift */,
);
path = ViewControllers;
sourceTree = "<group>";
@ -3387,6 +3401,7 @@
34B3F8931E8DF1710035BE1A /* SignalsNavigationController.m in Sources */,
34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */,
45B27B862037FFB400A539DF /* DebugUIFileBrowser.swift in Sources */,
3496955D219B605E00DCFE74 /* PhotoCollectionPickerController.swift in Sources */,
34CE88E71F2FB9A10098030F /* ProfileViewController.m in Sources */,
3403B95D20EA9527001A1F44 /* OWSContactShareButtonsView.m in Sources */,
34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */,
@ -3396,6 +3411,7 @@
34D1F0AE1F867BFC0066283D /* OWSMessageCell.m in Sources */,
4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */,
4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */,
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */,
45D231771DC7E8F10034FA89 /* SessionResetJob.swift in Sources */,
340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */,
452037D11EE84975004E4CDF /* DebugUISessionState.m in Sources */,
@ -3461,7 +3477,6 @@
45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */,
340FC8BB204DAC8D007AEB0F /* OWSAddToContactViewController.m in Sources */,
340FC8C0204DB7D2007AEB0F /* OWSBackupExportJob.m in Sources */,
4C1885D0218D0EA800B67051 /* ImagePickerController.swift in Sources */,
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
340FC8A7204DAC8D007AEB0F /* RegistrationViewController.m in Sources */,
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */,
@ -3480,6 +3495,7 @@
4CA5F793211E1F06008C2708 /* Toast.swift in Sources */,
3488F9362191CC4000E524CC /* ConversationMediaView.swift in Sources */,
45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */,
3496955C219B605E00DCFE74 /* ImagePickerController.swift in Sources */,
34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */,
457F671B20746193000EABCD /* QuotedReplyPreview.swift in Sources */,
34DBF004206BD5A500025978 /* OWSBubbleView.m in Sources */,

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "navbar_disclosure_down_small@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "navbar_disclosure_down_small@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "navbar_disclosure_down_small@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "navbar_disclosure_up_small@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "navbar_disclosure_up_small@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "navbar_disclosure_up_small@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -15,7 +15,7 @@
@objc init(fileURL: URL) {
self.fileURL = fileURL
super.init(nibName: nil, bundle: nil)
super.init()
self.contents = buildContents()
}

View File

@ -12,21 +12,25 @@ protocol ImagePickerControllerDelegate {
}
@objc(OWSImagePickerGridController)
class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate {
class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate, PhotoCollectionPickerDelegate {
@objc
weak var delegate: ImagePickerControllerDelegate?
private let library: PhotoLibrary = PhotoLibrary()
private let libraryAlbum: PhotoLibraryAlbum
var availableWidth: CGFloat = 0
private var photoCollection: PhotoCollection
private var photoCollectionContents: PhotoCollectionContents
private let photoMediaSize = PhotoMediaSize()
var collectionViewFlowLayout: UICollectionViewFlowLayout
private let titleLabel = UILabel()
init() {
collectionViewFlowLayout = type(of: self).buildLayout()
libraryAlbum = library.albumForAllPhotos()
photoCollection = library.defaultPhotoCollection()
photoCollectionContents = photoCollection.contents()
super.init(collectionViewLayout: collectionViewFlowLayout)
}
@ -39,9 +43,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
override func viewDidLoad() {
super.viewDidLoad()
self.title = libraryAlbum.localizedTitle
library.delegate = self
library.add(delegate: self)
guard let collectionView = collectionView else {
owsFailDebug("collectionView was unexpectedly nil")
@ -53,6 +55,23 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel,
target: self,
action: #selector(didPressCancel))
titleLabel.text = photoCollection.localizedTitle()
titleLabel.textColor = Theme.primaryColor
titleLabel.font = UIFont.ows_dynamicTypeBody.ows_mediumWeight()
let titleIconView = UIImageView()
titleIconView.tintColor = Theme.primaryColor
titleIconView.image = UIImage(named: "navbar_disclosure_down")?.withRenderingMode(.alwaysTemplate)
let titleView = UIStackView(arrangedSubviews: [titleLabel, titleIconView])
titleView.axis = .horizontal
titleView.alignment = .center
titleView.spacing = 5
titleView.isUserInteractionEnabled = true
titleView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(titleTapped)))
navigationItem.titleView = titleView
let featureFlag_isMultiselectEnabled = true
if featureFlag_isMultiselectEnabled {
updateSelectButton()
@ -72,7 +91,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
// Determine the size of the thumbnails to request
let scale = UIScreen.main.scale
let cellSize = collectionViewFlowLayout.itemSize
libraryAlbum.thumbnailSize = CGSize(width: cellSize.width * scale, height: cellSize.height * scale)
photoMediaSize.thumbnailSize = CGSize(width: cellSize.width * scale, height: cellSize.height * scale)
}
// MARK: Actions
@ -160,8 +179,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
return
}
let assets: [PHAsset] = indexPaths.compactMap { return self.libraryAlbum.asset(at: $0.row) }
let promises = assets.map { return libraryAlbum.outgoingAttachment(for: $0) }
let assets: [PHAsset] = indexPaths.compactMap { return photoCollectionContents.asset(at: $0.row) }
let promises = assets.map { return photoCollectionContents.outgoingAttachment(for: $0) }
when(fulfilled: promises).map { attachments in
self.dismiss(animated: true) {
self.delegate?.imagePicker(self, didPickImageAttachments: attachments)
@ -217,15 +236,39 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
collectionView?.reloadData()
}
// MARK: PhotoCollectionPickerDelegate
func photoCollectionPicker(_ photoCollectionPicker: PhotoCollectionPickerController, didPickCollection collection: PhotoCollection) {
photoCollection = collection
photoCollectionContents = photoCollection.contents()
titleLabel.text = photoCollection.localizedTitle()
collectionView?.reloadData()
}
// MARK: - Event Handlers
@objc func titleTapped(sender: UIGestureRecognizer) {
guard sender.state == .recognized else {
return
}
let view = PhotoCollectionPickerController(library: library,
lastPhotoCollection: photoCollection,
collectionDelegate: self)
let nav = UINavigationController(rootViewController: view)
self.present(nav, animated: true, completion: nil)
}
// MARK: UICollectionView
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if isInBatchSelectMode {
updateDoneButton()
} else {
let asset = libraryAlbum.asset(at: indexPath.row)
let asset = photoCollectionContents.asset(at: indexPath.row)
firstly {
libraryAlbum.outgoingAttachment(for: asset)
photoCollectionContents.outgoingAttachment(for: asset)
}.map { attachment in
self.dismiss(animated: true) {
self.delegate?.imagePicker(self, didPickImageAttachments: [attachment])
@ -243,7 +286,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return libraryAlbum.count
return photoCollectionContents.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
@ -251,183 +294,9 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
owsFail("cell was unexpectedly nil")
}
let mediaItem = libraryAlbum.mediaItem(at: indexPath.item)
cell.configure(item: mediaItem)
let assetItem = photoCollectionContents.assetItem(at: indexPath.item, photoMediaSize: photoMediaSize)
cell.configure(item: assetItem)
return cell
}
}
protocol PhotoLibraryDelegate: class {
func photoLibraryDidChange(_ photoLibrary: PhotoLibrary)
}
class ImagePickerGridItem: PhotoGridItem {
let asset: PHAsset
let album: PhotoLibraryAlbum
init(asset: PHAsset, album: PhotoLibraryAlbum) {
self.asset = asset
self.album = album
}
// MARK: PhotoGridItem
var type: PhotoGridItemType {
if asset.mediaType == .video {
return .video
}
// TODO show GIF badge?
return .photo
}
func asyncThumbnail(completion: @escaping (UIImage?) -> Void) -> UIImage? {
album.requestThumbnail(for: self.asset) { image, _ in
completion(image)
}
return nil
}
}
class PhotoLibraryAlbum {
let fetchResult: PHFetchResult<PHAsset>
let localizedTitle: String?
var thumbnailSize: CGSize = .zero
enum PhotoLibraryError: Error {
case assertionError(description: String)
case unsupportedMediaType
}
init(fetchResult: PHFetchResult<PHAsset>, localizedTitle: String?) {
self.fetchResult = fetchResult
self.localizedTitle = localizedTitle
}
var count: Int {
return fetchResult.count
}
private let imageManager = PHCachingImageManager()
func asset(at index: Int) -> PHAsset {
return fetchResult.object(at: index)
}
func mediaItem(at index: Int) -> ImagePickerGridItem {
let mediaAsset = asset(at: index)
return ImagePickerGridItem(asset: mediaAsset, album: self)
}
// MARK: ImageManager
func requestThumbnail(for asset: PHAsset, resultHandler: @escaping (UIImage?, [AnyHashable: Any]?) -> Void) {
_ = imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .aspectFill, options: nil, resultHandler: resultHandler)
}
private func requestImageDataSource(for asset: PHAsset) -> Promise<(dataSource: DataSource, dataUTI: String)> {
return Promise { resolver in
_ = imageManager.requestImageData(for: asset, options: nil) { imageData, dataUTI, _, _ in
guard let imageData = imageData else {
resolver.reject(PhotoLibraryError.assertionError(description: "imageData was unexpectedly nil"))
return
}
guard let dataUTI = dataUTI else {
resolver.reject(PhotoLibraryError.assertionError(description: "dataUTI was unexpectedly nil"))
return
}
guard let dataSource = DataSourceValue.dataSource(with: imageData, utiType: dataUTI) else {
resolver.reject(PhotoLibraryError.assertionError(description: "dataSource was unexpectedly nil"))
return
}
resolver.fulfill((dataSource: dataSource, dataUTI: dataUTI))
}
}
}
private func requestVideoDataSource(for asset: PHAsset) -> Promise<(dataSource: DataSource, dataUTI: String)> {
return Promise { resolver in
_ = imageManager.requestExportSession(forVideo: asset, options: nil, exportPreset: AVAssetExportPresetMediumQuality) { exportSession, _ in
guard let exportSession = exportSession else {
resolver.reject(PhotoLibraryError.assertionError(description: "exportSession was unexpectedly nil"))
return
}
exportSession.outputFileType = AVFileType.mp4
exportSession.metadataItemFilter = AVMetadataItemFilter.forSharing()
let exportPath = OWSFileSystem.temporaryFilePath(withFileExtension: "mp4")
let exportURL = URL(fileURLWithPath: exportPath)
exportSession.outputURL = exportURL
Logger.debug("starting video export")
exportSession.exportAsynchronously {
Logger.debug("Completed video export")
guard let dataSource = DataSourcePath.dataSource(with: exportURL, shouldDeleteOnDeallocation: true) else {
resolver.reject(PhotoLibraryError.assertionError(description: "Failed to build data source for exported video URL"))
return
}
resolver.fulfill((dataSource: dataSource, dataUTI: kUTTypeMPEG4 as String))
}
}
}
}
func outgoingAttachment(for asset: PHAsset) -> Promise<SignalAttachment> {
switch asset.mediaType {
case .image:
return requestImageDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in
return SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .medium)
}
case .video:
return requestVideoDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in
return SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI)
}
default:
return Promise(error: PhotoLibraryError.unsupportedMediaType)
}
}
}
class PhotoLibrary: NSObject, PHPhotoLibraryChangeObserver {
weak var delegate: PhotoLibraryDelegate?
var assetCollection: PHAssetCollection!
var availableWidth: CGFloat = 0
func photoLibraryDidChange(_ changeInstance: PHChange) {
DispatchQueue.main.async {
self.delegate?.photoLibraryDidChange(self)
}
}
override init() {
super.init()
PHPhotoLibrary.shared().register(self)
}
deinit {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
func albumForAllPhotos() -> PhotoLibraryAlbum {
let allPhotosOptions = PHFetchOptions()
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
let fetchResult = PHAsset.fetchAssets(with: allPhotosOptions)
let title = NSLocalizedString("PHOTO_PICKER_DEFAULT_ALBUM", comment: "navbar title when viewing the default photo album, which includes all photos")
return PhotoLibraryAlbum(fetchResult: fetchResult, localizedTitle: title)
}
}

View File

@ -0,0 +1,146 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import Photos
import PromiseKit
protocol PhotoCollectionPickerDelegate: class {
func photoCollectionPicker(_ photoCollectionPicker: PhotoCollectionPickerController, didPickCollection collection: PhotoCollection)
}
class PhotoCollectionPickerController: OWSTableViewController, PhotoLibraryDelegate {
private weak var collectionDelegate: PhotoCollectionPickerDelegate?
private let library: PhotoLibrary
private let lastPhotoCollection: PhotoCollection
private var photoCollections: PhotoCollections
required init(library: PhotoLibrary,
lastPhotoCollection: PhotoCollection,
collectionDelegate: PhotoCollectionPickerDelegate) {
self.library = library
self.lastPhotoCollection = lastPhotoCollection
self.photoCollections = library.allPhotoCollections()
self.collectionDelegate = collectionDelegate
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
let titleLabel = UILabel()
titleLabel.text = lastPhotoCollection.localizedTitle()
titleLabel.textColor = Theme.primaryColor
titleLabel.font = UIFont.ows_dynamicTypeBody.ows_mediumWeight()
let titleIconView = UIImageView()
titleIconView.tintColor = Theme.primaryColor
titleIconView.image = UIImage(named: "navbar_disclosure_up")?.withRenderingMode(.alwaysTemplate)
let titleView = UIStackView(arrangedSubviews: [titleLabel, titleIconView])
titleView.axis = .horizontal
titleView.alignment = .center
titleView.spacing = 5
titleView.isUserInteractionEnabled = true
titleView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(titleTapped)))
navigationItem.titleView = titleView
library.add(delegate: self)
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel,
target: self,
action: #selector(didPressCancel))
updateContents()
}
private func updateContents() {
photoCollections = library.allPhotoCollections()
let section = OWSTableSection()
let count = photoCollections.count
for index in 0..<count {
let collection = photoCollections.collection(at: index)
section.add(OWSTableItem.init(customCellBlock: { () -> UITableViewCell in
let cell = OWSTableItem.newCell()
let imageView = UIImageView()
let kImageSize = 50
imageView.autoSetDimensions(to: CGSize(width: kImageSize, height: kImageSize))
let contents = collection.contents()
if contents.count > 0 {
let photoMediaSize = PhotoMediaSize(thumbnailSize: CGSize(width: kImageSize, height: kImageSize))
let assetItem = contents.assetItem(at: 0, photoMediaSize: photoMediaSize)
imageView.image = assetItem.asyncThumbnail { [weak imageView] image in
guard let strongImageView = imageView else {
return
}
guard let image = image else {
return
}
strongImageView.image = image
}
}
let titleLabel = UILabel()
titleLabel.text = collection.localizedTitle()
titleLabel.font = UIFont.ows_regularFont(withSize: 18)
titleLabel.textColor = Theme.primaryColor
let stackView = UIStackView(arrangedSubviews: [imageView, titleLabel])
stackView.axis = .horizontal
stackView.alignment = .center
stackView.spacing = 10
cell.contentView.addSubview(stackView)
stackView.ows_autoPinToSuperviewMargins()
return cell
},
customRowHeight: UITableViewAutomaticDimension,
actionBlock: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.didSelectCollection(collection: collection)
}))
}
let contents = OWSTableContents()
contents.addSection(section)
self.contents = contents
}
// MARK: Actions
@objc
func didPressCancel(sender: UIBarButtonItem) {
self.dismiss(animated: true)
}
func didSelectCollection(collection: PhotoCollection) {
collectionDelegate?.photoCollectionPicker(self, didPickCollection: collection)
self.dismiss(animated: true)
}
@objc func titleTapped(sender: UIGestureRecognizer) {
guard sender.state == .recognized else {
return
}
self.dismiss(animated: true)
}
// MARK: PhotoLibraryDelegate
func photoLibraryDidChange(_ photoLibrary: PhotoLibrary) {
updateContents()
}
}

View File

@ -0,0 +1,288 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import Photos
import PromiseKit
protocol PhotoLibraryDelegate: class {
func photoLibraryDidChange(_ photoLibrary: PhotoLibrary)
}
class PhotoMediaSize {
var thumbnailSize: CGSize
init() {
self.thumbnailSize = .zero
}
init(thumbnailSize: CGSize) {
self.thumbnailSize = thumbnailSize
}
}
class PhotoPickerAssetItem: PhotoGridItem {
let asset: PHAsset
let album: PhotoCollectionContents
let photoMediaSize: PhotoMediaSize
init(asset: PHAsset, album: PhotoCollectionContents, photoMediaSize: PhotoMediaSize) {
self.asset = asset
self.album = album
self.photoMediaSize = photoMediaSize
}
// MARK: PhotoGridItem
var type: PhotoGridItemType {
if asset.mediaType == .video {
return .video
}
// TODO show GIF badge?
return .photo
}
func asyncThumbnail(completion: @escaping (UIImage?) -> Void) -> UIImage? {
album.requestThumbnail(for: self.asset, thumbnailSize: photoMediaSize.thumbnailSize) { image, _ in
completion(image)
}
return nil
}
}
class PhotoCollectionContents {
let fetchResult: PHFetchResult<PHAsset>
let localizedTitle: String?
enum PhotoLibraryError: Error {
case assertionError(description: String)
case unsupportedMediaType
}
init(fetchResult: PHFetchResult<PHAsset>, localizedTitle: String?) {
self.fetchResult = fetchResult
self.localizedTitle = localizedTitle
}
var count: Int {
return fetchResult.count
}
private let imageManager = PHCachingImageManager()
func asset(at index: Int) -> PHAsset {
return fetchResult.object(at: index)
}
func assetItem(at index: Int, photoMediaSize: PhotoMediaSize) -> PhotoPickerAssetItem {
let mediaAsset = asset(at: index)
return PhotoPickerAssetItem(asset: mediaAsset, album: self, photoMediaSize: photoMediaSize)
}
// MARK: ImageManager
func requestThumbnail(for asset: PHAsset, thumbnailSize: CGSize, resultHandler: @escaping (UIImage?, [AnyHashable: Any]?) -> Void) {
_ = imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .aspectFill, options: nil, resultHandler: resultHandler)
}
private func requestImageDataSource(for asset: PHAsset) -> Promise<(dataSource: DataSource, dataUTI: String)> {
return Promise { resolver in
_ = imageManager.requestImageData(for: asset, options: nil) { imageData, dataUTI, _, _ in
guard let imageData = imageData else {
resolver.reject(PhotoLibraryError.assertionError(description: "imageData was unexpectedly nil"))
return
}
guard let dataUTI = dataUTI else {
resolver.reject(PhotoLibraryError.assertionError(description: "dataUTI was unexpectedly nil"))
return
}
guard let dataSource = DataSourceValue.dataSource(with: imageData, utiType: dataUTI) else {
resolver.reject(PhotoLibraryError.assertionError(description: "dataSource was unexpectedly nil"))
return
}
resolver.fulfill((dataSource: dataSource, dataUTI: dataUTI))
}
}
}
private func requestVideoDataSource(for asset: PHAsset) -> Promise<(dataSource: DataSource, dataUTI: String)> {
return Promise { resolver in
_ = imageManager.requestExportSession(forVideo: asset, options: nil, exportPreset: AVAssetExportPresetMediumQuality) { exportSession, _ in
guard let exportSession = exportSession else {
resolver.reject(PhotoLibraryError.assertionError(description: "exportSession was unexpectedly nil"))
return
}
exportSession.outputFileType = AVFileType.mp4
exportSession.metadataItemFilter = AVMetadataItemFilter.forSharing()
let exportPath = OWSFileSystem.temporaryFilePath(withFileExtension: "mp4")
let exportURL = URL(fileURLWithPath: exportPath)
exportSession.outputURL = exportURL
Logger.debug("starting video export")
exportSession.exportAsynchronously {
Logger.debug("Completed video export")
guard let dataSource = DataSourcePath.dataSource(with: exportURL, shouldDeleteOnDeallocation: true) else {
resolver.reject(PhotoLibraryError.assertionError(description: "Failed to build data source for exported video URL"))
return
}
resolver.fulfill((dataSource: dataSource, dataUTI: kUTTypeMPEG4 as String))
}
}
}
}
func outgoingAttachment(for asset: PHAsset) -> Promise<SignalAttachment> {
switch asset.mediaType {
case .image:
return requestImageDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in
return SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .medium)
}
case .video:
return requestVideoDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in
return SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI)
}
default:
return Promise(error: PhotoLibraryError.unsupportedMediaType)
}
}
}
class PhotoCollection {
private let collection: PHAssetCollection
init(collection: PHAssetCollection) {
self.collection = collection
}
func localizedTitle() -> String {
guard let localizedTitle = collection.localizedTitle?.stripped,
localizedTitle.count > 0 else {
return NSLocalizedString("PHOTO_PICKER_UNNAMED_COLLECTION", comment: "label for system photo collections which have no name.")
}
return localizedTitle
}
func contents() -> PhotoCollectionContents {
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
let fetchResult = PHAsset.fetchAssets(in: collection, options: options)
return PhotoCollectionContents(fetchResult: fetchResult, localizedTitle: localizedTitle())
}
}
class PhotoCollections {
let collections: [PhotoCollection]
init(collections: [PhotoCollection]) {
self.collections = collections
}
var count: Int {
return collections.count
}
func collection(at index: Int) -> PhotoCollection {
return collections[index]
}
}
class PhotoLibrary: NSObject, PHPhotoLibraryChangeObserver {
final class WeakDelegate {
weak var delegate: PhotoLibraryDelegate?
init(_ value: PhotoLibraryDelegate) {
delegate = value
}
}
var delegates = [WeakDelegate]()
public func add(delegate: PhotoLibraryDelegate) {
delegates.append(WeakDelegate(delegate))
}
var assetCollection: PHAssetCollection!
func photoLibraryDidChange(_ changeInstance: PHChange) {
DispatchQueue.main.async {
for weakDelegate in self.delegates {
weakDelegate.delegate?.photoLibraryDidChange(self)
}
}
}
override init() {
super.init()
PHPhotoLibrary.shared().register(self)
}
deinit {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
func defaultPhotoCollection() -> PhotoCollection {
guard let photoCollection = allPhotoCollections().collections.first else {
owsFail("Could not locate Camera Roll.")
}
return photoCollection
}
func allPhotoCollections() -> PhotoCollections {
var collections = [PhotoCollection]()
var collectionIds = Set<String>()
let processPHCollection: (PHCollection) -> Void = { (collection) in
// De-duplicate by id.
let collectionId = collection.localIdentifier
guard !collectionIds.contains(collectionId) else {
return
}
collectionIds.insert(collectionId)
guard let assetCollection = collection as? PHAssetCollection else {
owsFailDebug("Asset collection has unexpected type: \(type(of: collection))")
return
}
let photoCollection = PhotoCollection(collection: assetCollection)
// Hide empty collections.
guard photoCollection.contents().count > 0 else {
return
}
collections.append(photoCollection)
}
let processPHAssetCollections: (PHFetchResult<PHAssetCollection>) -> Void = { (fetchResult) in
for index in 0..<fetchResult.count {
processPHCollection(fetchResult.object(at: index))
}
}
let processPHCollections: (PHFetchResult<PHCollection>) -> Void = { (fetchResult) in
for index in 0..<fetchResult.count {
processPHCollection(fetchResult.object(at: index))
}
}
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "endDate", ascending: true)]
// Try to add "Camera Roll" first.
processPHAssetCollections(PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: fetchOptions))
// User-created albums.
processPHCollections(PHAssetCollection.fetchTopLevelUserCollections(with: fetchOptions))
// Smart albums.
processPHAssetCollections(PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: fetchOptions))
return PhotoCollections(collections: collections)
}
}

View File

@ -133,6 +133,8 @@ typedef UITableViewCell *_Nonnull (^OWSTableCustomCellBlock)(void);
@property (nonatomic) UITableViewStyle tableViewStyle;
- (instancetype)init NS_DESIGNATED_INITIALIZER;
#pragma mark - Presentation
- (void)presentFromViewController:(UIViewController *)fromViewController;