📷 Camera App

Escolhas Técnicas

Documentação das principais decisões de arquitetura e design, com justificativas e trade-offs.


🏛️ Decisões Arquiteturais

1. MVVM com Combine

Decisão: Usar MVVM como padrão arquitetural com Combine para reatividade.

Razões:

Alternativas Consideradas:

Trade-offs:

Exemplo:

class CameraViewModel: ObservableObject {
    @Published var isRecording: Bool = false
    
    func toggleRecording() {
        // Logic aqui
        isRecording.toggle()
    }
}

struct ContentView: View {
    @StateObject var model = CameraViewModel()
    
    var body: some View {
        Button(model.isRecording ? "Stop" : "Record") {
            model.toggleRecording()
        }
    }
}

2. SwiftUI para UI

Decisão: Usar SwiftUI como framework UI principal.

Razões:

Ponte UIKit Quando Necessário:

Implementação:

struct CameraPreviewView: UIViewRepresentable {
    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        let previewLayer = AVCaptureVideoPreviewLayer(session: session)
        view.layer.addSublayer(previewLayer)
        return view
    }
}

Trade-offs:


3. Serial DispatchQueue para Session

Decisão: Usar queue serial dedicada para todas as operações de AVFoundation.

Razões:

Implementação:

private let sessionQueue = DispatchQueue(label: "camera.session.queue")

func configureSession() {
    sessionQueue.async { [weak self] in
        self?.session.beginConfiguration()
        // Configuração thread-safe
        self?.session.commitConfiguration()
    }
}

Alternativa Rejeitada: Locks manuais (NSLock)


4. Gravação Segmentada vs Única

Decisão: Implementar sistema de gravação segmentada (múltiplos takes).

Razões:

Trade-offs:

Implementação:

class SegmentedRecorder {
    func startNewSegment() {
        let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
            .appendingPathComponent("segment_\(UUID()).mov")
        output.startRecording(to: tempURL, recordingDelegate: self)
    }
}

Alternativa Considerada: Gravação única contínua


5. Torch de Tela para Front Camera

Decisão: Simular torch aumentando brilho da tela na câmera frontal.

Razões:

Implementação:

func setTorchEnabled(_ enabled: Bool) {
    if currentPosition == .front {
        if enabled {
            savedScreenBrightness = UIScreen.main.brightness
            UIScreen.main.brightness = 1.0
        } else {
            UIScreen.main.brightness = savedScreenBrightness ?? 0.5
        }
    } else {
        // Use hardware torch
        device.torchMode = enabled ? .on : .off
    }
}

Trade-off: Consome bateria (aceitável para uso temporário)


6. HEVC como Codec Padrão

Decisão: Preferir HEVC (H.265) com fallback para H.264.

Razões:

Implementação:

func setPreferredCodecHEVC(_ enabled: Bool) {
    let available = movieFileOutput.availableVideoCodecTypes
    
    if enabled, available.contains(.hevc) {
        movieFileOutput.setOutputSettings(
            [AVVideoCodecKey: AVVideoCodecType.hevc],
            for: connection
        )
    } else if available.contains(.h264) {
        movieFileOutput.setOutputSettings(
            [AVVideoCodecKey: AVVideoCodecType.h264],
            for: connection
        )
    }
}

Trade-off: Encoding ligeiramente mais lento (negligível em devices modernos)


7. Filtros na Exportação

Decisão: Aplicar filtros durante export, não em tempo real.

Razões:

Implementação:

let videoComposition = AVVideoComposition(asset: asset) { request in
    let filter = CIFilter.photoEffectMono()
    filter.inputImage = request.sourceImage
    request.finish(with: filter.outputImage, context: nil)
}
exporter.videoComposition = videoComposition

Trade-off: Usuário não vê preview exato do filtro (apenas hint)

Alternativa Rejeitada: Tempo real com AVCaptureVideoDataOutput


8. Frame Rate Padrão 60fps

Decisão: Usar 60fps como padrão, com opção de 30fps.

Razões:

Seleção de Formato:

// Prefere maior resolução que suporte o FPS
var bestFormat: AVCaptureDevice.Format?
var maxPixels = 0

for format in device.formats {
    guard let range = format.videoSupportedFrameRateRanges.first(
        where: { $0.maxFrameRate >= 60 }
    ) else { continue }
    
    let dims = CMVideoFormatDescriptionGetDimensions(format.formatDescription)
    let pixels = Int(dims.width) * Int(dims.height)
    
    if pixels > maxPixels {
        maxPixels = pixels
        bestFormat = format
    }
}

Trade-off: 2x tamanho de arquivo vs 30fps (mitigado por HEVC)


9. Limite de Zoom 6.0x

Decisão: Limitar zoom máximo em 6.0x mesmo que device suporte mais.

Razões:

Implementação:

let maxZoom = min(device.maxAvailableVideoZoomFactor, 6.0)
let clamped = max(minZoom, min(maxZoom, factor))
device.videoZoomFactor = clamped

Trade-off: Menos flexibilidade (aceitável para qualidade)


10. UITextView para Teleprompter

Decisão: Usar UITextView via UIViewRepresentable para scroll do teleprompter.

Razões:

Bridge SwiftUI-UIKit:

struct TeleprompterTextView: UIViewRepresentable {
    @Binding var contentOffset: CGFloat
    
    func makeCoordinator() -> Coordinator {
        Coordinator(contentOffset: $contentOffset)
    }
    
    func updateUIView(_ textView: UITextView, context: Context) {
        if abs(textView.contentOffset.y - contentOffset) > 0.5 {
            context.coordinator.isProgrammaticScroll = true
            textView.setContentOffset(CGPoint(x: 0, y: contentOffset), animated: false)
            context.coordinator.isProgrammaticScroll = false
        }
    }
}

Trade-off: Complexidade do Coordinator pattern (necessária para evitar loops)


11. Debouncing durante Interação

Decisão: Debounce de cálculos pesados (height) durante drag/resize.

Razões:

Implementação:

func scheduleContentHeightUpdate() {
    if isInteracting {
        scheduledUpdate?.cancel()
        let work = DispatchWorkItem { [weak self] in
            self?.updateContentHeight(...)
        }
        scheduledUpdate = work
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.12, execute: work)
    } else {
        updateContentHeight(...)
    }
}

Trade-off: Altura pode estar ligeiramente desatualizada durante interação (imperceptível)


12. Orientação Automática

Decisão: Suportar todas as orientações com transform automático.

Razões:

Implementação:

NotificationCenter.default.addObserver(
    forName: UIDevice.orientationDidChangeNotification,
    object: nil,
    queue: .main
) { [weak self] _ in
    if let videoOrientation = AVCaptureVideoOrientation(
        deviceOrientation: UIDevice.current.orientation
    ) {
        self?.controller.setVideoOrientation(videoOrientation)
        self?.recorder?.updateOrientation(from: UIDevice.current.orientation)
    }
}

// Durante concatenação
videoTrack.preferredTransform = assetVideoTrack.preferredTransform

Trade-off: Nenhum significativo


13. Virtual Device Preferencial

Decisão: Preferir virtual devices (Triple/Dual Camera) quando disponíveis.

Razões:

Hierarquia de Preferência (Back):

  1. .builtInTripleCamera
  2. .builtInDualWideCamera
  3. .builtInDualCamera
  4. .builtInWideAngleCamera

Trade-off: Menos controle granular sobre lente específica (não é problema para nossa UX)


🎯 Princípios de Design

1. Performance First

Decisões guiadas por performance:

2. User Experience

Decisões guiadas por UX:

3. Code Quality

Decisões guiadas por qualidade:

4. Maintainability

Decisões guiadas por manutenibilidade:


📊 Trade-off Matrix

Decisão Prós Contras Escolha
MVVM Testável, Separado Boilerplate ✅ Vale a pena
SwiftUI Moderno, Rápido dev iOS 18.5+ ✅ Target ok
60fps Smooth 2x arquivo ✅ + HEVC mitiga
Segmentado Flexível Complexo ✅ UX crítico
Filtros Export Performático Sem preview ✅ Trade-off ok
Zoom 6x Qualidade Menos zoom ✅ Qualidade > zoom

🔮 Decisões Futuras

Considerando

  1. Filtros em Tempo Real
    • Usar Metal para performance
    • Preview preciso do filtro
    • Trade-off: Complexidade++
  2. Background Recording
    • Continuar gravando em background
    • Trade-off: Bateria, iOS restrictions
  3. Cloud Sync
    • Upload automático de vídeos
    • Trade-off: Privacidade, storage

📚 Ver Também


← Performance Início →