πŸ“· Camera App

Arquitetura

VisΓ£o completa da arquitetura do Camera App, padrΓ΅es utilizados e fluxo de dados.


πŸ›οΈ PadrΓ£o Arquitetural

A aplicaΓ§Γ£o utiliza MVVM (Model-View-ViewModel) com comunicaΓ§Γ£o reativa via Combine, garantindo:


πŸ“Š Diagrama Geral

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    SwiftUI Views                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ ContentView  β”‚  β”‚ Teleprompter β”‚  β”‚ CameraPreviewβ”‚  β”‚
β”‚  β”‚              β”‚  β”‚   Overlay    β”‚  β”‚     View     β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚         β”‚                 β”‚                 β”‚           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚                 β”‚                 β”‚
          β–Ό                 β–Ό                 β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     ViewModels                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚   CameraViewModel        β”‚  β”‚ TeleprompterViewModelβ”‚ β”‚
β”‚  β”‚  @Published properties   β”‚  β”‚  @Published props    β”‚ β”‚
β”‚  β”‚  Business logic          β”‚  β”‚  Scroll management   β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚           β”‚                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚
            β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚               Controllers & Services                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚ CaptureSession       β”‚  β”‚ SegmentedRecorder    β”‚    β”‚
β”‚  β”‚   Controller         β”‚  β”‚                      β”‚    β”‚
β”‚  β”‚ - Session management β”‚  β”‚ - Recording segments β”‚    β”‚
β”‚  β”‚ - Device config      β”‚  β”‚ - Delegate callbacks β”‚    β”‚
β”‚  β”‚ - Format selection   β”‚  β”‚                      β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚
            β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  AVFoundation                           β”‚
β”‚         AVCaptureSession β€’ AVCaptureDevice              β”‚
β”‚    AVCaptureMovieFileOutput β€’ AVCaptureDeviceInput      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“¦ Responsabilidades por Camada

1. View (SwiftUI)

Responsabilidades:

CaracterΓ­sticas:

Exemplo:

struct ContentView: View {
    @StateObject private var model = CameraViewModel()
    
    var body: some View {
        ZStack {
            CameraPreviewRepresentable(controller: model.controller)
            
            Button("Record") {
                model.toggleRecording() // Delega para ViewModel
            }
        }
    }
}

2. ViewModel

Responsabilidades:

CaracterΓ­sticas:

Exemplo:

class CameraViewModel: ObservableObject {
    @Published var isRecording: Bool = false
    @Published var segments: [RecordedSegment] = []
    
    private let controller: CaptureSessionController
    private var recorder: SegmentedRecorder?
    
    func toggleRecording() {
        if isRecording {
            recorder?.stopCurrentSegment()
        } else {
            recorder?.startNewSegment()
        }
        isRecording.toggle()
    }
}

3. Controller/Service

Responsabilidades:

CaracterΓ­sticas:

Exemplo:

class CaptureSessionController {
    private let session = AVCaptureSession()
    private let sessionQueue = DispatchQueue(label: "camera.session.queue")
    
    func configureSession(desiredFrameRate: DesiredFrameRate) {
        sessionQueue.async {
            self.session.beginConfiguration()
            // ConfiguraΓ§Γ£o pesada...
            self.session.commitConfiguration()
        }
    }
}

4. AVFoundation

Responsabilidades:

CaracterΓ­sticas:


πŸ”„ Fluxo de Dados

Fluxo de InicializaΓ§Γ£o

sequenceDiagram
    participant ContentView
    participant CameraViewModel
    participant Controller
    participant System

    ContentView->>CameraViewModel: .onAppear()
    CameraViewModel->>CameraViewModel: requestPermissionsAndConfigure()
    CameraViewModel->>Controller: requestPermissions()
    Controller->>System: AVCaptureDevice.requestAccess(.video)
    Controller->>System: AVCaptureDevice.requestAccess(.audio)
    System-->>Controller: granted = true
    Controller-->>CameraViewModel: completion(granted: true)
    CameraViewModel->>CameraViewModel: isAuthorized = true
    CameraViewModel->>Controller: configureSession(fps: .fps60)
    Controller->>System: Configure devices and session
    Controller-->>CameraViewModel: completion(error: nil)
    CameraViewModel->>Controller: startSession()
    CameraViewModel-->>ContentView: isSessionRunning = true

Fluxo de GravaΓ§Γ£o

sequenceDiagram
    participant User
    participant ContentView
    participant CameraViewModel
    participant SegmentedRecorder
    participant AVFoundation
    participant FileSystem

    User->>ContentView: Tap Record Button
    ContentView->>CameraViewModel: toggleRecording()
    CameraViewModel->>SegmentedRecorder: startNewSegment()
    SegmentedRecorder->>FileSystem: Create temp URL
    SegmentedRecorder->>AVFoundation: output.startRecording(to: tempURL)
    AVFoundation-->>SegmentedRecorder: didStartRecordingTo
    SegmentedRecorder-->>CameraViewModel: (recording started)
    CameraViewModel-->>ContentView: isRecording = true
    
    Note over User,AVFoundation: User records...
    
    User->>ContentView: Tap Stop Button
    ContentView->>CameraViewModel: toggleRecording()
    CameraViewModel->>SegmentedRecorder: stopCurrentSegment()
    SegmentedRecorder->>AVFoundation: output.stopRecording()
    AVFoundation-->>SegmentedRecorder: didFinishRecordingTo: URL
    SegmentedRecorder-->>CameraViewModel: delegate.didFinishSegment(url)
    CameraViewModel->>CameraViewModel: Generate thumbnail
    CameraViewModel-->>ContentView: segments.append(segment)

Ver fluxo completo de dados β†’


🧩 Componentes Principais

CameraViewModel

Papel: ViewModel central da aplicaΓ§Γ£o

Responsabilidades:

DocumentaΓ§Γ£o completa β†’


CaptureSessionController

Papel: Controller de baixo nΓ­vel para AVFoundation

Responsabilidades:

DocumentaΓ§Γ£o completa β†’


SegmentedRecorder

Papel: Gerenciador de gravaΓ§Γ£o segmentada

Responsabilidades:

DocumentaΓ§Γ£o completa β†’


TeleprompterOverlay

Papel: Componente UI do teleprompter

Responsabilidades:

DocumentaΓ§Γ£o completa β†’


πŸ”€ DependΓͺncias entre MΓ³dulos

CameraApp (Entry Point)
    └── ContentView
            β”œβ”€β”€ CameraViewModel
            β”‚       β”œβ”€β”€ CaptureSessionController
            β”‚       β”‚       └── AVFoundation
            β”‚       └── SegmentedRecorder
            β”‚               └── AVFoundation
            β”œβ”€β”€ CameraPreviewView (UIKit Bridge)
            β”‚       └── AVCaptureVideoPreviewLayer
            └── TeleprompterOverlay
                    β”œβ”€β”€ TeleprompterViewModel
                    └── TeleprompterTextView (UIKit Bridge)
                            └── UITextView

PrincΓ­pio: DependΓͺncias fluem para baixo. Views nΓ£o conhecem Controllers. Controllers nΓ£o conhecem Views.


🧡 Threading Model

Queues Utilizadas

Queue PropΓ³sito Tipo
Main AtualizaΓ§Γ£o de UI Serial
sessionQueue ConfiguraΓ§Γ£o AVCapture Serial
Global(.userInitiated) Thumbnails Concurrent
Global(.utility) Export de vΓ­deo Concurrent

Regras de Threading

⚠️ Crítico

Session Configuration: Sempre na sessionQueue sessionQueue.async {
  session.beginConfiguration()
  // modifications...
  session.commitConfiguration()
}

⚠️ Crítico

UI Updates: Sempre na Main thread DispatchQueue.main.async {
  self.isSessionRunning = true
}

βœ… Best Practice

Device Lock: Na sessionQueue com try-catch sessionQueue.async {
  try device.lockForConfiguration()
  device.videoZoomFactor = 2.0
  device.unlockForConfiguration()
}

🎭 Padráes de Design

Observer Pattern

Usado via Combine para reatividade:

@Published var isRecording: Bool = false

View observa automaticamente:

.onChange(of: model.isRecording) { newValue in
    // React to change
}

Delegation Pattern

Usado para callbacks de gravaΓ§Γ£o:

protocol SegmentedRecorderDelegate: AnyObject {
    func recorder(_ recorder: SegmentedRecorder, didFinishSegment url: URL)
    func recorder(_ recorder: SegmentedRecorder, didFailWith error: Error)
}

CameraViewModel implementa:

extension CameraViewModel: SegmentedRecorderDelegate {
    func recorder(_ recorder: SegmentedRecorder, didFinishSegment url: URL) {
        generateThumbnail(for: url)
    }
}

Coordinator Pattern

Usado em UIViewRepresentable bridges:

struct TeleprompterTextView: UIViewRepresentable {
    class Coordinator: NSObject, UITextViewDelegate {
        var isProgrammaticScroll: Bool = false
        
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            guard !isProgrammaticScroll else { return }
            // Update SwiftUI binding
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
}

πŸ—οΈ Arquitetura de Arquivos

Camera/
β”œβ”€β”€ App Layer
β”‚   └── CameraApp.swift                    # Entry point
β”‚
β”œβ”€β”€ View Layer
β”‚   β”œβ”€β”€ ContentView.swift                  # Main orchestrator
β”‚   β”œβ”€β”€ CameraPreviewView.swift            # UIKit bridge
β”‚   β”œβ”€β”€ TeleprompterOverlay.swift          # Teleprompter UI
β”‚   β”œβ”€β”€ TeleprompterTextView.swift         # UITextView bridge
β”‚   └── GlassCompat.swift                  # Reusable UI components
β”‚
β”œβ”€β”€ ViewModel Layer
β”‚   β”œβ”€β”€ CameraViewModel.swift              # Main state & logic
β”‚   └── TeleprompterViewModel.swift        # Teleprompter state
β”‚
β”œβ”€β”€ Controller/Service Layer
β”‚   β”œβ”€β”€ CaptureSessionController.swift     # AVFoundation manager
β”‚   └── SegmentedRecorder.swift            # Recording manager
β”‚
└── Resources
    β”œβ”€β”€ Assets.xcassets/                   # Visual assets
    └── Camera.entitlements                # Permissions

πŸ“ PrincΓ­pios Seguidos

Single Responsibility

Cada classe tem uma ΓΊnica responsabilidade clara:

Dependency Inversion

Controllers nΓ£o conhecem ViewModels. ViewModels conhecem Controllers (injeΓ§Γ£o de dependΓͺncia):

class CameraViewModel {
    private let controller: CaptureSessionController
    
    init(controller: CaptureSessionController = .init()) {
        self.controller = controller
    }
}

Open/Closed

ExtensΓ­vel sem modificar cΓ³digo existente. Exemplo: adicionar novos filtros:

enum VideoFilter: String, CaseIterable {
    case none
    case mono
    case sepia  // ← Novo filtro
}

πŸ” PrΓ³ximos Passos

Agora que vocΓͺ entende a arquitetura geral:

  1. Explore os Componentes - Detalhes de cada mΓ³dulo
  2. Veja o Fluxo de Dados - Como os dados fluem
  3. Entenda as Escolhas TΓ©cnicas - Por que essas decisΓ΅es

← ComeΓ§ando Componentes β†’