Balancing Innovation and Scalability: Developing Vestiaire Collective’s Photo Library Feature on iOS

Ali Fakih
Vestiaire Connected
14 min readApr 19, 2023
Apple Photos and Vestiaire Collective

As an experienced iOS engineer who has navigated the fast-paced, iterative world of agile development, you know that creating a feature with intricate technicalities can be a daunting task. Whether it’s utilizing native libraries or third-party tools, the learning curve can be steep. I want to take you on a journey through the development of the Photo Library feature at Vestiaire Collective and share the strategies that I found to be effective. Remember, there’s no one-size-fits-all solution, but hopefully, my experience will provide valuable insights and inspiration for your next project. Let’s dive in!

RFC

An essential aspect of any successful iOS development team is the creation and adherence to an RFC (Request for Comments) process. But what exactly is an RFC and why is it so crucial? An RFC is a document that outlines the proposed feature, its impact on users, and the technical and architectural plan for its development. The term originated from the days of ARPANET (Advanced Research Projects Agency Network), where researchers would present ideas for discussion and feedback from their peers. In the team standards, it is a requirement that an RFC is created and approved before development can begin. This may seem like an added step that could potentially slow down the development process, but it serves a critical purpose. It ensures that all necessary research and planning have been done. It also helps to identify and address potential issues before they become costly and time-consuming problems down the line.

RFC to the Rescue: How to Avoid Common Pitfalls in iOS Development

When a developer — let’s call them “A” — is tasked with creating a new feature, they may jump straight into coding without conducting proper research. This leads to a flurry of questions and feedback from the other iOS engineers on the team during the review process. This constant back-and-forth can be incredibly disruptive to the developer’s focus and attention and can leave them feeling like they’re dancing on eggshells. Furthermore, if these changes end up altering the core of the initial plan, it can lead to two types of bugs: those that are hard to detect during testing due to time constraints, and those that slow down the overall progress of the feature’s development. This can be a risky and nerve-wracking experience for the developer.

With a little reflection, it becomes clear that the time spent on creating an RFC may seem like a wasted effort in the beginning, but it is actually a valuable investment in terms of onboarding the team, sharing ideas, and receiving feedback. It’s important to remember that this is just one example of the many benefits that an RFC can provide.

“None of us is as smart as all of us.” — Ken Blanchard

Coding Time

First steps with the Photos framework on iOS

Now let’s go on a journey through the process of developing a photo library. To begin, let’s review the fundamental methods of fetching photos. One way to retrieve photos from the iOS native photo library is by utilizing the photos framework provided by Apple. This framework grants access to the user’s photos and videos, enabling various operations such as retrieving metadata and editing them.

Here’s an example of how to fetch all photos from the user’s library:

  1. Import the Photos framework in your ViewController:
import Photos

2. Create a PHFetchOptions object to specify the options for fetching the photos. For example, you can set the sort descriptor to sort the photos by date:

let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]

3. Use the PHFetchResult object to fetch the photos from the library:

let fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)

4. Iterate through the fetchResult object to access each photo:

fetchResult.enumerateObjects { (asset, _, _) in
// do something with the asset, for example, retrieve the image by calling the following function
let image = getAssetThumbnail(asset: asset)
}

5. To retrieve the image from PHAsset, you can use the following function:

func getAssetThumbnail(asset: PHAsset) -> UIImage {
let manager = PHImageManager.default()
let option = PHImageRequestOptions()
var thumbnail = UIImage()
option.isSynchronous = true
manager.requestImage(for: asset, targetSize: CGSize(width: 100, height: 100), contentMode: .aspectFit, options: option, resultHandler: {(result, info)->Void in
thumbnail = result!
})
return thumbnail
}

Note: You should check for the user’s authorization before accessing their photo library.

Straightforward right? It’s a good place to start.

Defining the architecture of your photo library

With these steps in mind, you need to think about refactoring it and scaling it so it fits the project architecture. Here, you will need to brainstorm your ideas and try to explain every thought to yourself to make sure that you’ve found the right way forward. You will need to convert your thoughts into an architectural approach that you can explain to your team.

Here’s my architectural plan after reading the Apple documentation and researching existing solutions:

UML Diagram

In this diagram, you can see a new way of implementing the functional PHAsset and PHAssetCollection.

Note: I will not dive deep into writing the code.

In the bottom left, the MediaPickerCoordinator is where the navigation is handled (check the coordinator pattern). Inside the coordinator, we initiate two properties:

  1. MediaManager
  2. LibraryDataLogic

The MediaManager is similar to a functional repository that will handle your requests to fetch PHAsset , PHAssetCollection or PHAsset from a specific PHAssetCollection.

On the bottom right of the diagram, the MediaManager holds two kinds of repositories:

1. PhotoMediaRepository

2. AlbumMediaRepository

Each repository is responsible for providing the requested data and passing them to the MediaManager to convert them to a friendly model so that the UI can consume it.

PhotoMediaRepository

Three functions will be contained by this class:

1. loadLibrary

2. getPhotoPaginated

3. getFirstMedia

loadLibrary is responsible for returning the number of assets that exist in the library and retrieving PHFetchResult<PHAsset> which is the result of all the assets that do exist in the library, and that is only because we had a different approach previously — this will be deleted after full release — .

getPhotoPaginated, as per its name, will return a paginated MediaProtocol. MediaProtocol is a type wrapper of the PHAsset.

getFirstMedia will return the first media from a PHAssetResult.

AlbumMediaRepository

This class contains one main function named loadCollections and will return an array of MediaCollectionProtocol type (which is a type wrapper of PHAssetCollection).

Building the LibraryMediaManager

Now that we have these two repositories, we can start building the LibraryMediaManager which will feed us with serval properties and let us take action upon album selection.

Here are the provided functions:

1. func loadLibrary()->Int

2. func loadMedia(from collection: MediaCollectionProtocol)->Int

3. func getPhotoPaginated<T:MediaProtocol>(pagination:LibraryMediaPagination) throws ->[T]

4. func getFirstMedia<T:MediaProtocol>(from mediaRsult: PHAssetResult) throws -> T

5. func loadCollections<T:MediaCollectionProtocol>(collectionType:MediaCollection.CollectionType) -> [T]

6. func getRecentCollection<T:MediaCollectionProtocol>() throws -> T

Some delegates are also available to refresh the results of library changes.

These functions are called from a DataLogic class that communicates with the ViewModel accordingly. Additionally, the MediaManager also has delegates to refresh the results on library changes.

This architecture allows for a separation of concerns with the following split:

  • The MediaManager handles the communication with the photo library.
  • The repositories handle the specific data fetching.
  • The DataLogic and ViewModel handle the application logic and presentation of the data.

This makes the code more organized, maintainable, and easy to test.

This design also allows for flexibility in the future, as new features or changes to the photo library can be easily implemented in the MediaManager and repositories without affecting the rest of the codebase.

By using a functional repository pattern, the MediaManager can be reused across different parts of the app. It also allows for easy testing as the MediaManager and repositories can be tested independently of the rest of the codebase.

In summary, the MediaManager is designed to handle the communication with the photo library, the PhotoMediaRepository and AlbumMediaRepository handle the specific data fetching, and the DataLogic and ViewModel handle the application logic and presentation of the data. This design allows for a separation of concerns, flexibility, and maintainability in the codebase.

Testing and troobleshooting

With this architectural setup, all our use cases were succeeding without any issues. Surprisingly however, one problem popped up at the last moment before release.

It’s important to keep in mind that even with a solid architecture, bugs may still appear, but having a good structure in place can make it easier to track down and fix them. Additionally, keeping in mind the user experience, accessibility, and security will help you deliver a better product.

During testing, we primarily utilized test devices but occasionally used personal devices. We encountered no performance issues as the number of media on these devices did not exceed 4,000 photos. However, when a teammate tested the feature on their device containing 70,000 photos, it became clear that there were significant performance issues, specifically that the library screen took 2–3 seconds (and sometimes longer) to open. This bug was unexpected as performance considerations were not explicitly detailed in Apple’s documentation or other sources.

Here are my findings on improving fetching performance.

Use caching

In order to apply caching using the Photos framework’s built-in mechanism, you can use the PHCachingImageManager class. This class allows you to cache multiple assets at once, and automatically caches them for fast access.

let manager = PHCachingImageManager()
let assets = PHAsset.fetchAssets(with: .image, options: nil)
manager.startCachingImages(for: assets, targetSize: CGSize(width: 100, height: 100), contentMode: .aspectFill, options: nil)

This will cache all assets of type “image” with a target size of 100x100 pixels and content mode of aspectFill.

The PHCachingImageManager class also provides the stopCachingImages method to stop caching images when they are no longer needed. This can help to improve the performance of your app by only caching the images that are currently being displayed.

It’s worth noting that when using the PHCachingImageManager class, you should consider your app’s specific use case and your company’s requirements. Besides, it’s important to test the app on different device models and iOS versions to make sure that it works correctly. Also, make sure to consider the performance and memory usage when working with large amounts of data.

For third-party caching libraries such as SDWebImage or Kingfisher, you can use the built-in caching functionality provided by the library. For example, SDWebImage provides a UIImageView extension that allows you to load an image from a URL and automatically cache it for future use.

let imageView = UIImageView()
imageView.sd_setImage(with: imageURL)

Kingfisher also provides similar functionality, you can load an image with a URL and it will cache it for future use.

let imageView = UIImageView()
imageView.kf.setImage(with: imageURL)

Use the correct target size

When fetching assets from the Photo Library, you can specify the target size of the image by using the PHImageManager class. The target size is specified in pixels and represents the dimensions of the image you want to retrieve. By specifying a smaller target size, you can reduce the time it takes to fetch the image.

Here’s an example of how to fetch an image with a target size of 100x100 pixels:

let manager = PHImageManager.default()
let options = PHImageRequestOptions()
options.deliveryMode = .fastFormat
options.resizeMode = .fast
manager.requestImage(for: asset, targetSize: CGSize(width: 100, height: 100), contentMode: .aspectFill, options: options) { (image, _) in
// Use the image here
}

It’s important to note that specifying a smaller target size may result in a lower-resolution image. For this reason, it’s always better to consider the specific use case of your app and the needs of your users when choosing the target size. You should also consider that using a small target size will have a positive impact on the performance of your app, but using too small a target size might not be enough to display the image correctly in some scenarios.

Implement asynchronous requests

Using asynchronous requests to fetch assets from the photo library is a good way to improve the performance and efficiency of your app. When making synchronous requests, the app will block the main thread until the request is completed, which can result in a poor user experience.

By using asynchronous requests, the app can continue running while the request is being processed in the background, resulting in a more responsive user interface.

The Photos framework provides several ways to make asynchronous requests. One of the most common ways is to use the PHImageManager class to make an asynchronous request for an image. Here’s an example:

let manager = PHImageManager.default()
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.isSynchronous = false
manager.requestImage(for: asset, targetSize: CGSize(width: 100, height: 100), contentMode: .aspectFill, options: options) { (image, _) in
// Use the image here
}

In the above code snippet, the isSynchronous property is set to false. This makes the request asynchronous. You can also specify other options, such as the delivery mode, to further optimize the request.

You can use some third-party libraries like AlamoFire to make async requests as well.

In all cases, it’s important to note that using asynchronous requests will generally improve performance. It’s also essential to handle the responses correctly and not overload the system with too many requests at once.

You can use the options provided by PHImageRequestOptions to specify how the image should be delivered and to optimize the fetching process.

The PHImageRequestOptions class provides various options such as deliveryMode, resizeMode, isNetworkAccessAllowed, version and more which you can use to fine-tune the request.

For example, you can set the deliveryMode to .fastFormat which will increase the fetching speed.

let options = PHImageRequestOptions()
options.deliveryMode = .fastFormat

This will tell the image manager to deliver the requested image as quickly as possible. When this option is set, the image manager may return a lower-quality image.

Another option you can use is resizeMode. You can set it to .fast which will cause the image manager to resize the image as quickly as possible.

options.resizeMode = .fast

You can also set isNetworkAccessAllowed to true to allow the image manager to retrieve the image from the network if it’s not available locally.

options.isNetworkAccessAllowed = true

PHImageRequestOptions provides a version property too. This property is used to request a specific version of an asset.

options.version = .original

It’s important to note that when using the PHImageRequestOptions class, you should consider your app’s specific use case and your company’s requirements. Additionally, you should test the app on different device models and iOS versions to make sure that it works correctly. Make sure to also consider the performance and memory usage when working with large amounts of data.

Choose the correct image format

Use the correct image format. The format of the image you fetch can have a significant impact on the performance of your app. When fetching images that will be displayed on the screen, you should use a format that is optimized for screen display, such as JPEG or PNG. These formats are more efficient and take up less space than other formats like TIFF or HEIF.

JPEG is a lossy format, meaning that it discards some image data to reduce file size. It’s good for images with a lot of color details such as photographs. It’s also supported by all web browsers and many image editing software.

PNG is a lossless format. It means that it preserves all the data in the image. It’s good for images with solid colors and simple shapes. It’s supported by all web browsers and many image editing software as well.

On the other hand, if you are fetching images that will be processed or manipulated, you should use a format such as HEIF or TIFF that can provide higher quality. HEIF is a new format developed by Apple. It’s designed for high-quality images and videos, and it’s also more efficient than JPEG and TIFF in terms of storage space.

TIFF is a lossless format that is good for images that need to be edited and manipulated. It’s also good for images with a lot of color details such as photographs but is not supported by all web browsers and many image editing software.

It’s important to consider the specific use case of your app and the requirements of your company when choosing the format of the image you fetch. Additionally, you should test the app on different device models and iOS versions to make sure that it works correctly and also to consider the performance and memory usage when working with large amounts of data.

Leverage the content mode option

When fetching an image, you can use the content mode option to specify how the image should be displayed. This can help to reduce the time it takes to fetch the image and improve the performance of your app.

The Photos framework provides several content mode options that you can use, including:

  • PHImageContentMode.aspectFit: This content mode scales the image to fit within the specified size while preserving the aspect ratio of the original image. This can be useful for displaying small thumbnails where the entire image should be visible but doesn’t have to fill the entire space.
  • PHImageContentMode.aspectFill: This content mode scales the image to fill the specified size while preserving the aspect ratio of the original image. This can be useful for displaying images in a full-screen view where the entire image should be visible and fill the whole space.
  • PHImageContentMode.default: This content mode returns the image in its original size and aspect ratio.

You can use the appropriate content mode based on your app’s specific use case and your company’s requirements. Additionally, you should test the app on different device models and iOS versions to make sure that it works correctly, and also to consider the performance and memory usage when working with large amounts of data.

It’s important to note that when specifying the content mode, the framework will try to return the image in the exact size you asked for, but if the image size is not available, the framework will scale the image to match the size you asked for. Scaling images can take a long time and consume more resources on a device, so you should use the appropriate content mode for the specific use case of your app.

Conclusion

Developing a photo library or any complex feature for an iOS app can be a challenging task, but with your skills as an iOS developer, you are well-suited to tackle it. Here are a few things to consider as you begin.

1. Understand the requirements: Before you start coding, make sure you have a clear understanding of what the photo library should do and how it should behave. Get answers to questions such as “Will users be able to upload their own photos?”, “Will the library include editing tools?”, “How will the photos be organized and displayed to users?”

2. Research existing solutions: There are many open-source libraries and frameworks available that can help you implement a photo library in your app. Look into popular options such as Photos Framework, Photos UI, and Kingfisher and see if they meet your needs.

3. Plan your architecture: Once you have a good understanding of the requirements and existing solutions, you can start planning the architecture of your library. Consider factors such as performance, scalability, and maintainability.

4. Write clean, well-commented code: As you begin coding, make sure to write clean, well-organized code that is easy to understand and maintain. Remember to add comments and documentation to help others understand your code.

5. Test and debug: As you develop the library, be sure to test it thoroughly, and debug any issues that you encounter. Make sure it works correctly on different device models and iOS versions.

6. Follow the company’s guidelines and use their tools. Also, it’s better to use the company’s RFC process to get feedback and approval before implementation.

7. Remember that coding is not the only element of your project. You should also consider the user experience, accessibility, and security.

8. Don’t hesitate to ask for help or feedback from other team members. They may have different experiences that could help you.

--

--

Ali Fakih
Vestiaire Connected

I'm a software dev & tech enthusiast who loves iOS, Swift, networking & electronics. Follow me for insightful articles on software & tech.