overview

EXIF.ER started as a script based on Phil Harvey’s exiftool. i originally built this automation to organize my photo backups. since i take lots of photos with my iPhone, i soon adapted the script to work through the Shortcuts app.

later, at the Apple Developer Academy, we were challenged to create a tool that solved a real practical problem. the choice felt natural: what started as a script was the perfect foundation for a full app.

that’s how EXIF.ER was born. it automatically renames photos using their original capture dates (EXIF metadata), letting you choose from multiple naming templates for easier organization.

all processing works locally on-device—no uploads or servers. the app integrates with both the file system and the Photo Library and supports multiple formats, including .DNG (Apple ProRAW). photos are renamed while preserving their original quality and metadata.

available in 6 languages, developing EXIF.ER was an enriching technical challenge that covered the full development cycle: from interface and code to data manipulation. i am proud of the result, not only because it is a tool i use daily, but because it marks my debut on the App Store.

design choices

palette

matte #F2F2F7
shutter #1C1C1E
flash #FFFFFF

typography

SF Mono / technical feel
The quick fox jumps over the lazy dog
SF Pro Display / readability & clarity
The quick fox jumps over the lazy dog

some fonts used in this project are proprietary and may not display correctly if they are not installed on your system.

rationale

EXIF.ER follows a minimalist and functional design, focused on usability. the light and dark color palette provides a comfortable visual experience in different lighting conditions.

i used the native system color .systemGray6, which ensures the app adapts to both light and dark modes.

the typography combines SF Mono for a technical feel, evoking an efficient and reliable tool, with SF Pro Display in the settings panel for readability and clarity.

tech stack

SwiftUI
UI framework
PhotosUI
photo handling
FileManager
file system
Icon Composer
asset creation

code snippets

Transferring photo data

swift
struct PhotoFile: Transferable {
    let url: URL
    
    static var transferRepresentation: some TransferRepresentation {
        FileRepresentation(contentType: .image) { photoFile in
            SentTransferredFile(photoFile.url)
        } importing: { received in
            let fileName = received.file.lastPathComponent
            let copyURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName)
            
            if FileManager.default.fileExists(atPath: copyURL.path) {
                try? FileManager.default.removeItem(at: copyURL)
            }
            
            try FileManager.default.copyItem(at: received.file, to: copyURL)
            return PhotoFile(url: copyURL)
        }
    }
}

Performance and memory usage optimization

swift
private func loadThumbnails() async {
    self.isLoadingThumbnails = true
    var loadedImages: [UIImage] = []
    
    for item in selectedPhotoItems.prefix(3) {
        if let data = try? await item.loadTransferable(type: Data.self) {
             if let source = CGImageSourceCreateWithData(data as CFData, nil) {
                 let options: [CFString: Any] = [
                    kCGImageSourceCreateThumbnailFromImageAlways: true,
                    kCGImageSourceCreateThumbnailWithTransform: true,
                    kCGImageSourceThumbnailMaxPixelSize: 300
                 ]
                 if let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) {
                     loadedImages.append(UIImage(cgImage: cgImage))
                 }
             }
        }
    }
}

Concurrency and data safety

swift
actor NamingService {
    func generateFinalURL(baseName: String, fileExtension: String, in folderURL: URL) -> URL {
        var finalURL: URL
        var counter = 0
        
        repeat {
            let suffix = counter > 0 ? "_(counter)" : ""
            let newFilename = "(baseName)(suffix).(fileExtension)"
            finalURL = folderURL.appendingPathComponent(newFilename)
            counter += 1
        } while FileManager.default.fileExists(atPath: finalURL.path)
        
        return finalURL
    }
}

Performant, safe metadata extraction

swift
private func getImageCreationDate(from url: URL) -> Date? {
    
    guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
          let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any] else {
        return nil
    }
    
    let exif = properties[kCGImagePropertyExifDictionary as String] as? [String: Any]
    let tiff = properties[kCGImagePropertyTIFFDictionary as String] as? [String: Any]
    
    let dateString = (exif?[kCGImagePropertyExifDateTimeOriginal as String] as? String) ??
                     (tiff?[kCGImagePropertyTIFFDateTime as String] as? String)
    
    guard let dateString = dateString else { return nil }
    
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy:MM:dd HH:mm:ss"
    return formatter.date(from: dateString)
}

credits

people

design · development · shipping
pedro wiezel

assets

iconography