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#F2F2F7shutter#1C1C1Eflash#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)}