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:
- ✅ Separação clara View/Logic
- ✅ ViewModels testáveis sem SwiftUI
- ✅
@Published
properties = reatividade sem boilerplate - ✅ Padrão nativo do ecossistema Swift
Alternativas Consideradas:
- MVC: Muito acoplamento entre View e Controller
- VIPER: Over-engineering para escopo da app
- Redux/TCA: Complexidade desnecessária
Trade-offs:
- Boilerplate para bindings
- Learning curve para Combine
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:
- ✅ Declarativo e reativo
- ✅ Hot reload para desenvolvimento rápido
- ✅ Bindings bidirecionais nativos
- ✅ Animation system poderoso
- ✅ Futuro do desenvolvimento Apple
Ponte UIKit Quando Necessário:
AVCaptureVideoPreviewLayer
(não existe em SwiftUI)UITextView
para scroll preciso- Gestos avançados em preview
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:
- iOS 18.5+ requirement
- Alguns recursos ainda requerem UIKit
3. Serial DispatchQueue para Session
Decisão: Usar queue serial dedicada para todas as operações de AVFoundation.
Razões:
- ✅
AVCaptureSession
não é thread-safe - ✅ Serial queue garante operações sequenciais
- ✅ Evita race conditions sem locks complexos
- ✅ Padrão recomendado pela Apple
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)
- Mais complexo
- Propenso a deadlocks
- Menos idiomático
4. Gravação Segmentada vs Única
Decisão: Implementar sistema de gravação segmentada (múltiplos takes).
Razões:
- ✅ Permite erros sem perder trabalho
- ✅ Facilita iteração criativa
- ✅ Comum em workflow de roteiros
- ✅ Flexibilidade para usuário
Trade-offs:
- Maior uso de disco temporário
- Complexidade de concatenação
- Precisa gerenciar múltiplos arquivos
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
- Mais simples
- Menos flexível
- Usuário perde todo vídeo se errar
5. Torch de Tela para Front Camera
Decisão: Simular torch aumentando brilho da tela na câmera frontal.
Razões:
- ✅ Front camera não tem flash hardware
- ✅ Método padrão da indústria (Instagram, TikTok)
- ✅ Funciona em todos os devices
- ✅ Feedback visual claro para usuário
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:
- ✅ ~50% menor tamanho com qualidade equivalente
- ✅ Suportado em iOS 11+ (target é 18.5+)
- ✅ Padrão em devices modernos
- ✅ Melhor para armazenamento e compartilhamento
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:
- ✅
AVCaptureMovieFileOutput
não suporta composição inline - ✅ Mantém gravação performática
- ✅ Export assíncrono não bloqueia UI
- ✅ Permite filtros complexos sem impacto em tempo real
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
- Muito mais complexo
- Impacto significativo em performance
- Consome bateria
- Requer
AVAssetWriter
manual
8. Frame Rate Padrão 60fps
Decisão: Usar 60fps como padrão, com opção de 30fps.
Razões:
- ✅ Smooth motion para conteúdo moderno
- ✅ Padrão para tech reviews e apresentações
- ✅ Devices modernos suportam facilmente
- ✅ Diferencial de qualidade
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:
- ✅ Acima de 6x, interpolação digital degrada qualidade
- ✅ Evita UX ruim (vídeo pixelado)
- ✅ Profissionais raramente usam >6x
- ✅ Força usuário a se aproximar fisicamente (melhor enquadramento)
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:
- ✅ SwiftUI
Text
não oferece scroll programático preciso - ✅
UITextView
permite controle total decontentOffset
- ✅ Performance superior para textos longos
- ✅ Tipografia consistente com cálculos de height
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:
- ✅
boundingRect
é caro (~5ms) - ✅ Durante drag, usuário não precisa de precisão
- ✅ Evita centenas de recálculos desnecessários
- ✅ Mantém scroll suave
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:
- ✅ Usuário não precisa pensar em orientação
- ✅ Vídeos sempre reproduzem corretamente
- ✅ Flexibilidade para diferentes cenários (landscape tutorials, etc)
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:
- ✅ Sistema gerencia transições entre lentes automaticamente
- ✅ Smooth zoom 0.5x-2x+ sem troca manual
- ✅ Melhor UX (sem “jump” visual)
- ✅ Acesso a múltiplas câmeras via zoom
Hierarquia de Preferência (Back):
.builtInTripleCamera
.builtInDualWideCamera
.builtInDualCamera
.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:
- Serial queue para thread-safety
- Background processing para thumbnails
- Cache de cálculos pesados
- Async export
2. User Experience
Decisões guiadas por UX:
- 60fps padrão para smoothness
- Gravação segmentada para flexibilidade
- Teleprompter flutuante e redimensionável
- Torch simulado na front camera
3. Code Quality
Decisões guiadas por qualidade:
- MVVM para testabilidade
[weak self]
para evitar leaks- Error handling robusto
- Logging detalhado
4. Maintainability
Decisões guiadas por manutenibilidade:
- Separação de concerns
- Componentes modulares
- Documentação inline
- Naming semântico
📊 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
- Filtros em Tempo Real
- Usar Metal para performance
- Preview preciso do filtro
- Trade-off: Complexidade++
- Background Recording
- Continuar gravando em background
- Trade-off: Bateria, iOS restrictions
- Cloud Sync
- Upload automático de vídeos
- Trade-off: Privacidade, storage
📚 Ver Também
- Arquitetura - Visão geral
- Performance - Otimizações aplicadas
- Fluxo de Dados - Como tudo se conecta
← Performance | Início → |