This commit is contained in:
fmodf 2024-07-03 13:50:59 +02:00
parent cdccfb9e3e
commit ce85b7dff9
2 changed files with 447 additions and 227 deletions

View file

@ -1,110 +1,110 @@
import AVFoundation // import AVFoundation
import Photos // import Photos
import SwiftUI // import SwiftUI
import UIKit // import UIKit
//
class MediaManager: NSObject, ObservableObject { // class MediaManager: NSObject, ObservableObject {
// @Published var photos: [UIImage] = [] // // @Published var photos: [UIImage] = []
@Published var cameraFeed: UIImage? // @Published var cameraFeed: UIImage?
//
// @Published var galleryAccessLevel: PHAuthorizationStatus = .notDetermined // // @Published var galleryAccessLevel: PHAuthorizationStatus = .notDetermined
@Published var cameraAccessLevel: AVAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) // @Published var cameraAccessLevel: AVAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
//
override init() { // override init() {
super.init() // super.init()
NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) // NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
//
// DispatchQueue.main.async { [weak self] in // // DispatchQueue.main.async { [weak self] in
// // self?.fetchPhotos() // // // self?.fetchPhotos()
// } // // }
} // }
//
// private func fetchPhotos() { // // private func fetchPhotos() {
// galleryAccessLevel = PHPhotoLibrary.authorizationStatus() // // galleryAccessLevel = PHPhotoLibrary.authorizationStatus()
// // //
// let fetchOptions = PHFetchOptions() // // let fetchOptions = PHFetchOptions()
// fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] // // fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
// let assets = PHAsset.fetchAssets(with: .image, options: fetchOptions) // // let assets = PHAsset.fetchAssets(with: .image, options: fetchOptions)
// // //
// let manager = PHImageManager.default() // // let manager = PHImageManager.default()
// let option = PHImageRequestOptions() // // let option = PHImageRequestOptions()
// option.isSynchronous = true // // option.isSynchronous = true
// // //
// assets.enumerateObjects { asset, _, _ in // // assets.enumerateObjects { asset, _, _ in
// manager.requestImage(for: asset, targetSize: CGSize(width: 200, height: 200), contentMode: .aspectFill, options: option) { image, _ in // // manager.requestImage(for: asset, targetSize: CGSize(width: 200, height: 200), contentMode: .aspectFill, options: option) { image, _ in
// if let image = image { // // if let image = image {
// DispatchQueue.main.async { // // DispatchQueue.main.async {
// self.photos.append(image) // // self.photos.append(image)
// } // // }
// } // // }
// } // // }
// } // // }
// } // // }
//
private func setupCameraFeed() { // private func setupCameraFeed() {
cameraAccessLevel = AVCaptureDevice.authorizationStatus(for: .video) // cameraAccessLevel = AVCaptureDevice.authorizationStatus(for: .video)
//
let captureSession = AVCaptureSession() // let captureSession = AVCaptureSession()
captureSession.sessionPreset = .medium // captureSession.sessionPreset = .medium
//
guard let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { // guard let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
print("Unable to access the back camera!") // print("Unable to access the back camera!")
return // return
} // }
//
do { // do {
let input = try AVCaptureDeviceInput(device: backCamera) // let input = try AVCaptureDeviceInput(device: backCamera)
if captureSession.canAddInput(input) { // if captureSession.canAddInput(input) {
captureSession.addInput(input) // captureSession.addInput(input)
} // }
} catch { // } catch {
print("Error Unable to initialize back camera: \(error.localizedDescription)") // print("Error Unable to initialize back camera: \(error.localizedDescription)")
} // }
//
let videoOutput = AVCaptureVideoDataOutput() // let videoOutput = AVCaptureVideoDataOutput()
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue")) // videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue"))
if captureSession.canAddOutput(videoOutput) { // if captureSession.canAddOutput(videoOutput) {
captureSession.addOutput(videoOutput) // captureSession.addOutput(videoOutput)
} // }
//
captureSession.startRunning() // captureSession.startRunning()
} // }
} // }
//
extension MediaManager: AVCaptureVideoDataOutputSampleBufferDelegate { // extension MediaManager: AVCaptureVideoDataOutputSampleBufferDelegate {
func captureOutput(_: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from _: AVCaptureConnection) { // func captureOutput(_: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from _: AVCaptureConnection) {
print("Capturing output started") // print("Capturing output started")
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { // guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return // return
} // }
//
let ciImage = CIImage(cvPixelBuffer: pixelBuffer) // let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
let context = CIContext() // let context = CIContext()
guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else { // guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else {
return // return
} // }
//
DispatchQueue.main.async { // DispatchQueue.main.async {
self.cameraFeed = UIImage(cgImage: cgImage) // self.cameraFeed = UIImage(cgImage: cgImage)
print("Updated camera feed") // print("Updated camera feed")
} // }
} // }
} // }
//
extension MediaManager { // extension MediaManager {
func openAppSettings() { // func openAppSettings() {
if // if
let appSettingsUrl = URL(string: UIApplication.openSettingsURLString), // let appSettingsUrl = URL(string: UIApplication.openSettingsURLString),
UIApplication.shared.canOpenURL(appSettingsUrl) // UIApplication.shared.canOpenURL(appSettingsUrl)
{ // {
UIApplication.shared.open(appSettingsUrl, completionHandler: nil) // UIApplication.shared.open(appSettingsUrl, completionHandler: nil)
} // }
} // }
//
@objc private func appDidBecomeActive() { // @objc private func appDidBecomeActive() {
// Update access levels // // Update access levels
// galleryAccessLevel = PHPhotoLibrary.authorizationStatus() // // galleryAccessLevel = PHPhotoLibrary.authorizationStatus()
cameraAccessLevel = AVCaptureDevice.authorizationStatus(for: .video) // cameraAccessLevel = AVCaptureDevice.authorizationStatus(for: .video)
setupCameraFeed() // setupCameraFeed()
} // }
} // }

View file

@ -1,141 +1,361 @@
import AVFoundation
import Photos
import SwiftUI import SwiftUI
struct AttachmentMediaPickerView: View { struct AttachmentMediaPickerView: View {
@StateObject private var mediaManager = MediaManager() @State private var isCameraAccessGranted = AVCaptureDevice.authorizationStatus(for: .video) == .authorized
@State private var isGalleryAccessGranted = PHPhotoLibrary.authorizationStatus() == .authorized
@State private var images = [UIImage]()
let gridSize = UIScreen.main.bounds.width / 3
var body: some View { var body: some View {
ScrollView { let columns = Array(repeating: GridItem(.flexible(), spacing: 0), count: 3)
LazyVGrid(columns: Array(repeating: .init(.flexible()), count: 3)) {
ForEach(elements) { element in ScrollView(showsIndicators: false) {
element LazyVGrid(columns: columns, spacing: 0) {
// For camera
if isCameraAccessGranted {
ZStack {
CameraView()
.aspectRatio(1, contentMode: .fit)
.frame(maxWidth: .infinity)
Image(systemName: "camera")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
.foregroundColor(.white)
.padding(8)
.background(Color.black.opacity(0.5))
.clipShape(Circle())
.padding(8)
}
} else {
Button {
openAppSettings()
} label: {
ZStack {
Rectangle()
.fill(Color.Main.backgroundLight)
.overlay {
VStack {
Image(systemName: "camera")
.foregroundColor(.Material.tortoiseLight300)
.font(.system(size: 30))
Text("Allow camera access")
.foregroundColor(.Main.black)
.font(.body3)
}
}
.frame(height: 100)
}
}
}
// For pictures
if isGalleryAccessGranted {
ForEach(images.indices, id: \.self) { index in
Image(uiImage: images[index])
.resizable()
.aspectRatio(1, contentMode: .fit)
.frame(maxWidth: .infinity)
.clipped()
}
} else {
Button {
openAppSettings()
} label: {
ZStack {
Rectangle()
.fill(Color.Main.backgroundLight)
.overlay {
VStack {
Image(systemName: "photo")
.foregroundColor(.Material.tortoiseLight300)
.font(.system(size: 30))
Text("Allow gallery access")
.foregroundColor(.Main.black)
.font(.body3)
}
}
.frame(height: 100)
}
}
} }
} }
.padding(.horizontal, 8)
} }
.padding(.vertical, 8) .onAppear {
checkCameraAccess()
checkGalleryAccess()
}
} }
private var elements: [GridElement] { private func checkCameraAccess() {
print("Creating elements") let status = AVCaptureDevice.authorizationStatus(for: .video)
var result: [GridElement] = [] switch status {
case .authorized:
isCameraAccessGranted = true
// camera case .notDetermined:
if let feed = mediaManager.cameraFeed, mediaManager.cameraAccessLevel == .authorized { AVCaptureDevice.requestAccess(for: .video) { granted in
result.append(GridElement(id: UUID(), type: .cameraFeed, content: feed) { DispatchQueue.main.async {
print("Go to capture???") self.isCameraAccessGranted = granted
}) }
print("Added camera feed") }
} else if mediaManager.cameraAccessLevel == .restricted {
result.append(GridElement(id: UUID(), type: .cameraRestricted, content: nil) { case .denied, .restricted:
print("Show alert") isCameraAccessGranted = false
})
print("Added camera restricted") @unknown default:
} else { isCameraAccessGranted = false
result.append(GridElement(id: UUID(), type: .cameraAskButton, content: nil) {
mediaManager.openAppSettings()
})
print("Added camera ask button")
} }
}
// photos private func checkGalleryAccess() {
// if mediaManager.galleryAccessLevel == .restricted { let status = PHPhotoLibrary.authorizationStatus()
// result.append(GridElement(id: UUID(), type: .photoRestricted, content: nil)) switch status {
// } else { case .authorized, .limited:
// for photo in mediaManager.photos { isGalleryAccessGranted = true
// result.append(GridElement(id: UUID(), type: .photo, content: photo)) fetchImages()
// }
// if mediaManager.galleryAccessLevel != .authorized {
// result.append(GridElement(id: UUID(), type: .photoAskButton, content: nil))
// }
// }
case .notDetermined:
PHPhotoLibrary.requestAuthorization { status in
DispatchQueue.main.async {
self.isGalleryAccessGranted = status == .authorized
if self.isGalleryAccessGranted {
self.fetchImages()
}
}
}
case .denied, .restricted:
isGalleryAccessGranted = false
@unknown default:
isGalleryAccessGranted = false
}
}
private func fetchImages() {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let assets = PHAsset.fetchAssets(with: .image, options: fetchOptions)
let manager = PHImageManager.default()
let option = PHImageRequestOptions()
option.isSynchronous = true
assets.enumerateObjects { asset, _, _ in
manager.requestImage(
for: asset,
targetSize: PHImageManagerMaximumSize,
contentMode: .aspectFill,
options: option
) { image, _ in
if let image = image {
DispatchQueue.main.async {
if let img = scaleAndCropImage(image, toSize: CGSize(width: gridSize, height: gridSize)) {
self.images.append(img)
}
}
}
}
}
}
func openAppSettings() {
if
let appSettingsUrl = URL(string: UIApplication.openSettingsURLString),
UIApplication.shared.canOpenURL(appSettingsUrl)
{
UIApplication.shared.open(appSettingsUrl, completionHandler: nil)
}
}
func scaleAndCropImage(_ image: UIImage, toSize size: CGSize) -> UIImage? {
let imageView = UIImageView(frame: CGRect(origin: .zero, size: size))
imageView.contentMode = .scaleAspectFill
imageView.image = image
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
imageView.layer.render(in: context)
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return result return result
} }
} }
private enum GridElementType { class CameraUIView: UIView {
case cameraFeed var previewLayer: AVCaptureVideoPreviewLayer?
case cameraAskButton
case cameraRestricted
case photo
case photoAskButton
case photoRestricted
}
private struct GridElement: View, Identifiable { override func layoutSubviews() {
let id: UUID super.layoutSubviews()
let type: GridElementType previewLayer?.frame = bounds
let content: UIImage?
let action: () -> Void
var body: some View {
switch type {
case .cameraFeed:
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100)
.clipped()
case .cameraAskButton:
Button {
action()
} label: {
RoundedRectangle(cornerRadius: 5)
.stroke(Color.Main.backgroundDark, lineWidth: 2)
.overlay {
Image(systemName: "camera")
.foregroundColor(.Material.tortoiseLight300)
.font(.system(size: 40))
}
.frame(height: 100)
// .resizable()
// .aspectRatio(contentMode: .fill)
// .frame(width: 100, height: 100)
// .clipped()
}
case .photo:
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100)
.clipped()
case .photoAskButton:
Button {
action()
} label: {
Image(systemName: "photo.badge.plus")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100)
.clipped()
}
case .photoRestricted, .cameraRestricted:
Button {
action()
} label: {
Image(systemName: "cross")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100)
.clipped()
}
}
}
private var image: Image {
guard let content = content else {
return Image(systemName: "questionmark.square.dashed")
}
return Image(uiImage: content)
} }
} }
struct AttachmentMediaPickerView_Previews: PreviewProvider { struct CameraView: UIViewRepresentable {
static var previews: some View { func makeUIView(context _: Context) -> CameraUIView {
AttachmentMediaPickerView() let view = CameraUIView()
let captureSession = AVCaptureSession()
guard let captureDevice = AVCaptureDevice.default(for: .video) else { return view }
guard let input = try? AVCaptureDeviceInput(device: captureDevice) else { return view }
captureSession.addInput(input)
let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
view.previewLayer = previewLayer
captureSession.startRunning()
return view
}
func updateUIView(_ uiView: CameraUIView, context _: Context) {
// Update the previewLayer frame when the view updates
uiView.previewLayer?.frame = uiView.bounds
} }
} }
// struct AttachmentMediaPickerView: View {
// @StateObject private var mediaManager = MediaManager()
//
// var body: some View {
// ScrollView {
// LazyVGrid(columns: Array(repeating: .init(.flexible()), count: 3)) {
// ForEach(elements) { element in
// element
// }
// }
// .padding(.horizontal, 8)
// }
// .padding(.vertical, 8)
// }
//
// private var elements: [GridElement] {
// print("Creating elements")
// var result: [GridElement] = []
//
// // camera
// if let feed = mediaManager.cameraFeed, mediaManager.cameraAccessLevel == .authorized {
// result.append(GridElement(id: UUID(), type: .cameraFeed, content: feed) {
// print("Go to capture???")
// })
// print("Added camera feed")
// } else if mediaManager.cameraAccessLevel == .restricted {
// result.append(GridElement(id: UUID(), type: .cameraRestricted, content: nil) {
// print("Show alert")
// })
// print("Added camera restricted")
// } else {
// result.append(GridElement(id: UUID(), type: .cameraAskButton, content: nil) {
// mediaManager.openAppSettings()
// })
// print("Added camera ask button")
// }
//
// // photos
// // if mediaManager.galleryAccessLevel == .restricted {
// // result.append(GridElement(id: UUID(), type: .photoRestricted, content: nil))
// // } else {
// // for photo in mediaManager.photos {
// // result.append(GridElement(id: UUID(), type: .photo, content: photo))
// // }
// // if mediaManager.galleryAccessLevel != .authorized {
// // result.append(GridElement(id: UUID(), type: .photoAskButton, content: nil))
// // }
// // }
//
// return result
// }
// }
//
// private enum GridElementType {
// case cameraFeed
// case cameraAskButton
// case cameraRestricted
// case photo
// case photoAskButton
// case photoRestricted
// }
//
// private struct GridElement: View, Identifiable {
// let id: UUID
// let type: GridElementType
// let content: UIImage?
// let action: () -> Void
//
// var body: some View {
// switch type {
// case .cameraFeed:
// image
// .resizable()
// .aspectRatio(contentMode: .fill)
// .frame(width: 100, height: 100)
// .clipped()
//
// case .cameraAskButton:
// Button {
// action()
// } label: {
// RoundedRectangle(cornerRadius: 5)
// .stroke(Color.Main.backgroundDark, lineWidth: 2)
// .overlay {
// Image(systemName: "camera")
// .foregroundColor(.Material.tortoiseLight300)
// .font(.system(size: 40))
// }
// .frame(height: 100)
// // .resizable()
// // .aspectRatio(contentMode: .fill)
// // .frame(width: 100, height: 100)
// // .clipped()
// }
//
// case .photo:
// image
// .resizable()
// .aspectRatio(contentMode: .fill)
// .frame(width: 100, height: 100)
// .clipped()
//
// case .photoAskButton:
// Button {
// action()
// } label: {
// Image(systemName: "photo.badge.plus")
// .resizable()
// .aspectRatio(contentMode: .fill)
// .frame(width: 100, height: 100)
// .clipped()
// }
//
// case .photoRestricted, .cameraRestricted:
// Button {
// action()
// } label: {
// Image(systemName: "cross")
// .resizable()
// .aspectRatio(contentMode: .fill)
// .frame(width: 100, height: 100)
// .clipped()
// }
// }
// }
//
// private var image: Image {
// guard let content = content else {
// return Image(systemName: "questionmark.square.dashed")
// }
// return Image(uiImage: content)
// }
// }
//
// struct AttachmentMediaPickerView_Previews: PreviewProvider {
// static var previews: some View {
// AttachmentMediaPickerView()
// }
// }