Files
daymade 4e3a54175e Release v1.18.0: Add iOS-APP-developer and promptfoo-evaluation skills
### Added
- **New Skill**: iOS-APP-developer v1.1.0 - iOS development with XcodeGen, SwiftUI, and SPM
  - XcodeGen project.yml configuration
  - SPM dependency resolution
  - Device deployment and code signing
  - Camera/AVFoundation debugging
  - iOS version compatibility handling
  - Library not loaded @rpath framework error fixes
  - State machine testing patterns for @MainActor classes
  - Bundled references: xcodegen-full.md, camera-avfoundation.md, swiftui-compatibility.md, testing-mainactor.md

- **New Skill**: promptfoo-evaluation v1.0.0 - LLM evaluation framework using Promptfoo
  - Promptfoo configuration (promptfooconfig.yaml)
  - Python custom assertions
  - llm-rubric for LLM-as-judge evaluations
  - Few-shot example management
  - Model comparison and prompt testing
  - Bundled reference: promptfoo_api.md

### Changed
- Updated marketplace version from 1.16.0 to 1.18.0
- Updated marketplace skills count from 23 to 25
- Updated skill-creator to v1.2.2:
  - Fixed best practices documentation URL (platform.claude.com)
  - Enhanced quick_validate.py to exclude file:// prefixed paths from validation
- Updated marketplace.json metadata description to include new skills

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 17:23:08 +08:00

8.1 KiB

Camera / AVFoundation Reference

Camera Preview Implementation

Complete Working Example

import SwiftUI
import AVFoundation
import os

private let logger = Logger(subsystem: "com.app", category: "Camera")

// MARK: - Session Manager

@MainActor
final class CameraSessionManager: ObservableObject {
    @Published private(set) var isRunning = false
    @Published private(set) var error: CameraError?

    let session = AVCaptureSession()
    private var videoInput: AVCaptureDeviceInput?

    enum CameraError: LocalizedError {
        case noCamera
        case setupFailed(String)
        case permissionDenied

        var errorDescription: String? {
            switch self {
            case .noCamera: return "No camera available"
            case .setupFailed(let reason): return "Setup failed: \(reason)"
            case .permissionDenied: return "Camera permission denied"
            }
        }
    }

    func start() async {
        logger.info("start() called, isRunning=\(self.isRunning)")
        guard !isRunning else { return }

        // Check permission
        guard await requestPermission() else {
            error = .permissionDenied
            return
        }

        // Get camera
        guard let device = AVCaptureDevice.default(
            .builtInWideAngleCamera,
            for: .video,
            position: .front
        ) else {
            logger.error("No front camera available")
            error = .noCamera
            return
        }

        // Configure session
        session.beginConfiguration()
        session.sessionPreset = .high

        do {
            let input = try AVCaptureDeviceInput(device: device)
            if session.canAddInput(input) {
                session.addInput(input)
                videoInput = input
            }
            session.commitConfiguration()

            // Start on background thread
            await withCheckedContinuation { continuation in
                DispatchQueue.global(qos: .userInitiated).async { [weak self] in
                    self?.session.startRunning()
                    DispatchQueue.main.async {
                        self?.isRunning = true
                        logger.info("Camera session started")
                        continuation.resume()
                    }
                }
            }
        } catch {
            session.commitConfiguration()
            self.error = .setupFailed(error.localizedDescription)
        }
    }

    func stop() {
        guard isRunning else { return }
        DispatchQueue.global(qos: .userInitiated).async { [weak self] in
            self?.session.stopRunning()
            DispatchQueue.main.async {
                self?.isRunning = false
            }
        }
    }

    private func requestPermission() async -> Bool {
        switch AVCaptureDevice.authorizationStatus(for: .video) {
        case .authorized: return true
        case .notDetermined:
            return await AVCaptureDevice.requestAccess(for: .video)
        default: return false
        }
    }
}

// MARK: - SwiftUI View

struct CameraPreviewView: UIViewRepresentable {
    let session: AVCaptureSession

    func makeUIView(context: Context) -> CameraPreviewUIView {
        let view = CameraPreviewUIView()
        view.backgroundColor = .black
        view.session = session
        return view
    }

    func updateUIView(_ uiView: CameraPreviewUIView, context: Context) {}
}

final class CameraPreviewUIView: UIView {
    override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }

    var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer }

    var session: AVCaptureSession? {
        get { previewLayer.session }
        set {
            previewLayer.session = newValue
            previewLayer.videoGravity = .resizeAspectFill
            configureMirroring()
        }
    }

    private func configureMirroring() {
        guard let connection = previewLayer.connection,
              connection.isVideoMirroringSupported else { return }
        // CRITICAL: Must disable automatic adjustment BEFORE setting manual mirroring
        // Without this, iOS throws: "Cannot be set when automaticallyAdjustsVideoMirroring is YES"
        connection.automaticallyAdjustsVideoMirroring = false
        connection.isVideoMirrored = true
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        previewLayer.frame = bounds
    }
}

// MARK: - Usage in SwiftUI

struct ContentView: View {
    @StateObject private var cameraManager = CameraSessionManager()

    var body: some View {
        ZStack {
            // CRITICAL: Use GeometryReader for proper sizing
            GeometryReader { geo in
                CameraPreviewView(session: cameraManager.session)
                    .frame(width: geo.size.width, height: geo.size.height)
            }
            .ignoresSafeArea()

            // Overlay content here
        }
        .onAppear {
            Task { await cameraManager.start() }
        }
        .onDisappear {
            cameraManager.stop()
        }
    }
}

Common Issues and Solutions

Issue: Camera preview shows nothing

Debug steps:

  1. Check if running on simulator (camera not available):
#if targetEnvironment(simulator)
logger.warning("Camera not available on simulator")
#endif
  1. Add logging to trace execution:
logger.info("Permission status: \(AVCaptureDevice.authorizationStatus(for: .video).rawValue)")
logger.info("Session running: \(session.isRunning)")
logger.info("Preview layer bounds: \(previewLayer.bounds)")
  1. Verify Info.plist has camera permission:
<key>NSCameraUsageDescription</key>
<string>Camera access for preview</string>

Issue: UIViewRepresentable has zero size

Cause: In ZStack, UIViewRepresentable doesn't expand like SwiftUI views.

Solution: Wrap in GeometryReader with explicit frame:

GeometryReader { geo in
    CameraPreviewView(session: session)
        .frame(width: geo.size.width, height: geo.size.height)
}

Issue: Preview layer connection is nil

Cause: Connection isn't established until session is running and layer is in view hierarchy.

Solution: Configure mirroring in layoutSubviews:

override func layoutSubviews() {
    super.layoutSubviews()
    previewLayer.frame = bounds
    // Retry mirroring here
    configureMirroring()
}

private func configureMirroring() {
    guard let conn = previewLayer.connection,
          conn.isVideoMirroringSupported else { return }
    conn.automaticallyAdjustsVideoMirroring = false
    conn.isVideoMirrored = true
}

Issue: Crash on setVideoMirrored

Error: *** -[AVCaptureConnection setVideoMirrored:] Cannot be set when automaticallyAdjustsVideoMirroring is YES

Cause: iOS automatically adjusts mirroring by default. Setting isVideoMirrored while automatic adjustment is enabled throws an exception.

Solution: Always disable automatic adjustment first:

// WRONG - crashes on some devices
connection.isVideoMirrored = true

// CORRECT - disable automatic first
connection.automaticallyAdjustsVideoMirroring = false
connection.isVideoMirrored = true

Affected Devices: Primarily older devices (iPhone X, etc.) but can affect any device.

Issue: Swift 6 concurrency errors with AVCaptureSession

Error: "cannot access property 'session' with non-Sendable type from nonisolated deinit"

Solution: Don't access session in deinit. Use explicit stop() call:

deinit {
    // Don't access session here
    // Cleanup handled by stop() call from view
}

Debugging with Console.app

  1. Open Console.app
  2. Select your device
  3. Filter by:
    • Subsystem: com.yourapp
    • Category: Camera
  4. Look for the log sequence:
    start() called, isRunning=false
    Permission granted
    Found front camera: Front Camera
    Camera session started
    

Camera + Audio Conflict

If using AudioKit or AVAudioEngine, camera audio input may conflict.

Solution: Use video-only input, no audio:

// Only add video input, skip audio
let videoInput = try AVCaptureDeviceInput(device: videoDevice)
session.addInput(videoInput)
// Do NOT add audio input