How to save image to custom album?

2019-03-08 01:04发布

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.

标签: ios swift ios8
10条回答
在下西门庆
2楼-- · 2019-03-08 01:13

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..."
                        }
                    }
                }
            }
        }
    }
}
查看更多
等我变得足够好
3楼-- · 2019-03-08 01:13

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)
                    }
                }
查看更多
▲ chillily
4楼-- · 2019-03-08 01:14

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)
            }
        }
    }

}
查看更多
beautiful°
5楼-- · 2019-03-08 01:18

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

查看更多
Juvenile、少年°
6楼-- · 2019-03-08 01:19

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
  }
}
查看更多
再贱就再见
7楼-- · 2019-03-08 01:20

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)
查看更多
登录 后发表回答