スキル一覧に戻る
co-labs-co

swiftui-avfoundation-camera-bridge

by co-labs-co

Central skills repository for ContextHarness - curated agent skills and workflows

0🍴 0📅 2026年1月12日
GitHubで見るManusで実行

SKILL.md


name: swiftui-avfoundation-camera-bridge version: 0.1.0 author: cmtzco description: Integrating AVCaptureVideoPreviewLayer into SwiftUI using UIViewRepresentable. Use this skill when building camera preview interfaces in SwiftUI, implementing real-time video capture views, adding pinch-to-zoom or gesture-based camera controls, or handling camera orientation properly.

Skill: SwiftUI-AVFoundation Camera Bridge

Integrating AVCaptureVideoPreviewLayer into SwiftUI using UIViewRepresentable, with support for orientation handling, gesture recognition, and proper lifecycle management.

When to Use

  • Building camera preview interfaces in SwiftUI applications
  • Integrating real-time video capture with SwiftUI views
  • Implementing pinch-to-zoom or other gesture-based camera controls
  • Creating camera apps that need proper orientation handling
  • Bridging any AVFoundation preview layer into SwiftUI

Key Concepts

UIViewRepresentable Protocol

The bridge between UIKit and SwiftUI:

  • makeUIView(context:) - Creates the UIKit view once
  • updateUIView(_:context:) - Called when SwiftUI state changes

Coordinator Pattern

Handles UIKit delegate patterns and gesture recognition:

  • Created once via makeCoordinator()
  • Persists across view updates
  • Ideal for gesture recognizer targets

iOS 17+ Orientation API

The modern videoRotationAngle property (CGFloat degrees) replaces deprecated videoOrientation:

  • = Landscape Right
  • 90° = Portrait
  • 180° = Landscape Left
  • 270° = Portrait Upside Down

Implementation Guide

Step 1: Create the Custom UIView Subclass

import UIKit
import AVFoundation

class CameraPreviewUIView: UIView {
    var previewLayer: AVCaptureVideoPreviewLayer? {
        didSet {
            oldValue?.removeFromSuperlayer()
            if let layer = previewLayer {
                self.layer.addSublayer(layer)
                setNeedsLayout()
            }
        }
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        previewLayer?.frame = bounds
        updatePreviewOrientation()
    }
    
    func updatePreviewOrientation() {
        guard let connection = previewLayer?.connection else { return }
        
        let interfaceOrientation = window?.windowScene?.interfaceOrientation ?? .portrait
        let angle = interfaceOrientation.videoRotationAngle
        
        if connection.isVideoRotationAngleSupported(angle) {
            connection.videoRotationAngle = angle
        }
    }
}

extension UIInterfaceOrientation {
    var videoRotationAngle: CGFloat {
        switch self {
        case .portrait: return 90
        case .portraitUpsideDown: return 270
        case .landscapeLeft: return 180
        case .landscapeRight: return 0
        default: return 90
        }
    }
}

Step 2: Implement UIViewRepresentable

import SwiftUI
import AVFoundation

struct CameraPreviewView: UIViewRepresentable {
    let cameraManager: CameraManager
    var onZoomChange: ((CGFloat, CGFloat) -> Void)?
    
    func makeUIView(context: Context) -> CameraPreviewUIView {
        let view = CameraPreviewUIView()
        view.previewLayer = cameraManager.makePreviewLayer()
        
        // Add pinch gesture targeting the coordinator
        let pinchGesture = UIPinchGestureRecognizer(
            target: context.coordinator,
            action: #selector(Coordinator.handlePinch(_:))
        )
        view.addGestureRecognizer(pinchGesture)
        
        return view
    }
    
    func updateUIView(_ uiView: CameraPreviewUIView, context: Context) {
        uiView.updatePreviewOrientation()
        context.coordinator.onZoomChange = onZoomChange
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(cameraManager: cameraManager, onZoomChange: onZoomChange)
    }
}

Step 3: Implement the Coordinator

extension CameraPreviewView {
    class Coordinator: NSObject {
        let cameraManager: CameraManager
        private var baseZoomFactor: CGFloat = 1.0
        var onZoomChange: ((CGFloat, CGFloat) -> Void)?
        
        init(cameraManager: CameraManager, onZoomChange: ((CGFloat, CGFloat) -> Void)?) {
            self.cameraManager = cameraManager
            self.onZoomChange = onZoomChange
        }
        
        @MainActor @objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {
            switch gesture.state {
            case .began:
                baseZoomFactor = cameraManager.currentZoomFactor
                
            case .changed:
                cameraManager.applyZoomDelta(
                    scale: gesture.scale,
                    baseZoom: baseZoomFactor
                )
                onZoomChange?(
                    cameraManager.currentZoomFactor,
                    cameraManager.maxZoomFactor
                )
                
            case .ended, .cancelled:
                cameraManager.finalizeZoom()
                
            default:
                break
            }
        }
    }
}

Step 4: Usage in SwiftUI

struct CameraScreen: View {
    @StateObject private var cameraManager = CameraManager()
    @State private var currentZoom: CGFloat = 1.0
    
    var body: some View {
        ZStack {
            CameraPreviewView(
                cameraManager: cameraManager,
                onZoomChange: { current, max in
                    currentZoom = current
                }
            )
            .ignoresSafeArea()
            
            VStack {
                Spacer()
                Text(String(format: "%.1fx", currentZoom))
                    .padding()
                    .background(.ultraThinMaterial)
                    .cornerRadius(8)
            }
        }
        .onAppear { cameraManager.startSession() }
        .onDisappear { cameraManager.stopSession() }
    }
}

Common Pitfalls

1. Layer Frame Not Updating

Problem: Preview layer doesn't resize with the view. Solution: Update previewLayer.frame = bounds in layoutSubviews().

2. Gesture Target Deallocated

Problem: Gesture recognizer stops working after view updates. Solution: Use the Coordinator as the gesture target—it persists across updates.

3. Orientation Not Updating

Problem: Preview doesn't rotate with device. Solution: Call updatePreviewOrientation() in both layoutSubviews() AND updateUIView().

4. Thread Safety Violations

Problem: Crashes from wrong thread access. Solution: Mark gesture handlers with @MainActor.

5. Using Deprecated API

Problem: videoOrientation property deprecated in iOS 17. Solution: Use videoRotationAngle with isVideoRotationAngleSupported(_:) check.

6. Zoom Beyond Device Limits

Problem: Crashes when setting zoom factor outside valid range. Solution: Clamp zoom between 1.0 and device.maxAvailableVideoZoomFactor.

Best Practices Summary

AspectBest Practice
Layer ManagementCustom UIView subclass with didSet observer
Frame UpdatesOverride layoutSubviews(), update layer frame
Gesture HandlingCoordinator as target, @MainActor annotations
OrientationUse videoRotationAngle (iOS 17+) with support check
Thread Safety@MainActor on all UI-touching methods

References


Derived from CarSeet project - CameraPreviewView.swift

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です