photos-camera-media by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill photos-camera-media针对 iOS 26+ 和 Swift 6.2 的现代照片选取、相机拍摄、图像加载和媒体权限模式。除非特别说明,这些模式向后兼容至 iOS 16。
完整的选取器使用示例请参见 references/photospicker-patterns.md,AVCaptureSession 模式请参见 references/camera-capture.md。
PhotosPicker 是用于替代 UIImagePickerController 的原生 SwiftUI 组件。它在进程外运行,浏览时无需照片库权限,并支持通过媒体类型过滤进行单选或多选。
import SwiftUI
import PhotosUI
struct SinglePhotoPicker: View {
@State private var selectedItem: PhotosPickerItem?
@State private var selectedImage: Image?
var body: some View {
VStack {
if let selectedImage {
selectedImage
.resizable()
.scaledToFit()
.frame(maxHeight: 300)
}
PhotosPicker("选择照片", selection: $selectedItem, matching: .images)
}
.onChange(of: selectedItem) { _, newItem in
Task {
if let data = try? await newItem?.loadTransferable(type: Data.self),
let uiImage = UIImage(data: data) {
selectedImage = Image(uiImage: uiImage)
}
}
}
}
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
struct MultiPhotoPicker: View {
@State private var selectedItems: [PhotosPickerItem] = []
@State private var selectedImages: [Image] = []
var body: some View {
VStack {
ScrollView(.horizontal) {
HStack {
ForEach(selectedImages.indices, id: \.self) { index in
selectedImages[index]
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
}
PhotosPicker(
"选择照片",
selection: $selectedItems,
maxSelectionCount: 5,
matching: .images
)
}
.onChange(of: selectedItems) { _, newItems in
Task {
selectedImages = []
for item in newItems {
if let data = try? await item.loadTransferable(type: Data.self),
let uiImage = UIImage(data: data) {
selectedImages.append(Image(uiImage: uiImage))
}
}
}
}
}
}
使用 PHPickerFilter 组合进行过滤,以限制可选择的媒体:
// 仅图片
PhotosPicker(selection: $items, matching: .images)
// 仅视频
PhotosPicker(selection: $items, matching: .videos)
// 仅实况照片
PhotosPicker(selection: $items, matching: .livePhotos)
// 仅屏幕截图
PhotosPicker(selection: $items, matching: .screenshots)
// 图片和视频组合
PhotosPicker(selection: $items, matching: .any(of: [.images, .videos]))
// 图片(排除屏幕截图)
PhotosPicker(selection: $items, matching: .all(of: [.images, .not(.screenshots)]))
PhotosPickerItem 通过 loadTransferable(type:) 异步加载内容。定义一个 Transferable 类型以进行自动解码:
struct PickedImage: Transferable {
let data: Data
let image: Image
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(importedContentType: .image) { data in
guard let uiImage = UIImage(data: data) else {
throw TransferError.importFailed
}
return PickedImage(data: data, image: Image(uiImage: uiImage))
}
}
}
enum TransferError: Error {
case importFailed
}
// 用法
if let picked = try? await item.loadTransferable(type: PickedImage.self) {
selectedImage = picked.image
}
始终在 Task 中加载以避免阻塞主线程。处理 nil 返回值和抛出的错误——用户可能选择无法解码的格式。
iOS 为照片库提供两种访问级别。当应用请求 .readWrite 访问权限时,系统会自动呈现有限库选取器——用户选择要共享的照片。
| 访问级别 | 描述 | Info.plist 键 |
|---|---|---|
| 仅添加 | 无需读取即可向库写入照片 | NSPhotoLibraryAddUsageDescription |
| 读写 | 完全或有限的读取权限加上写入权限 | NSPhotoLibraryUsageDescription |
PhotosPicker 浏览时无需权限——它在进程外运行,并且仅授予对选定项的访问权限。仅当需要读取整个库(例如,自定义相册)或保存照片时,才请求显式权限。
import Photos
func requestPhotoLibraryAccess() async -> PHAuthorizationStatus {
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
switch status {
case .notDetermined:
return await PHPhotoLibrary.requestAuthorization(for: .readWrite)
case .authorized, .limited:
return status
case .denied, .restricted:
return status
@unknown default:
return status
}
}
在 Info.plist 中添加 NSCameraUsageDescription。在配置捕获会话之前检查并请求访问权限:
import AVFoundation
func requestCameraAccess() async -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .notDetermined:
return await AVCaptureDevice.requestAccess(for: .video)
case .authorized:
return true
case .denied, .restricted:
return false
@unknown default:
return false
}
}
当用户拒绝访问时,引导他们前往设置。切勿重复提示或静默隐藏功能。
struct PermissionDeniedView: View {
let message: String
@Environment(\.openURL) private var openURL
var body: some View {
ContentUnavailableView {
Label("访问被拒绝", systemImage: "lock.shield")
} description: {
Text(message)
} actions: {
Button("打开设置") {
if let url = URL(string: UIApplication.openSettingsURLString) {
openURL(url)
}
}
}
}
}
| 键 | 何时需要 |
|---|---|
NSPhotoLibraryUsageDescription | 从照片库读取照片 |
NSPhotoLibraryAddUsageDescription | 将照片/视频保存到库 |
NSCameraUsageDescription | 访问相机 |
NSMicrophoneUsageDescription | 录制音频(带声音的视频) |
省略必需的键会在权限对话框即将出现时导致运行时崩溃。
在专用的 @Observable 模型中管理相机会话。可表示视图仅显示预览。完整模式请参见 references/camera-capture.md。
import AVFoundation
@available(iOS 17.0, *)
@Observable
@MainActor
final class CameraManager {
let session = AVCaptureSession()
private let photoOutput = AVCapturePhotoOutput()
private var currentDevice: AVCaptureDevice?
var isRunning = false
var capturedImage: Data?
func configure() async {
guard await requestCameraAccess() else { return }
session.beginConfiguration()
session.sessionPreset = .photo
// 添加相机输入
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera,
for: .video,
position: .back) else { return }
currentDevice = device
guard let input = try? AVCaptureDeviceInput(device: device),
session.canAddInput(input) else { return }
session.addInput(input)
// 添加照片输出
guard session.canAddOutput(photoOutput) else { return }
session.addOutput(photoOutput)
session.commitConfiguration()
}
func start() {
guard !session.isRunning else { return }
Task.detached { [session] in
session.startRunning()
}
isRunning = true
}
func stop() {
guard session.isRunning else { return }
Task.detached { [session] in
session.stopRunning()
}
isRunning = false
}
private func requestCameraAccess() async -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: .video)
if status == .notDetermined {
return await AVCaptureDevice.requestAccess(for: .video)
}
return status == .authorized
}
}
在后台队列上启动和停止 AVCaptureSession。startRunning() 和 stopRunning() 方法是同步的,会阻塞调用线程。
将 AVCaptureVideoPreviewLayer 包装在 UIViewRepresentable 中。重写 layerClass 以实现自动调整大小:
import SwiftUI
import AVFoundation
struct CameraPreview: UIViewRepresentable {
let session: AVCaptureSession
func makeUIView(context: Context) -> PreviewView {
let view = PreviewView()
view.previewLayer.session = session
view.previewLayer.videoGravity = .resizeAspectFill
return view
}
func updateUIView(_ uiView: PreviewView, context: Context) {
if uiView.previewLayer.session !== session {
uiView.previewLayer.session = session
}
}
}
final class PreviewView: UIView {
override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer }
}
struct CameraScreen: View {
@State private var cameraManager = CameraManager()
var body: some View {
ZStack(alignment: .bottom) {
CameraPreview(session: cameraManager.session)
.ignoresSafeArea()
Button {
// 拍摄照片 -- 参见 references/camera-capture.md
} label: {
Circle()
.fill(.white)
.frame(width: 72, height: 72)
.overlay(Circle().stroke(.gray, lineWidth: 3))
}
.padding(.bottom, 32)
}
.task {
await cameraManager.configure()
cameraManager.start()
}
.onDisappear {
cameraManager.stop()
}
}
}
始终在 onDisappear 中调用 stop()。正在运行的捕获会话会独占相机并消耗电量。
AsyncImage(url: imageURL) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image
.resizable()
.scaledToFill()
case .failure:
Image(systemName: "photo")
.foregroundStyle(.secondary)
@unknown default:
EmptyView()
}
}
.frame(width: 200, height: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))
AsyncImage 不会跨视图重绘缓存图像。对于包含许多图像的生产应用,请使用专用的图像加载库或实现基于 URLCache 的缓存。
将库中的全分辨率照片加载到显示尺寸的 CGImage 中,以避免内存峰值。一张 48MP 的照片未压缩时可能占用超过 200 MB 内存。
import ImageIO
import UIKit
func downsample(data: Data, to pointSize: CGSize, scale: CGFloat = UITraitCollection.current.displayScale) -> UIImage? {
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
]
guard let source = CGImageSourceCreateWithData(data as CFData, nil),
let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
return nil
}
return UIImage(cgImage: cgImage)
}
在列表、网格或缩略图中显示用户选择的照片时,请使用此方法。在创建 UIImage 之前,将来自 PhotosPickerItem 的原始 Data 直接传递给下采样器。
// Original: 按原样显示图像及其原始颜色
Image("photo")
.renderingMode(.original)
// Template: 将图像视为蒙版,由 foregroundStyle 着色
Image(systemName: "heart.fill")
.renderingMode(.template)
.foregroundStyle(.red)
对照片和艺术作品使用 .original。对应采用当前色调颜色的图标使用 .template。
不要: 使用 UIImagePickerController 进行照片选取。要: 使用 PhotosPicker (SwiftUI) 或 PHPickerViewController (UIKit)。原因: UIImagePickerController 是功能有限的旧版 API。PhotosPicker 在进程外运行,支持多选,且浏览时无需库权限。
不要: 当你只需要用户选取照片时,请求完整的照片库访问权限。要: 使用无需权限的 PhotosPicker,或者请求 .readWrite 并让系统处理有限访问。原因: 对于大多数选取即用工作流,完全访问是不必要的。系统的有限库选取器尊重用户隐私,并且仍然授予对选定项的访问权限。
不要: 将全分辨率图像加载到内存中以生成缩略图。要: 使用带有 kCGImageSourceThumbnailMaxPixelSize 的 CGImageSource 进行下采样。一张 48MP 的图像未压缩时超过 200 MB。
不要: 阻塞主线程加载 PhotosPickerItem 数据。要: 在 Task 中使用 async loadTransferable(type:)。
不要: 忘记在视图消失时停止 AVCaptureSession。要: 在 onDisappear 或 dismantleUIView 中调用 session.stopRunning()。
不要: 假设相机访问权限已授予而不进行检查。要: 检查 AVCaptureDevice.authorizationStatus(for: .video) 并处理 .denied/.restricted 情况。
不要: 在主线程上调用 session.startRunning()。要: 使用 Task.detached 或专用的串行队列调度到后台线程。原因: startRunning() 是一个同步阻塞调用,在硬件初始化时可能需要数百毫秒。
不要: 在 UIViewRepresentable 内部创建 AVCaptureSession。要: 在单独的 @Observable 模型中拥有会话。
PhotosPicker 而非已弃用的 UIImagePickerControllerCGImageSource 对大图像进行下采样onDisappear 中停止AVCaptureSession 由模型拥有,而非在 UIViewRepresentable 内部创建Sendable 的references/photospicker-patterns.md — 选取器模式、媒体加载、HEIC 处理references/camera-capture.md — AVCaptureSession、照片/视频拍摄、二维码扫描references/image-loading-caching.md — AsyncImage、缓存、下采样references/av-playback.md — AVPlayer、流媒体、音频会话、后台音频每周安装量
378
代码库
GitHub 星标数
269
首次出现
2026年3月3日
安全审计
安装于
codex375
cursor372
amp372
cline372
github-copilot372
kimi-cli372
Modern patterns for photo picking, camera capture, image loading, and media permissions targeting iOS 26+ with Swift 6.2. Patterns are backward-compatible to iOS 16 unless noted.
See references/photospicker-patterns.md for complete picker recipes and references/camera-capture.md for AVCaptureSession patterns.
PhotosPicker is the native SwiftUI replacement for UIImagePickerController. It runs out-of-process, requires no photo library permission for browsing, and supports single or multi-selection with media type filtering.
import SwiftUI
import PhotosUI
struct SinglePhotoPicker: View {
@State private var selectedItem: PhotosPickerItem?
@State private var selectedImage: Image?
var body: some View {
VStack {
if let selectedImage {
selectedImage
.resizable()
.scaledToFit()
.frame(maxHeight: 300)
}
PhotosPicker("Select Photo", selection: $selectedItem, matching: .images)
}
.onChange(of: selectedItem) { _, newItem in
Task {
if let data = try? await newItem?.loadTransferable(type: Data.self),
let uiImage = UIImage(data: data) {
selectedImage = Image(uiImage: uiImage)
}
}
}
}
}
struct MultiPhotoPicker: View {
@State private var selectedItems: [PhotosPickerItem] = []
@State private var selectedImages: [Image] = []
var body: some View {
VStack {
ScrollView(.horizontal) {
HStack {
ForEach(selectedImages.indices, id: \.self) { index in
selectedImages[index]
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
}
PhotosPicker(
"Select Photos",
selection: $selectedItems,
maxSelectionCount: 5,
matching: .images
)
}
.onChange(of: selectedItems) { _, newItems in
Task {
selectedImages = []
for item in newItems {
if let data = try? await item.loadTransferable(type: Data.self),
let uiImage = UIImage(data: data) {
selectedImages.append(Image(uiImage: uiImage))
}
}
}
}
}
}
Filter with PHPickerFilter composites to restrict selectable media:
// Images only
PhotosPicker(selection: $items, matching: .images)
// Videos only
PhotosPicker(selection: $items, matching: .videos)
// Live Photos only
PhotosPicker(selection: $items, matching: .livePhotos)
// Screenshots only
PhotosPicker(selection: $items, matching: .screenshots)
// Images and videos combined
PhotosPicker(selection: $items, matching: .any(of: [.images, .videos]))
// Images excluding screenshots
PhotosPicker(selection: $items, matching: .all(of: [.images, .not(.screenshots)]))
PhotosPickerItem loads content asynchronously via loadTransferable(type:). Define a Transferable type for automatic decoding:
struct PickedImage: Transferable {
let data: Data
let image: Image
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(importedContentType: .image) { data in
guard let uiImage = UIImage(data: data) else {
throw TransferError.importFailed
}
return PickedImage(data: data, image: Image(uiImage: uiImage))
}
}
}
enum TransferError: Error {
case importFailed
}
// Usage
if let picked = try? await item.loadTransferable(type: PickedImage.self) {
selectedImage = picked.image
}
Always load in a Task to avoid blocking the main thread. Handle nil returns and thrown errors -- the user may select a format that cannot be decoded.
iOS provides two access levels for the photo library. The system automatically presents the limited-library picker when an app requests .readWrite access -- users choose which photos to share.
| Access Level | Description | Info.plist Key |
|---|---|---|
| Add-only | Write photos to the library without reading | NSPhotoLibraryAddUsageDescription |
| Read-write | Full or limited read access plus write | NSPhotoLibraryUsageDescription |
PhotosPicker requires no permission to browse -- it runs out-of-process and only grants access to selected items. Request explicit permission only when you need to read the full library (e.g., a custom gallery) or save photos.
import Photos
func requestPhotoLibraryAccess() async -> PHAuthorizationStatus {
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
switch status {
case .notDetermined:
return await PHPhotoLibrary.requestAuthorization(for: .readWrite)
case .authorized, .limited:
return status
case .denied, .restricted:
return status
@unknown default:
return status
}
}
Add NSCameraUsageDescription to Info.plist. Check and request access before configuring a capture session:
import AVFoundation
func requestCameraAccess() async -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .notDetermined:
return await AVCaptureDevice.requestAccess(for: .video)
case .authorized:
return true
case .denied, .restricted:
return false
@unknown default:
return false
}
}
When the user denies access, guide them to Settings. Never repeatedly prompt or hide functionality silently.
struct PermissionDeniedView: View {
let message: String
@Environment(\.openURL) private var openURL
var body: some View {
ContentUnavailableView {
Label("Access Denied", systemImage: "lock.shield")
} description: {
Text(message)
} actions: {
Button("Open Settings") {
if let url = URL(string: UIApplication.openSettingsURLString) {
openURL(url)
}
}
}
}
}
| Key | When Required |
|---|---|
NSPhotoLibraryUsageDescription | Reading photos from the library |
NSPhotoLibraryAddUsageDescription | Saving photos/videos to the library |
NSCameraUsageDescription | Accessing the camera |
NSMicrophoneUsageDescription | Recording audio (video with sound) |
Omitting a required key causes a runtime crash when the permission dialog would appear.
Manage camera sessions in a dedicated @Observable model. The representable view only displays the preview. See references/camera-capture.md for complete patterns.
import AVFoundation
@available(iOS 17.0, *)
@Observable
@MainActor
final class CameraManager {
let session = AVCaptureSession()
private let photoOutput = AVCapturePhotoOutput()
private var currentDevice: AVCaptureDevice?
var isRunning = false
var capturedImage: Data?
func configure() async {
guard await requestCameraAccess() else { return }
session.beginConfiguration()
session.sessionPreset = .photo
// Add camera input
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera,
for: .video,
position: .back) else { return }
currentDevice = device
guard let input = try? AVCaptureDeviceInput(device: device),
session.canAddInput(input) else { return }
session.addInput(input)
// Add photo output
guard session.canAddOutput(photoOutput) else { return }
session.addOutput(photoOutput)
session.commitConfiguration()
}
func start() {
guard !session.isRunning else { return }
Task.detached { [session] in
session.startRunning()
}
isRunning = true
}
func stop() {
guard session.isRunning else { return }
Task.detached { [session] in
session.stopRunning()
}
isRunning = false
}
private func requestCameraAccess() async -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: .video)
if status == .notDetermined {
return await AVCaptureDevice.requestAccess(for: .video)
}
return status == .authorized
}
}
Start and stop AVCaptureSession on a background queue. The startRunning() and stopRunning() methods are synchronous and block the calling thread.
Wrap AVCaptureVideoPreviewLayer in a UIViewRepresentable. Override layerClass for automatic resizing:
import SwiftUI
import AVFoundation
struct CameraPreview: UIViewRepresentable {
let session: AVCaptureSession
func makeUIView(context: Context) -> PreviewView {
let view = PreviewView()
view.previewLayer.session = session
view.previewLayer.videoGravity = .resizeAspectFill
return view
}
func updateUIView(_ uiView: PreviewView, context: Context) {
if uiView.previewLayer.session !== session {
uiView.previewLayer.session = session
}
}
}
final class PreviewView: UIView {
override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer }
}
struct CameraScreen: View {
@State private var cameraManager = CameraManager()
var body: some View {
ZStack(alignment: .bottom) {
CameraPreview(session: cameraManager.session)
.ignoresSafeArea()
Button {
// Capture photo -- see references/camera-capture.md
} label: {
Circle()
.fill(.white)
.frame(width: 72, height: 72)
.overlay(Circle().stroke(.gray, lineWidth: 3))
}
.padding(.bottom, 32)
}
.task {
await cameraManager.configure()
cameraManager.start()
}
.onDisappear {
cameraManager.stop()
}
}
}
Always call stop() in onDisappear. A running capture session holds the camera exclusively and drains battery.
AsyncImage(url: imageURL) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image
.resizable()
.scaledToFill()
case .failure:
Image(systemName: "photo")
.foregroundStyle(.secondary)
@unknown default:
EmptyView()
}
}
.frame(width: 200, height: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))
AsyncImage does not cache images across view redraws. For production apps with many images, use a dedicated image loading library or implement URLCache-based caching.
Load full-resolution photos from the library into a display-sized CGImage to avoid memory spikes. A 48MP photo can consume over 200 MB uncompressed.
import ImageIO
import UIKit
func downsample(data: Data, to pointSize: CGSize, scale: CGFloat = UITraitCollection.current.displayScale) -> UIImage? {
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
]
guard let source = CGImageSourceCreateWithData(data as CFData, nil),
let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
return nil
}
return UIImage(cgImage: cgImage)
}
Use this whenever displaying user-selected photos in lists, grids, or thumbnails. Pass the raw Data from PhotosPickerItem directly to the downsampler before creating a UIImage.
// Original: display the image as-is with its original colors
Image("photo")
.renderingMode(.original)
// Template: treat the image as a mask, colored by foregroundStyle
Image(systemName: "heart.fill")
.renderingMode(.template)
.foregroundStyle(.red)
Use .original for photos and artwork. Use .template for icons that should adopt the current tint color.
DON'T: Use UIImagePickerController for photo picking. DO: Use PhotosPicker (SwiftUI) or PHPickerViewController (UIKit). Why: UIImagePickerController is legacy API with limited functionality. PhotosPicker runs out-of-process, supports multi-selection, and requires no library permission for browsing.
DON'T: Request full photo library access when you only need the user to pick photos. DO: Use PhotosPicker which requires no permission, or request .readWrite and let the system handle limited access. Why: Full access is unnecessary for most pick-and-use workflows. The system's limited-library picker respects user privacy and still grants access to selected items.
DON'T: Load full-resolution images into memory for thumbnails. DO: Use CGImageSource with kCGImageSourceThumbnailMaxPixelSize to downsample. A 48MP image is over 200 MB uncompressed.
DON'T: Block the main thread loading PhotosPickerItem data. DO: Use async loadTransferable(type:) in a Task.
DON'T: Forget to stop AVCaptureSession when the view disappears. DO: Call session.stopRunning() in onDisappear or dismantleUIView.
DON'T: Assume camera access is granted without checking. DO: Check AVCaptureDevice.authorizationStatus(for: .video) and handle .denied/.restricted.
DON'T: Call session.startRunning() on the main thread. DO: Dispatch to a background thread with Task.detached or a dedicated serial queue. Why: startRunning() is a synchronous blocking call that can take hundreds of milliseconds while the hardware initializes.
DON'T: Create AVCaptureSession inside a UIViewRepresentable. DO: Own the session in a separate @Observable model.
PhotosPicker used instead of deprecated UIImagePickerControllerCGImageSource before displayonDisappearAVCaptureSession owned by model, not created inside UIViewRepresentableSendable when passed across concurrency boundariesreferences/photospicker-patterns.md — Picker patterns, media loading, HEIC handlingreferences/camera-capture.md — AVCaptureSession, photo/video capture, QR scanningreferences/image-loading-caching.md — AsyncImage, caching, downsamplingreferences/av-playback.md — AVPlayer, streaming, audio session, background audioWeekly Installs
378
Repository
GitHub Stars
269
First Seen
Mar 3, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex375
cursor372
amp372
cline372
github-copilot372
kimi-cli372
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装