Here is a comprehensive implementation for accessing both the camera and photo gallery in SwiftUI. Here’s a breakdown of the key components:
Main Features
- Access to photo gallery using PhotosPicker (modern API)
- Camera access with AVFoundation
- Permission handling for both camera and photo library.
- User-friendly UI with permission denied screens.
- Flash control for camera.
- Preview of captured/selected images.
import SwiftUI
import PhotosUI
import AVFoundation
// MARK: - Main View
struct ContentView: View {
@StateObject private var cameraManager = CameraManager()
@State private var showImagePicker = false
@State private var showCameraView = false
@State private var selectedImage: UIImage?
var body: some View {
VStack(spacing: 20) {
if let selectedImage {
Image(uiImage: selectedImage)
.resizable()
.scaledToFit()
.frame(height: 300)
.clipShape(RoundedRectangle(cornerRadius: 10))
} else {
Image(systemName: "photo.fill")
.resizable()
.scaledToFit()
.frame(height: 200)
.foregroundColor(.gray.opacity(0.5))
}
HStack(spacing: 30) {
Button {
showImagePicker = true
} label: {
VStack {
Image(systemName: "photo.on.rectangle")
.font(.system(size: 30))
Text("Gallery")
}
.frame(width: 120)
.padding()
.background(Color.blue.opacity(0.1))
.cornerRadius(10)
}
Button {
cameraManager.checkPermission { granted in
if granted {
showCameraView = true
}
}
} label: {
VStack {
Image(systemName: "camera.fill")
.font(.system(size: 30))
Text("Camera")
}
.frame(width: 120)
.padding()
.background(Color.blue.opacity(0.1))
.cornerRadius(10)
}
}
}
.padding()
.sheet(isPresented: $showImagePicker) {
PhotoPickerView(selectedImage: $selectedImage)
}
.sheet(isPresented: $showCameraView) {
CameraView(image: $selectedImage, cameraManager: cameraManager)
}
}
}
// MARK: - Photo Gallery Access
struct PhotoPickerView: View {
@Binding var selectedImage: UIImage?
@Environment(\.dismiss) var dismiss
@State private var photosPermission: Bool?
var body: some View {
NavigationView {
VStack {
if let photosPermission, !photosPermission {
PermissionDeniedView(
message: "Access to your photo library is needed to select photos.",
icon: "photo.fill.on.rectangle.fill"
)
} else {
PhotosPicker(selection: $selectedImageItem, matching: .images) {
Text("Browse Photos")
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
.padding()
}
}
}
.navigationTitle("Select a Photo")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Cancel") {
dismiss()
}
}
}
.onAppear {
checkPhotosPermission()
}
}
}
@State private var selectedImageItem: PhotosPickerItem?
private func checkPhotosPermission() {
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
switch status {
case .authorized, .limited:
photosPermission = true
case .denied, .restricted:
photosPermission = false
case .notDetermined:
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
DispatchQueue.main.async {
photosPermission = status == .authorized || status == .limited
}
}
@unknown default:
photosPermission = false
}
}
private var imageSelection: Binding<PhotosPickerItem?> {
Binding {
selectedImageItem
} set: { newValue in
selectedImageItem = newValue
if let newValue {
Task {
if let data = try? await newValue.loadTransferable(type: Data.self),
let uiImage = UIImage(data: data) {
DispatchQueue.main.async {
selectedImage = uiImage
dismiss()
}
}
}
}
}
}
}
// MARK: - Camera Manager
class CameraManager: NSObject, ObservableObject, AVCapturePhotoCaptureDelegate {
@Published var session = AVCaptureSession()
@Published var output = AVCapturePhotoOutput()
@Published var preview: AVCaptureVideoPreviewLayer?
@Published var isTaken = false
@Published var flashMode: AVCaptureDevice.FlashMode = .off
var completionHandler: ((UIImage?) -> Void)?
override init() {
super.init()
checkPermission { _ in }
}
func checkPermission(completion: @escaping (Bool) -> Void) {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
setupSession()
completion(true)
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
if granted {
DispatchQueue.main.async {
self?.setupSession()
completion(true)
}
} else {
DispatchQueue.main.async {
completion(false)
}
}
}
case .denied, .restricted:
completion(false)
@unknown default:
completion(false)
}
}
func setupSession() {
do {
self.session.beginConfiguration()
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
return
}
let input = try AVCaptureDeviceInput(device: device)
if self.session.canAddInput(input) {
self.session.addInput(input)
}
if self.session.canAddOutput(self.output) {
self.session.addOutput(self.output)
}
self.session.commitConfiguration()
} catch {
print("Error setting up camera: \(error.localizedDescription)")
}
}
func takePicture(completion: @escaping (UIImage?) -> Void) {
self.completionHandler = completion
let settings = AVCapturePhotoSettings()
settings.flashMode = self.flashMode
self.output.capturePhoto(with: settings, delegate: self)
DispatchQueue.main.async {
withAnimation {
self.isTaken = true
}
}
}
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
if let error = error {
print("Error capturing photo: \(error.localizedDescription)")
self.completionHandler?(nil)
return
}
guard let imageData = photo.fileDataRepresentation(),
let image = UIImage(data: imageData) else {
self.completionHandler?(nil)
return
}
self.completionHandler?(image)
}
func resetCamera() {
DispatchQueue.main.async {
withAnimation {
self.isTaken = false
}
}
}
func toggleFlash() {
self.flashMode = self.flashMode == .off ? .on : .off
}
}
// MARK: - Camera View
struct CameraView: View {
@Binding var image: UIImage?
@ObservedObject var cameraManager: CameraManager
@Environment(\.dismiss) var dismiss
@State private var showFlash = false
var body: some View {
ZStack {
if let preview = cameraManager.preview {
CameraPreview(preview: preview)
.ignoresSafeArea()
} else {
Color.black
.ignoresSafeArea()
.onAppear {
setupPreview()
}
}
VStack {
Spacer()
HStack {
Button {
cameraManager.toggleFlash()
showFlash = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
showFlash = false
}
} label: {
Image(systemName: cameraManager.flashMode == .on ? "bolt.fill" : "bolt.slash.fill")
.font(.system(size: 24))
.foregroundColor(.white)
.padding()
}
Spacer()
Button {
cameraManager.takePicture { capturedImage in
if let capturedImage = capturedImage {
image = capturedImage
dismiss()
}
}
} label: {
Circle()
.strokeBorder(Color.white, lineWidth: 3)
.frame(width: 70, height: 70)
.background(Circle().fill(Color.white.opacity(0.2)))
}
Spacer()
Button {
dismiss()
} label: {
Image(systemName: "xmark")
.font(.system(size: 24))
.foregroundColor(.white)
.padding()
}
}
.padding(.bottom, 30)
}
if showFlash {
VStack {
Text(cameraManager.flashMode == .on ? "Flash: On" : "Flash: Off")
.foregroundColor(.white)
.padding(8)
.background(Color.black.opacity(0.7))
.cornerRadius(8)
Spacer()
}
.padding(.top, 50)
}
}
.onAppear {
DispatchQueue.global(qos: .background).async {
cameraManager.session.startRunning()
}
}
.onDisappear {
cameraManager.session.stopRunning()
}
}
func setupPreview() {
let preview = AVCaptureVideoPreviewLayer(session: cameraManager.session)
preview.videoGravity = .resizeAspectFill
cameraManager.preview = preview
}
}
// MARK: - UIViewRepresentable for Camera Preview
struct CameraPreview: UIViewRepresentable {
var preview: AVCaptureVideoPreviewLayer
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: UIScreen.main.bounds)
preview.frame = view.frame
view.layer.addSublayer(preview)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
// MARK: - Permission Denied View
struct PermissionDeniedView: View {
let message: String
let icon: String
var body: some View {
VStack(spacing: 20) {
Image(systemName: icon)
.font(.system(size: 60))
.foregroundColor(.gray)
Text("Permission Required")
.font(.title2)
.fontWeight(.bold)
Text(message)
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
Button {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
} label: {
Text("Open Settings")
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
.padding(.horizontal)
}
.padding()
}
}
// MARK: - Preview
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Permission Texts in Info.plist
You’ll need to add these permission strings to your Info.plist file:
<!-- Camera Permission -->
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to take photos.</string>
<!-- Photo Library Permission -->
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to your photo library to select photos.</string>
Implementation Details
- ContentView: The main view with buttons for accessing camera or gallery
- PhotoPickerView: Handles photo library access using PhotosPicker API.
- CameraManager: Manages camera setup, permissions, and photo capture.
- CameraView: UI for the camera interface with capture button and flash toggle.
- Permission UI: Dedicated views for permission-denied scenarios.
Key Techniques Used
PHPhotoLibrary.requestAuthorization
for photo library permissions.AVCaptureDevice.requestAccess
for camera permissions.AVCaptureSession
for managing the camera capture process.- SwiftUI’s
.sheet
modifier for presenting modals. - PhotosPicker for modern photo selection.
- Environment values for dismissing sheets.
- Error handling for both permission and capture processes.
This implementation follows best practices for iOS development, including proper permission handling, clean architecture separation, and modern SwiftUI APIs where available.
Thanks for reading!