iOS Photo Album Creator
Nov 1, 2022
5 min read
views
While a React Native community library for managing your phone’s camera roll already exists, in this article, we will build a native module that helps us create named albums in the Photos app. This is a feature not offered by the community library at the time of writing. The module we will build (if it has the permissions in your Info.plist) will work on your iPhone Simulator as well as on your physical device. So let’s get started.
The Native Module
The Bridge
We need to register our module with the React Native bridge. Create a file called RNAlbumCreator.m (the required Objective-C file) at the root of your ios directory.
// RNAlbumCreator.m
#import <Foundation/Foundation.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(RNAlbumCreator, NSNetService)
// This is the only method from our Swift class that we will make available to our JavaScript.
RCT_EXTERN_METHOD(
createAlbum: (NSString *)albumName
resolve: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject
)
@end
The Swift Part
We will write the native part of this in Swift. Why? Well, Objective-C is becoming less common and we can actually write the Swift code so that it’s interoperable with Objective-C (the more conventional language for writing native modules for iOS).
- Create a new Swift file called
RNAlbumCreator.swift
at the root of yourios
directory, at the same level as the Objective C file you just created. - Let’s first import the Swift modules that we need. This is most notably React and AssetsLibrary.
import UIKit
import Photos
import AssetsLibrary
import React # We will use this (in part) so we can make the createAlbum method return a Promise.
Ok, so we now need to create an album creator class that is interoperable with Objective-C and is able to talk to our JavaScript.
@objc(RNAlbumCreator)
class RNAlbumCreator: RCTEventEmitter {
...
}
And now we will add the boilerplate that is required for a native module in iOS. Insert the following into the body of our album-creating class.
...
// Run this module on a separate thread than the main one.
@objc static public override func requiresMainQueueSetup() -> Bool {
return false
}
/*
The supportedEvents method would be more relevant to us if we were continually emitting events to our JavaScript.
For example, if we had a native timer that we wanted to go off every 10 and 15 seconds,
we could emit named events "tenSecondEvent" and "fifteenSecondEvent" and put those strings into the supportedEvents method.
*/
override func supportedEvents() -> [String]! {
return []
}
...
Now we will implement the createAlbum method and its supporting method for checking whether the album already exists.
// Insert outside of the class
enum CreateNewAlbumError: Error {
case failureToCreateError
}
...
...
@objc(RNAlbumCreator)
class RNAlbumCreator: RCTEventEmitter {
...
// Insert inside the class
@objc public func createAlbum(_ albumName: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
if let _ = fetchAssetCollection(albumName) {
// Resolve the promise for creating the album. You could also reject in this case, if you prefer.
resolve("Album already exists.")
return
} else {
// Album does not exist, create it and attempt to save the image
PHPhotoLibrary.shared().performChanges({
PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: albumName)
// Completion handler is a Swift closure that begins with `in`. Reject the promise with our custom error.
}, completionHandler: { (success: Bool, error: Error?) in
guard success == true && error == nil else {
NSLog("Could not create the album")
if let err = error {
NSLog("Error: \(err)")
reject("Error", "Could not create the album \(albumName)", CreateNewAlbumError.failureToCreateError)
}
return
}
if let _ = self.fetchAssetCollection(albumName) {
// Resolve the create album promise
resolve("Created new album with name \(albumName)")
}
})
}
}
// Check if the album already exists
func fetchAssetCollection(_ name: String) -> PHAssetCollection? {
let fetchOption = PHFetchOptions()
fetchOption.predicate = NSPredicate(format: "title == '" + name + "'")
let fetchResult = PHAssetCollection.fetchAssetCollections(
with: PHAssetCollectionType.album,
subtype: PHAssetCollectionSubtype.albumRegular,
options: fetchOption)
return fetchResult.firstObject
}
}
...
Configuration
As I hinted at the beginning of this article, you may need to modify your Info.plist so that your app can manage photo albums. Adding the below permissions should be sufficient.
<key>NSPhotoLibraryAddUsageDescription</key>
<string>INSERT YOUR DESCRIPTION HERE</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>INSERT YOUR DESCRIPTION HERE</string>
Usage
Now we can use our native module in our JavaScript or TypeScript. We will write a custom hook for creating photo albums that we can just drop in to our React components.
The Custom Hook
// useAlbumCreator.ts
import { NativeModules, Platform } from "react-native"
const { RNAlbumCreator } = NativeModules
interface ICreateAlbum {
albumName: string
}
const useAlbumCreator = () => {
const createAlbum = async ({ albumName }: ICreateAlbum): Promise<void> => {
try {
// Ignore this on non-iOS platforms
if (Platform.OS !== "ios") return
// Our native createAlbum method returns a Promise
await RNAlbumCreator.createAlbum(albumName)
} catch (err) {
console.error(err) // we might see our custom error enum here if something goes wrong
}
}
return {
createAlbum,
}
}
export default useAlbumCreator
Creating an Album from a Component
At this point, using our native module is very simple. Call the hook at the top level of your functional component and call the createAlbum method.
// MyComponent.tsx
...
const MyComponent = () => {
const { createAlbum } = useAlbumCreator()
return (
<Pressable onPress={() => createAlbum({
albumName: 'MyAlbum' // name it whatever you like!
})}>
<Text>Create MyAlbum</Text>
</Pressable>
)
}
...
Wrapping Up
In this post, we built a native module for iOS that allows your app to save photos to a custom album. If you found this article enjoyable or useful, please let me know. Or if you notice any errors or something that could be improved, feel free to open an issue on my GitHub or send me an email.