简体   繁体   中英

How to save image to custom album?

I have a brand new iOS app that generates images and lets the users save them into the Camera SavedPhotosAlbum. However, I wanna do something like Snapchat and Frontback, and save these images also to a custom-named album.

So this is my code right now:

let imageToSave = self.currentPreviewImage

let softwareContext = CIContext(options:[kCIContextUseSoftwareRenderer: true])
let cgimg = softwareContext.createCGImage(imageToSave, fromRect:imageToSave.extent())

ALAssetsLibrary().writeImageToSavedPhotosAlbum(cgimg, metadata:imageToSave.properties(), completionBlock:nil)

I've seen a few examples of people doing this in Objective-C but nothing that I could translate to Swift, and I've check the writeImageToSavedPhotosAlbum method signatures and none of them seem to allow saving to a custom album.

I came up with this singleton class to handle it:

import Photos

class CustomPhotoAlbum {

    static let albumName = "Flashpod"
    static let sharedInstance = CustomPhotoAlbum()

    var assetCollection: PHAssetCollection!

    init() {

        func fetchAssetCollectionForAlbum() -> PHAssetCollection! {

            let fetchOptions = PHFetchOptions()
            fetchOptions.predicate = NSPredicate(format: "title = %@", CustomPhotoAlbum.albumName)
            let collection = PHAssetCollection.fetchAssetCollectionsWithType(.Album, subtype: .Any, options: fetchOptions)

            if let firstObject: AnyObject = collection.firstObject {
                return collection.firstObject as! PHAssetCollection
            }

            return nil
        }

        if let assetCollection = fetchAssetCollectionForAlbum() {
            self.assetCollection = assetCollection
            return
        }

        PHPhotoLibrary.sharedPhotoLibrary().performChanges({
            PHAssetCollectionChangeRequest.creationRequestForAssetCollectionWithTitle(CustomPhotoAlbum.albumName)
        }) { success, _ in
            if success {
                self.assetCollection = fetchAssetCollectionForAlbum()
            }
        }
    }

    func saveImage(image: UIImage) {

        if assetCollection == nil {
            return   // If there was an error upstream, skip the save.
        }

        PHPhotoLibrary.sharedPhotoLibrary().performChanges({
            let assetChangeRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(image)
            let assetPlaceholder = assetChangeRequest.placeholderForCreatedAsset
            let albumChangeRequest = PHAssetCollectionChangeRequest(forAssetCollection: self.assetCollection)
            albumChangeRequest.addAssets([assetPlaceholder])
        }, completionHandler: nil)
    }


}

When you first instantiate the class, the custom album will be created if it doesn't already exist. You can save an image like this:

CustomPhotoAlbum.sharedInstance.saveImage(image)

NOTE: The CustomPhotoAlbum class assumes the app already has permission to access the Photo Library. Dealing with the permissions is a bit outside the scope of this question/answer. So make sure PHPhotoLibrary.authorizationStatus() == .Authorize before you use it. And request authorization if necessary.

Latest Swift 3.0 syntax. :)

import Foundation
import Photos


class CustomPhotoAlbum: NSObject {
    static let albumName = "Album Name"
    static let sharedInstance = CustomPhotoAlbum()

    var assetCollection: PHAssetCollection!

    override init() {
        super.init()

        if let assetCollection = fetchAssetCollectionForAlbum() {
            self.assetCollection = assetCollection
            return
        }

        if PHPhotoLibrary.authorizationStatus() != PHAuthorizationStatus.authorized {
            PHPhotoLibrary.requestAuthorization({ (status: PHAuthorizationStatus) -> Void in
                ()
            })
        }

        if PHPhotoLibrary.authorizationStatus() == PHAuthorizationStatus.authorized {
            self.createAlbum()
        } else {
            PHPhotoLibrary.requestAuthorization(requestAuthorizationHandler)
        }
    }

    func requestAuthorizationHandler(status: PHAuthorizationStatus) {
        if PHPhotoLibrary.authorizationStatus() == PHAuthorizationStatus.authorized {
            // ideally this ensures the creation of the photo album even if authorization wasn't prompted till after init was done
            print("trying again to create the album")
            self.createAlbum()
        } else {
            print("should really prompt the user to let them know it's failed")
        }
    }

    func createAlbum() {
        PHPhotoLibrary.shared().performChanges({
            PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: CustomPhotoAlbum.albumName)   // create an asset collection with the album name
        }) { success, error in
            if success {
                self.assetCollection = self.fetchAssetCollectionForAlbum()
            } else {
                print("error \(error)")
            }
        }
    }

    func fetchAssetCollectionForAlbum() -> PHAssetCollection? {
        let fetchOptions = PHFetchOptions()
        fetchOptions.predicate = NSPredicate(format: "title = %@", CustomPhotoAlbum.albumName)
        let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)

        if let _: AnyObject = collection.firstObject {
            return collection.firstObject
        }
        return nil
    }

    func save(image: UIImage) {
        if assetCollection == nil {
            return                          // if there was an error upstream, skip the save
        }

        PHPhotoLibrary.shared().performChanges({
            let assetChangeRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
            let assetPlaceHolder = assetChangeRequest.placeholderForCreatedAsset
            let albumChangeRequest = PHAssetCollectionChangeRequest(for: self.assetCollection)
            let enumeration: NSArray = [assetPlaceHolder!]
            albumChangeRequest!.addAssets(enumeration)

        }, completionHandler: nil)
    }
}

This is an updated version, works in Swift 2.1, and avoids the bug where the album is not created and images are not saved on the first launch (when authorization to write to the photo library is first requested/granted).

class CustomPhotoAlbum: NSObject {
    static let albumName = "Name of Custom Album"
    static let sharedInstance = CustomPhotoAlbum()

    var assetCollection: PHAssetCollection!

    override init() {
        super.init()

        if let assetCollection = fetchAssetCollectionForAlbum() {
            self.assetCollection = assetCollection
            return
        }

        if PHPhotoLibrary.authorizationStatus() != PHAuthorizationStatus.Authorized {
            PHPhotoLibrary.requestAuthorization({ (status: PHAuthorizationStatus) -> Void in
                status
            })
        }

        if PHPhotoLibrary.authorizationStatus() == PHAuthorizationStatus.Authorized {
            self.createAlbum()
        } else {
            PHPhotoLibrary.requestAuthorization(requestAuthorizationHandler)
        }
    }

    func requestAuthorizationHandler(status: PHAuthorizationStatus) {
        if PHPhotoLibrary.authorizationStatus() == PHAuthorizationStatus.Authorized {
            // ideally this ensures the creation of the photo album even if authorization wasn't prompted till after init was done
            print("trying again to create the album")
            self.createAlbum()
        } else {
            print("should really prompt the user to let them know it's failed")
        }
    }

    func createAlbum() {
        PHPhotoLibrary.sharedPhotoLibrary().performChanges({
        PHAssetCollectionChangeRequest.creationRequestForAssetCollectionWithTitle(CustomPhotoAlbum.albumName)   // create an asset collection with the album name
            }) { success, error in
                if success {
                    self.assetCollection = self.fetchAssetCollectionForAlbum()
                } else {
                    print("error \(error)")
                }
        }
    }

    func fetchAssetCollectionForAlbum() -> PHAssetCollection! {
        let fetchOptions = PHFetchOptions()
        fetchOptions.predicate = NSPredicate(format: "title = %@", CustomPhotoAlbum.albumName)
        let collection = PHAssetCollection.fetchAssetCollectionsWithType(.Album, subtype: .Any, options: fetchOptions)

        if let _: AnyObject = collection.firstObject {
            return collection.firstObject as! PHAssetCollection
        }        
        return nil
    }

    func saveImage(image: UIImage, metadata: NSDictionary) {
        if assetCollection == nil {
            return                          // if there was an error upstream, skip the save
        }

        PHPhotoLibrary.sharedPhotoLibrary().performChanges({                                                                    
            let assetChangeRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(image)                   
            let assetPlaceHolder = assetChangeRequest.placeholderForCreatedAsset             
            let albumChangeRequest = PHAssetCollectionChangeRequest(forAssetCollection: self.assetCollection)       
            albumChangeRequest!.addAssets([assetPlaceHolder!])                                                      
        }, completionHandler: nil)
    }
}

The previous answers were great and helped me a lot, but still there was a problem saving the image on the first call. The following solution is not perfectly clean but addresses the issue. Works with Swift 3/4.

import Photos

class CustomPhotoAlbum: NSObject {
    static let albumName = "Album name"
    static let shared = CustomPhotoAlbum()

    private var assetCollection: PHAssetCollection!

    private override init() {
        super.init()

        if let assetCollection = fetchAssetCollectionForAlbum() {
            self.assetCollection = assetCollection
            return
        }
    }

    private func checkAuthorizationWithHandler(completion: @escaping ((_ success: Bool) -> Void)) {
        if PHPhotoLibrary.authorizationStatus() == .notDetermined {
            PHPhotoLibrary.requestAuthorization({ (status) in
                self.checkAuthorizationWithHandler(completion: completion)
            })
        }
        else if PHPhotoLibrary.authorizationStatus() == .authorized {
            self.createAlbumIfNeeded()
            completion(true)
        }
        else {
            completion(false)
        }
    }

    private func createAlbumIfNeeded() {
        if let assetCollection = fetchAssetCollectionForAlbum() {
            // Album already exists
            self.assetCollection = assetCollection
        } else {
            PHPhotoLibrary.shared().performChanges({
                PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: CustomPhotoAlbum.albumName)   // create an asset collection with the album name
            }) { success, error in
                if success {
                    self.assetCollection = self.fetchAssetCollectionForAlbum()
                } else {
                    // Unable to create album
                }
            }
        }
    }

    private func fetchAssetCollectionForAlbum() -> PHAssetCollection? {
        let fetchOptions = PHFetchOptions()
        fetchOptions.predicate = NSPredicate(format: "title = %@", CustomPhotoAlbum.albumName)
        let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)

        if let _: AnyObject = collection.firstObject {
            return collection.firstObject
        }
        return nil
    }

    func save(image: UIImage) {
        self.checkAuthorizationWithHandler { (success) in
            if success, self.assetCollection != nil {
                PHPhotoLibrary.shared().performChanges({
                    let assetChangeRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
                    let assetPlaceHolder = assetChangeRequest.placeholderForCreatedAsset
                    let albumChangeRequest = PHAssetCollectionChangeRequest(for: self.assetCollection)
                    let enumeration: NSArray = [assetPlaceHolder!]
                    albumChangeRequest!.addAssets(enumeration)

                }, completionHandler: nil)
            }
        }
    }
}

I found that some proposed solutions here were working but I wanted to rewrite a reusable version of it. Here is how you use it:

let image = // this is your image object

// Use the shared instance that has the default album name
CustomPhotoAlbum.shared.save(image)

// Use a custom album name
let album = CustomPhotoAlbum("some title")
album.save(image)

When saving an image, it request the user's photo access (which returns immediately if previously authorized) and tries to create an album if one doesn't exist yet. Below is the full source code written in Swift 3 and compatible with Objective-C.

//
//  CustomPhotoAlbum.swift
//
//  Copyright © 2017 Et Voilapp. All rights reserved.
//

import Foundation
import Photos

@objc class CustomPhotoAlbum: NSObject {

  /// Default album title.
  static let defaultTitle = "Your title"

  /// Singleton
  static let shared = CustomPhotoAlbum(CustomPhotoAlbum.defaultTitle)

  /// The album title to use.
  private(set) var albumTitle: String

  /// This album's asset collection
  internal var assetCollection: PHAssetCollection?

  /// Initialize a new instance of this class.
  ///
  /// - Parameter title: Album title to use.
  init(_ title: String) {
    self.albumTitle = title
    super.init()
  }

  /// Save the image to this app's album.
  ///
  /// - Parameter image: Image to save.
  public func save(_ image: UIImage?) {
    guard let image = image else { return }

    // Request authorization and create the album
    requestAuthorizationIfNeeded { (_) in

      // If it all went well, we've got our asset collection
      guard let assetCollection = self.assetCollection else { return }

      PHPhotoLibrary.shared().performChanges({

        // Make sure that there's no issue while creating the request
        let request = PHAssetChangeRequest.creationRequestForAsset(from: image)
        guard let placeholder = request.placeholderForCreatedAsset,
          let albumChangeRequest = PHAssetCollectionChangeRequest(for: assetCollection) else {
            return
        }

        let enumeration: NSArray = [placeholder]
        albumChangeRequest.addAssets(enumeration)

      }, completionHandler: nil)
    }
  }
}

internal extension CustomPhotoAlbum {

  /// Request authorization and create the album if that went well.
  ///
  /// - Parameter completion: Called upon completion.
  func requestAuthorizationIfNeeded(_ completion: @escaping ((_ success: Bool) -> Void)) {

    PHPhotoLibrary.requestAuthorization { status in
      guard status == .authorized else {
        completion(false)
        return
      }

      // Try to find an existing collection first so that we don't create duplicates
      if let collection = self.fetchAssetCollectionForAlbum() {
        self.assetCollection = collection
        completion(true)

      } else {
        self.createAlbum(completion)
      }
    }
  }


  /// Creates an asset collection with the album name.
  ///
  /// - Parameter completion: Called upon completion.
  func createAlbum(_ completion: @escaping ((_ success: Bool) -> Void)) {

    PHPhotoLibrary.shared().performChanges({

      PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: self.albumTitle)

    }) { (success, error) in
      defer {
        completion(success)
      }

      guard error == nil else {
        print("error \(error!)")
        return
      }

      self.assetCollection = self.fetchAssetCollectionForAlbum()
    }
  }


  /// Fetch the asset collection matching this app's album.
  ///
  /// - Returns: An asset collection if found.
  func fetchAssetCollectionForAlbum() -> PHAssetCollection? {

    let fetchOptions = PHFetchOptions()
    fetchOptions.predicate = NSPredicate(format: "title = %@", albumTitle)

    let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
    return collection.firstObject
  }
}

Improved upon @Damien answer . Works with UIImage and video (with url) too. Swift4 tested:

import Photos

class MyAwesomeAlbum: NSObject {
  static let albumName = "My Awesome Album"
  static let shared = MyAwesomeAlbum()

  private var assetCollection: PHAssetCollection!

  private override init() {
    super.init()

    if let assetCollection = fetchAssetCollectionForAlbum() {
      self.assetCollection = assetCollection
      return
    }
  }

  private func checkAuthorizationWithHandler(completion: @escaping ((_ success: Bool) -> Void)) {
    if PHPhotoLibrary.authorizationStatus() == .notDetermined {
      PHPhotoLibrary.requestAuthorization({ (status) in
        self.checkAuthorizationWithHandler(completion: completion)
      })
    }
    else if PHPhotoLibrary.authorizationStatus() == .authorized {
      self.createAlbumIfNeeded { (success) in
        if success {
          completion(true)
        } else {
          completion(false)
        }

      }

    }
    else {
      completion(false)
    }
  }

  private func createAlbumIfNeeded(completion: @escaping ((_ success: Bool) -> Void)) {
    if let assetCollection = fetchAssetCollectionForAlbum() {
      // Album already exists
      self.assetCollection = assetCollection
      completion(true)
    } else {
      PHPhotoLibrary.shared().performChanges({
        PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: MyAwesomeAlbum.albumName)   // create an asset collection with the album name
      }) { success, error in
        if success {
          self.assetCollection = self.fetchAssetCollectionForAlbum()
          completion(true)
        } else {
          // Unable to create album
          completion(false)
        }
      }
    }
  }

  private func fetchAssetCollectionForAlbum() -> PHAssetCollection? {
    let fetchOptions = PHFetchOptions()
    fetchOptions.predicate = NSPredicate(format: "title = %@", MyAwesomeAlbum.albumName)
    let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)

    if let _: AnyObject = collection.firstObject {
      return collection.firstObject
    }
    return nil
  }

  func save(image: UIImage) {
    self.checkAuthorizationWithHandler { (success) in
      if success, self.assetCollection != nil {
        PHPhotoLibrary.shared().performChanges({
          let assetChangeRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
          let assetPlaceHolder = assetChangeRequest.placeholderForCreatedAsset
          if let albumChangeRequest = PHAssetCollectionChangeRequest(for: self.assetCollection) {
            let enumeration: NSArray = [assetPlaceHolder!]
            albumChangeRequest.addAssets(enumeration)
          }

        }, completionHandler: { (success, error) in
          if success {
            print("Successfully saved image to Camera Roll.")
          } else {
            print("Error writing to image library: \(error!.localizedDescription)")
          }
        })

      }
    }
  }

  func saveMovieToLibrary(movieURL: URL) {

    self.checkAuthorizationWithHandler { (success) in
      if success, self.assetCollection != nil {

        PHPhotoLibrary.shared().performChanges({

          if let assetChangeRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: movieURL) {
            let assetPlaceHolder = assetChangeRequest.placeholderForCreatedAsset
            if let albumChangeRequest = PHAssetCollectionChangeRequest(for: self.assetCollection) {
              let enumeration: NSArray = [assetPlaceHolder!]
              albumChangeRequest.addAssets(enumeration)
            }

          }

        }, completionHandler:  { (success, error) in
          if success {
            print("Successfully saved video to Camera Roll.")
          } else {
            print("Error writing to movie library: \(error!.localizedDescription)")
          }
        })


      }
    }

  }
}

Usage:

MyAwesomeAlbum.shared.save(image: image)

or

MyAwesomeAlbum.shared.saveMovieToLibrary(movieURL: url)

100% working and polished solution written in Swift 5. Completion blocks and error handled correctly. I switched to normal class because I need it only in a specific point of my app, but if you use it mostly you can convert to Singleton.

class PhotoManager {

    private var albumName: String
    private var album: PHAssetCollection?

    init(albumName: String) {
        self.albumName = albumName

        if let album = getAlbum() {
            self.album = album
            return
        }
    }

    private func getAlbum() -> PHAssetCollection? {
        let options = PHFetchOptions()
        options.predicate = NSPredicate(format: "title = %@", albumName)
        let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: options)
        return collection.firstObject ?? nil
    }

    private func createAlbum(completion: @escaping (Bool) -> ()) {
        PHPhotoLibrary.shared().performChanges({
            PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: self.albumName)
        }, completionHandler: { (result, error) in
            if let error = error {
                print("error: \(error.localizedDescription)")
            } else {
                self.album = self.getAlbum()
                completion(result)
            }
        })
    }

    private func add(image: UIImage, completion: @escaping (Bool, Error?) -> ()) {
        PHPhotoLibrary.shared().performChanges({
            let assetChangeRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
            if let album = self.album, let placeholder = assetChangeRequest.placeholderForCreatedAsset {
                let albumChangeRequest = PHAssetCollectionChangeRequest(for: album)
                let enumeration = NSArray(object: placeholder)
                albumChangeRequest?.addAssets(enumeration)
            }
        }, completionHandler: { (result, error) in
            completion(result, error)
        })
    }

    func save(_ image: UIImage, completion: @escaping (Bool, Error?) -> ()) {
        PHPhotoLibrary.requestAuthorization { status in
            guard status == .authorized else {
                // fail and redirect to app settings
                return
            }

            if let _ = self.album {
                self.add(image: image) { (result, error) in
                    completion(result, error)
                }
                return
            }

            self.createAlbum(completion: { _ in
                self.add(image: image) { (result, error) in
                    completion(result, error)
                }
            })
        }
    }
}

For those of you looking for a one-function solution using Swift 4, I have condensed some of the above code into a function that simply takes in a UIImage, String-type album name, and a callback indicating success/failure.

Note: this function is more complex so it will obviously have a slower runtime than the previous solutions, but I have posted it here for other peoples' convenience.

func save(image:UIImage, toAlbum:String, withCallback:((Bool)->Void)? = nil) {

    func fetchAssetCollection(forAlbum:String) -> PHAssetCollection! {

        let fetchOptions = PHFetchOptions()
        fetchOptions.predicate = NSPredicate(format: "title = %@", forAlbum)
        let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)

        if let _: AnyObject = collection.firstObject {
            return collection.firstObject
        }

        return nil
    }

    PHPhotoLibrary.shared().performChanges({
        PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: toAlbum)   // create an asset collection with the album name
    }) { success, error in
        if success {
            if success, let assetCollection = fetchAssetCollection(forAlbum: toAlbum) {

                PHPhotoLibrary.shared().performChanges({

                    let assetChangeRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
                    let assetPlaceholder = assetChangeRequest.placeholderForCreatedAsset
                    let albumChangeRequest = PHAssetCollectionChangeRequest(for: assetCollection)
                    let assetEnumeration:NSArray = [assetPlaceholder!]
                    albumChangeRequest!.addAssets(assetEnumeration)

                }, completionHandler: { (_ didComplete:Bool, _ error:Error?) -> Void in
                    if withCallback != nil {
                        withCallback!(didComplete && error == nil)
                    }
                })

            } else {
                if withCallback != nil {
                    // Failure to save
                    withCallback!(false)
                }
            }
        } else {
            if withCallback != nil {
                // Failure to save
                withCallback!(false)
            }
        }
    }

}

Swift 5 update, with added completion handler.

Usage:

CustomPhotoAlbum.sharedInstance.save(image, completion: { result, error in
    if let e = error {
        // handle error
        return
    }
    // save successful, do something (such as inform user)
})

Singleton class:

import Foundation
import Photos
import UIKit

class CustomPhotoAlbum: NSObject {
    static let albumName = "Album Name"
    static let sharedInstance = CustomPhotoAlbum()

    var assetCollection: PHAssetCollection!

    override init() {
        super.init()

        if let assetCollection = fetchAssetCollectionForAlbum() {
            self.assetCollection = assetCollection
            return
        }

        if PHPhotoLibrary.authorizationStatus() != PHAuthorizationStatus.authorized {
            PHPhotoLibrary.requestAuthorization({ (status: PHAuthorizationStatus) -> Void in
                ()
            })
        }

        if PHPhotoLibrary.authorizationStatus() == PHAuthorizationStatus.authorized {
            self.createAlbum()
        } else {
            PHPhotoLibrary.requestAuthorization(requestAuthorizationHandler)
        }
    }

    func requestAuthorizationHandler(status: PHAuthorizationStatus) {
        if PHPhotoLibrary.authorizationStatus() == PHAuthorizationStatus.authorized {
            // ideally this ensures the creation of the photo album even if authorization wasn't prompted till after init was done
            print("trying again to create the album")
            self.createAlbum()
        } else {
            print("should really prompt the user to let them know it's failed")
        }
    }

    func createAlbum() {
        PHPhotoLibrary.shared().performChanges({
            PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: CustomPhotoAlbum.albumName)   // create an asset collection with the album name
        }) { success, error in
            if success {
                self.assetCollection = self.fetchAssetCollectionForAlbum()
            } else {
                print("error \(String(describing: error))")
            }
        }
    }

    func fetchAssetCollectionForAlbum() -> PHAssetCollection? {
        let fetchOptions = PHFetchOptions()
        fetchOptions.predicate = NSPredicate(format: "title = %@", CustomPhotoAlbum.albumName)
        let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)

        if let _: AnyObject = collection.firstObject {
            return collection.firstObject
        }
        return nil
    }

    func save(_ image: UIImage, completion: @escaping ((Bool, Error?) -> ())) {
        if assetCollection == nil {
            // if there was an error upstream, skip the save
            return
        }

        PHPhotoLibrary.shared().performChanges({
            let assetChangeRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
            let assetPlaceHolder = assetChangeRequest.placeholderForCreatedAsset
            let albumChangeRequest = PHAssetCollectionChangeRequest(for: self.assetCollection)
            let enumeration: NSArray = [assetPlaceHolder!]
            albumChangeRequest!.addAssets(enumeration)

        }, completionHandler: { result, error in
            completion(result, error)
        })
    }
}

If you're interested in a protocol oriented approach that allows for simple saving to multiple albums with different names that's up to date with Swift 4 and avoids singleton use then read on.

This approach checks for and obtains user authorization, checks for or creates the photo album then saves the image to the requested album. If at any point an error is triggered the completion block is run with the corresponding error.

An upside of this approach is that the user is not prompted for photos access as soon as the instance is created, instead they are prompted when they are actually trying to save their image, if authorization is required.

This method also allows you to define a very simple class that encapsulates a photo album, conforming to the PhotoAlbumHandler protocol and thus getting all the photo album interaction logic for free, like this:

class PhotoAlbum: PhotoAlbumHandler {

    var albumName: String

    init(named: String) {
        albumName = named
    }
}

You can also then create an enum that manages and encapsulates all your photo albums. Adding support for another album is as simple as adding a new case to the enum and defining the corresponding albumName.

Like this:

public enum PhotoAlbums {
    case landscapes
    case portraits

    var albumName: String {
        switch self {
        case .landscapes: return "Landscapes"
        case .portraits: return "Portraits"
        }
    }

    func album() -> PhotoAlbumHandler {
        return PhotoAlbum.init(named: albumName)
    } 
}

Using this approach makes managing your photo albums a breeze, in your viewModel (or view controller if you're not using view models) you can create references to your albums like this:

let landscapeAlbum = PhotoAlbums.landscapes.album()
let portraitAlbum = PhotoAlbums.portraits.album()

Then to save an image to one of the albums you could do something like this:

let photo: UIImage = UIImage.init(named: "somePhotoName")

landscapeAlbum.save(photo) { (error) in
    DispatchQueue.main.async {
        if let error = error {
            // show alert with error message or...???
            self.label.text = error.message
            return
        }

        self.label.text = "Saved image to album"
    }
}

For error handling I opted to encapsulate any possible errors in an error enum:

public enum PhotoAlbumHandlerError {
    case unauthorized
    case authCancelled
    case albumNotExists
    case saveFailed
    case unknown

    var title: String {
        return "Photo Save Error"
    }

    var message: String {
        switch self {
        case .unauthorized:
            return "Not authorized to access photos. Enable photo access in the 'Settings' app to continue."
        case .authCancelled:
            return "The authorization process was cancelled. You will not be able to save to your photo albums without authorizing access."
        case .albumNotExists:
            return "Unable to create or find the specified album."
        case .saveFailed:
            return "Failed to save specified image."
        case .unknown:
            return "An unknown error occured."
        }
    }
}

The protocol that defines the interface and the protocol extension that handles interaction with the system Photo Album functionality is here:

import Photos

public protocol PhotoAlbumHandler: class {
    var albumName: String { get set }

    func save(_ photo: UIImage, completion: @escaping (PhotoAlbumHandlerError?) -> Void)
}

extension PhotoAlbumHandler {

    func save(_ photo: UIImage, completion: @escaping (PhotoAlbumHandlerError?) -> Void) {

        // Check for permission
        guard PHPhotoLibrary.authorizationStatus() == .authorized else {

            // not authorized, prompt for access
            PHPhotoLibrary.requestAuthorization({ [weak self] status in

                // not authorized, end with error
                guard let strongself = self, status == .authorized else {
                    completion(.authCancelled)
                    return
                }

                // received authorization, try to save photo to album
                strongself.save(photo, completion: completion)
            })
            return
        }

        // check for album, create if not exists
        guard let album = fetchAlbum(named: albumName) else {

            // album does not exist, create album now
            createAlbum(named: albumName, completion: { [weak self] success, error in

                // album not created, end with error
                guard let strongself = self, success == true, error == nil else {
                    completion(.albumNotExists)
                    return
                }

                // album created, run through again
                strongself.save(photo, completion: completion)
            })
            return
        }

        // save the photo now... we have permission and the desired album
        insert(photo: photo, in: album, completion: { success, error in

            guard success == true, error == nil else {
                completion(.saveFailed)
                return
            }

            // finish with no error
            completion(nil)
        })
    }

    internal func fetchAlbum(named: String) -> PHAssetCollection? {
        let options = PHFetchOptions()
        options.predicate = NSPredicate(format: "title = %@", named)
        let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: options)

        guard let album = collection.firstObject else {
            return nil
        }

        return album
    }

    internal func createAlbum(named: String, completion: @escaping (Bool, Error?) -> Void) {
        PHPhotoLibrary.shared().performChanges({
            PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: named)
        }, completionHandler: completion)
    }

    internal func insert(photo: UIImage, in collection: PHAssetCollection, completion: @escaping (Bool, Error?) -> Void) {
        PHPhotoLibrary.shared().performChanges({
            let request = PHAssetChangeRequest.creationRequestForAsset(from: photo)
            request.creationDate = NSDate.init() as Date

            guard let assetPlaceHolder = request.placeholderForCreatedAsset,
                  let albumChangeRequest = PHAssetCollectionChangeRequest(for: collection) else {
                    return
            }
            let enumeration: NSArray = [assetPlaceHolder]
            albumChangeRequest.addAssets(enumeration)

        }, completionHandler: completion)
    }
}

If you'd like to look through a sample Xcode project you can find one here: https://github.com/appteur/ios_photo_album_sample

Thanks, was trying to use this code, but found some logic errors. Here is the cleaned up code

import Photos

class CustomPhotoAlbum: NSObject {
    static let albumName = Bundle.main.infoDictionary![kCFBundleNameKey as String] as! String
    static let shared = CustomPhotoAlbum()

    private lazy var assetCollection = fetchAssetCollectionForAlbum()

    private override init() {
        super.init()
    }

    private func checkAuthorizationWithHandler(completion: @escaping ((_ success: Bool) -> Void)) {
        switch PHPhotoLibrary.authorizationStatus() {
        case .authorized:
            completion(true)
        case .notDetermined:
            PHPhotoLibrary.requestAuthorization(){ (status) in
                self.checkAuthorizationWithHandler(completion: completion)
            }
        case .denied, .restricted:
            completion(false)
        }
    }

    private func fetchAssetCollectionForAlbum() -> PHAssetCollection? {
        let fetchOptions = PHFetchOptions()
        fetchOptions.predicate = NSPredicate(format: "title = %@", CustomPhotoAlbum.albumName)
        let fetch = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
        return fetch.firstObject
    }

    func save(image: UIImage) {
        func saveIt(_ validAssets: PHAssetCollection){
            PHPhotoLibrary.shared().performChanges({
                let assetChangeRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
                let assetPlaceHolder = assetChangeRequest.placeholderForCreatedAsset
                let albumChangeRequest = PHAssetCollectionChangeRequest(for: validAssets)
                let enumeration: NSArray = [assetPlaceHolder!]
                albumChangeRequest!.addAssets(enumeration)

            }, completionHandler: nil)
        }
        self.checkAuthorizationWithHandler { (success) in
            if success {
                if let validAssets = self.assetCollection { // Album already exists
                    saveIt(validAssets)
                } else {                                    // create an asset collection with the album name
                    PHPhotoLibrary.shared().performChanges({
                        PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: CustomPhotoAlbum.albumName)
                    }) { success, error in
                        if success, let validAssets = self.fetchAssetCollectionForAlbum() {
                            self.assetCollection = validAssets
                            saveIt(validAssets)
                        } else {
                            // TODO: send user message "Sorry, unable to create album and save image..."
                        }
                    }
                }
            }
        }
    }
}

Even after the fixes, my PhotoAlbum still didn't work for the first image and if I wanted to save more than one images at once I've ended up with multiple empty albums. So I've upgraded the class and I only save the emoji after the album has been created.

New version:

class CustomPhotoAlbum: NSObject {
static let albumName = "AlbumName"
static let shared = CustomPhotoAlbum()

private var assetCollection: PHAssetCollection!

private override init() {
    super.init()

    if let assetCollection = fetchAssetCollectionForAlbum() {
        self.assetCollection = assetCollection
        return
    }
}

private func checkAuthorizationWithHandler(completion: @escaping ((_ success: Bool) -> Void)) {
    if PHPhotoLibrary.authorizationStatus() == .notDetermined {
        PHPhotoLibrary.requestAuthorization({ (status) in
            self.checkAuthorizationWithHandler(completion: completion)
        })
    }
    else if PHPhotoLibrary.authorizationStatus() == .authorized {
        self.createAlbumIfNeeded()
        completion(true)
    }
    else {
        completion(false)
    }
}

private func createAlbumIfNeeded() {
   /* if let assetCollection = fetchAssetCollectionForAlbum() {
        // Album already exists
        self.assetCollection = assetCollection
    } else {
        PHPhotoLibrary.shared().performChanges({
            PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: CustomPhotoAlbum.albumName)   // create an asset collection with the album name
        }) { success, error in
            if success {
                self.assetCollection = self.fetchAssetCollectionForAlbum()
            } else {
                // Unable to create album
            }
        }
    }*/
}

private func fetchAssetCollectionForAlbum() -> PHAssetCollection? {
    let fetchOptions = PHFetchOptions()
    fetchOptions.predicate = NSPredicate(format: "title = %@", CustomPhotoAlbum.albumName)
    let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)

    if let _: AnyObject = collection.firstObject {
        return collection.firstObject
    }
    return nil
}

func save(image: UIImage) {
    self.checkAuthorizationWithHandler { (success) in
        if success {
            if let assetCollection = self.fetchAssetCollectionForAlbum() {
                // Album already exists
                self.assetCollection = assetCollection
                PHPhotoLibrary.shared().performChanges({
                    let assetChangeRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
                    let assetPlaceHolder = assetChangeRequest.placeholderForCreatedAsset
                    let albumChangeRequest = PHAssetCollectionChangeRequest(for: self.assetCollection)
                    let enumeration: NSArray = [assetPlaceHolder!]
                    albumChangeRequest!.addAssets(enumeration)

                }, completionHandler: nil)
            } else {
                PHPhotoLibrary.shared().performChanges({
                    PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: CustomPhotoAlbum.albumName)   // create an asset collection with the album name
                }) { success, error in
                    if success {
                        self.assetCollection = self.fetchAssetCollectionForAlbum()
                        PHPhotoLibrary.shared().performChanges({
                            let assetChangeRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
                            let assetPlaceHolder = assetChangeRequest.placeholderForCreatedAsset
                            let albumChangeRequest = PHAssetCollectionChangeRequest(for: self.assetCollection)
                            let enumeration: NSArray = [assetPlaceHolder!]
                            albumChangeRequest!.addAssets(enumeration)

                        }, completionHandler: nil)
                    } else {
                        // Unable to create album
                    }
                }
            }
        }
    }
}

}

If you want to save multiple images at once, here is my code for that. The key here is to delay the saving of the other images which are not the first, because we have to create the album first. (otherwise we end up with duplicate albums, because all the saving processes will try to create a Custom Album). This is the code from my app, so you can understand the logic:

var overFirstSave = false
                for stickerName in filenames {
                    let url = self.getDocumentsDirectory().appendingPathComponent(stickerName as! String)
                    do{
                        if !overFirstSave{
                            CustomPhotoAlbum.shared.save(image: UIImage(contentsOfFile: url.path)!)
                            overFirstSave = true
                        }else{
                            DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3), execute: {
                                CustomPhotoAlbum.shared.save(image: UIImage(contentsOfFile: url.path)!)
                            })
                        }
                    }catch {
                        print(error)
                    }
                }

Swift 4 and 5

On Button click Call just like this

@IBAction func btnSave(_ sender: Any) {
    createAlbum()
    self.saveGIFInAlbum(url: gifUrl)
}

func createAlbum() {

    if let assetCollection = fetchAssetCollectionForAlbum() {
        self.assetCollection = assetCollection
        return
    }

    if PHPhotoLibrary.authorizationStatus() != PHAuthorizationStatus.authorized {
        PHPhotoLibrary.requestAuthorization({ (status: PHAuthorizationStatus) -> Void in
            ()
        })
    }

    if PHPhotoLibrary.authorizationStatus() == PHAuthorizationStatus.authorized {
        self.createAlbumIfNeeded()
    } else {
        PHPhotoLibrary.requestAuthorization(requestAuthorizationHandler)
    }
}

func requestAuthorizationHandler(status: PHAuthorizationStatus) {
    if PHPhotoLibrary.authorizationStatus() == PHAuthorizationStatus.authorized {
        // ideally this ensures the creation of the photo album even if authorization wasn't prompted till after init was done
        print("trying again to create the album")
        self.createAlbumIfNeeded()
    } else {
        print("should really prompt the user to let them know it's failed")
    }
}

func createAlbumIfNeeded() {
    PHPhotoLibrary.shared().performChanges({
        PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: "PicBloom")   // create an asset collection with the album name
    }) { success, error in
        if success {
            self.assetCollection = self.fetchAssetCollectionForAlbum()
        } else {
            print("error \(String(describing: error))")
        }
    }
}

func fetchAssetCollectionForAlbum() -> PHAssetCollection? {
    let fetchOptions = PHFetchOptions()
    fetchOptions.predicate = NSPredicate(format: "title = %@", "PicBloom")
    let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)

    if let _: AnyObject = collection.firstObject {
        return collection.firstObject
    }
    return nil
}

func saveGIFInAlbum(url: URL){

    PHPhotoLibrary.shared().performChanges({
        let assetChangeRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: url)
        let assetPlaceHolder = assetChangeRequest?.placeholderForCreatedAsset

        if(self.assetCollection != nil) {
            let albumChangeRequest = PHAssetCollectionChangeRequest(for: self.assetCollection)
            let enumeration: NSArray = [assetPlaceHolder!]
            albumChangeRequest!.addAssets(enumeration)
        }
    }, completionHandler: { success, error in
        DispatchQueue.main.async {
            if success {
                let message = "Gif has been saved to your gallery!"
                let alert = UIAlertController(title: "", message: message, preferredStyle: .alert)
                self.present(alert, animated: true)
                let duration: Double = 1.5
                DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + duration) {
                    alert.dismiss(animated: true)
                }
            }else if error != nil {
                print("handle error since couldn't save GIF")
            }
        }
    })
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM