A modern, lightweight Swift Package for iOS camera functionality with robust orientation handling and optional SwiftUI UI components.
- π₯ Simple Camera Service - Easy-to-use async/await camera operations
- πΈ Photo Capture - High-quality photo capture with metadata preservation
- π Session Management - Automatic session lifecycle management
- π‘οΈ Permission Handling - Built-in camera permission checking
β οΈ Error Handling - Comprehensive error types with localized descriptions
- π§ Core Motion Integration - Accurate device orientation tracking using accelerometer
- π± Discrete & Continuous Rotation - Support for both step-based and smooth rotation
- π― Capture Orientation - Proper EXIF metadata for photo orientation
- π Real-time Updates - Combine publishers for reactive orientation updates
- π¨ SwiftUI Components - Ready-to-use UI elements
- π Device Rotation Modifier - Rotate UI elements while keeping app locked to portrait
- β³ Loading Overlay - Customizable loading spinner component
- π± Native iOS Behavior - Mimics the native Camera app's interface rotation
- iOS 16.0+
- Swift 5.9+
- Xcode 15.0+
Add ASimpleCameraKit to your project using Xcode:
- File β Add Package Dependencies
- Enter the repository URL:
https://github.com/yourusername/ASimpleCameraKit - Select the version you want to use
Or add it to your Package.swift:
dependencies: [
.package(url: "https://github.com/yourusername/ASimpleCameraKit", from: "1.0.0")
]Then add the products to your target:
.target(
name: "YourApp",
dependencies: [
.product(name: "ASimpleCameraKit", package: "ASimpleCameraKit"),
.product(name: "ASimpleCameraKitUI", package: "ASimpleCameraKit"), // Optional UI components
]
)import SwiftUI
import ASimpleCameraKit
struct CameraView: View {
@StateObject private var cameraService = CameraService()
@State private var showingError = false
var body: some View {
ZStack {
// Camera preview
CameraPreviewView(session: cameraService.captureSession)
.ignoresSafeArea()
VStack {
Spacer()
// Capture button
Button {
Task {
do {
try await cameraService.capturePhoto()
} catch {
showingError = true
}
}
} label: {
Circle()
.fill(.white)
.frame(width: 70, height: 70)
.overlay(
Circle()
.stroke(.black, lineWidth: 2)
.frame(width: 60, height: 60)
)
}
.padding(.bottom, 50)
}
}
.task {
do {
try await cameraService.prepareSession()
await cameraService.startSession()
} catch {
showingError = true
}
}
.cameraErrorAlert(
isPresented: $showingError,
error: cameraService.error.value
)
}
}
// Camera preview view using AVCaptureVideoPreviewLayer
struct CameraPreviewView: UIViewRepresentable {
let session: AVCaptureSession
func makeUIView(context: Context) -> UIView {
let view = UIView()
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
if let layer = uiView.layer.sublayers?.first as? AVCaptureVideoPreviewLayer {
layer.frame = uiView.bounds
}
}
}The camera service provides a Combine publisher for captured images:
struct CapturedImageView: View {
@StateObject private var cameraService = CameraService()
@State private var capturedImage: UIImage?
var body: some View {
VStack {
if let image = capturedImage {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
Button("Clear") {
cameraService.clearCapturedImage()
}
} else {
Text("No image captured")
}
}
.onReceive(cameraService.capturedImage) { image in
capturedImage = image
}
}
}ASimpleCameraKit also provides an optional set of SwiftUI components to build a robust camera interface quickly. To use them, add the ASimpleCameraKitUI product to your target dependencies.
import ASimpleCameraKitUIIn apps that are locked to a specific orientation (e.g., portrait), you may still want certain UI elements to rotate to match the physical orientation of the device, just like the native iOS Camera app. You can achieve this with the .rotatesWithDevice() modifier.
Usage
This modifier requires an instance of the OrientationManager from the core ASimpleCameraKit library and supports two rotation styles:
.discrete(default) - Rotates in 90-degree steps.continuous- Rotates smoothly with device movement
import SwiftUI
import ASimpleCameraKit
import ASimpleCameraKitUI
struct MyCameraView: View {
// Get the orientation manager from your camera service
@StateObject private var cameraService = CameraService()
var body: some View {
ZStack {
// Your main, non-rotating content here...
CameraPreviewView(session: cameraService.captureSession)
.ignoresSafeArea()
VStack {
Spacer()
// These buttons will rotate with the device
HStack {
Button("Flash") { /* toggle flash */ }
Button("Grid") { /* toggle grid */ }
Button("Timer") { /* set timer */ }
}
.padding()
.background(.thinMaterial)
.cornerRadius(8)
.rotatesWithDevice(
orientationManager: cameraService.orientationManager,
style: .discrete // or .continuous for smooth rotation
)
Spacer()
}
}
.task {
do {
try await cameraService.prepareSession()
await cameraService.startSession()
} catch {
// Handle setup errors
}
}
}
}A customizable loading spinner component for displaying processing states.
struct ProcessingView: View {
@State private var isProcessing = false
var body: some View {
ZStack {
// Your main content
if isProcessing {
LoadingOverlay(
message: "Processing photo...",
backgroundOpacity: 0.7
)
}
}
}
}struct CameraViewWithCustomErrors: View {
@StateObject private var cameraService = CameraService()
@State private var errorMessage: String?
@State private var showingError = false
var body: some View {
// ... camera UI
.onReceive(cameraService.error) { error in
if let error = error {
errorMessage = error.localizedDescription
showingError = true
}
}
.alert("Camera Error", isPresented: $showingError) {
Button("OK") { }
Button("Retry") {
Task {
try? await cameraService.prepareSession()
await cameraService.startSession()
}
}
} message: {
Text(errorMessage ?? "Unknown error")
}
}
}struct OrientationAwareView: View {
@StateObject private var cameraService = CameraService()
@State private var currentOrientation: UIDeviceOrientation = .portrait
var body: some View {
VStack {
Text("Current orientation: \(currentOrientation.name)")
// Your camera interface
}
.onReceive(cameraService.orientationManager.$currentOrientation) { orientation in
currentOrientation = orientation
}
}
}struct CameraLifecycleView: View {
@StateObject private var cameraService = CameraService()
@State private var isSessionRunning = false
var body: some View {
VStack {
if isSessionRunning {
Text("Camera is running")
.foregroundColor(.green)
} else {
Text("Camera is stopped")
.foregroundColor(.red)
}
Button(isSessionRunning ? "Stop Camera" : "Start Camera") {
Task {
if isSessionRunning {
await cameraService.stopSession()
} else {
do {
try await cameraService.prepareSession()
await cameraService.startSession()
} catch {
// Handle errors
}
}
}
}
}
.onReceive(cameraService.isSessionRunning) { running in
isSessionRunning = running
}
}
}The main service class that manages camera operations.
captureSession: AVCaptureSession- The active capture sessioncapturedImage: CurrentValueSubject<UIImage?, Never>- Publisher for captured imagesdeviceOrientation: CurrentValueSubject<UIDeviceOrientation, Never>- Publisher for device orientationisSessionRunning: CurrentValueSubject<Bool, Never>- Publisher for session stateerror: CurrentValueSubject<Error?, Never>- Publisher for errorsorientationManager: OrientationManager- Orientation tracking manager
checkPermissions() async -> Bool- Check camera permissionsprepareSession() async throws- Setup camera session with permission checkingstartSession() async- Start the camera sessionstopSession() async- Stop the camera sessioncapturePhoto() async throws- Capture a photoclearCapturedImage()- Clear the captured imagegetCurrentOrientation() -> UIDeviceOrientation- Get current orientation
Manages device orientation using Core Motion for accuracy.
currentOrientation: UIDeviceOrientation- Current device orientationlastValidOrientation: UIDeviceOrientation- Last valid capture orientationcontinuousAngleRadians: Double- Continuous rotation angle in radians for smooth animationscaptureOrientation: UIDeviceOrientation- Orientation to use for photo capturevideoOrientation: AVCaptureVideoOrientation- Video orientation for AVFoundation
startTracking()- Start orientation tracking (called automatically)stopTracking()- Stop orientation tracking
Enumeration of possible camera errors.
.cameraUnavailable- Camera hardware is not available.cannotAddInput- Cannot add camera input to session.cannotAddOutput- Cannot add photo output to session.photoCaptureFailed- Photo capture operation failed.unknownError- An unknown error occurred.accessDenied- Camera access permission denied
- Use prepareSession() before starting the camera session - it handles permissions and setup
- Handle errors gracefully using the provided error types and alert modifier
- Use orientation manager from camera service for consistent orientation handling
- Stop sessions when the camera view disappears to save battery
- Clear captured images when no longer needed to free memory
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Run tests:
tests/ - Commit changes:
git commit -m 'Add amazing feature' - Push to branch:
git push origin feature/amazing-feature - Open a Pull Request
MIT License - see LICENSE file for details.
- Documentation: Check the inline code documentation
- Issues: Open GitHub issues for bugs and feature requests
- Discussions: Use GitHub Discussions for questions and ideas