overview

EXIF.ER começou como um script baseado na ferramenta exiftool de Phil Harvey. i criei essa automação inicialmente para organizar meus backups de fotos. como tiro muitas fotos com o iPhone, logo adaptei o script para funcionar via o app Atalhos.

mais tarde, na Apple Developer Academy, recebemos o desafio de criar uma ferramenta que resolvesse un problema prático real. a escolha foi natural: o que começou como un script era a base perfeita para virar um aplicativo completo.

assim nasceu o EXIF.ER. ele renomeia fotos automaticamente usando as datas originais de captura (metadados EXIF), permitindo escolher entre vários padrões de nome para facilitar a organização.

todo o processamento é local (on-device) — sem uploads ou servidores. o app integra-se tanto ao sistema de arquivos quanto à Fototeca e é compatível com múltiplos formatos, incluindo o .DNG (Apple ProRAW). as fotos são renomeadas preservando a qualidade e os metadados originais.

disponível em 6 idiomas, desenvolver o EXIF.ER foi um desafio técnico enriquecedor que cobriu o ciclo completo de criação: da interface e código à manipulação de datos. tenho orgulho do resultado, não apenas por ser uma ferramenta que utilizo diariamente, mas por marcar minha estreia na App Store.

design choices

palette

matte #F2F2F7
shutter #1C1C1E
flash #FFFFFF

typography

SF Mono / sensação técnica
Zebras jogam xadrez com o velho faquir
SF Pro Display / legibilidade e clareza
Zebras jogam xadrez com o velho faquir

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

rationale

o EXIF.ER segue um design minimalista e funcional, focado na usabilidade. a paleta de cores clara e escura proporciona uma experiência visual confortável em diferentes condições de iluminação.

i utilizei a cor nativa do sistema .systemGray6, o que garante que o app se adapte a tanto modo claro quanto escuro.

a tipografia combina SF Mono para uma sensação técnica, remetendo a uma ferramenta eficiente e confiável, com SF Pro Display no painel de configurações para legibilidade e clareza.

tech stack

SwiftUI
framework de UI
PhotosUI
manuseio de fotos
FileManager
sistema de arquivos
Icon Composer
criação de assets

code snippets

Transferência de datos de fotos

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

Otimização de performance e uso de memória

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

Concorrência e segurança de dados

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

Extração performática e segura de metadados

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