Last active
September 12, 2023 16:42
-
-
Save jasoneveleth/4fa2faced568cb1dbd2da296e8f50e88 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// THIS DOESN'T WORK, see the final comment for fix | |
// | |
import SwiftUI | |
import AVFoundation | |
struct ContentView: View { | |
@StateObject private var cameraManager = CameraManager() | |
var body: some View { | |
ZStack { | |
if cameraManager.isSessionInitialized { | |
VideoPreviewView().edgesIgnoringSafeArea(.all) | |
} else { | |
Text("loading cam") | |
} | |
RecordButton() | |
} | |
.environmentObject(cameraManager) | |
} | |
} | |
class RecordingHandler: NSObject, AVCaptureFileOutputRecordingDelegate { | |
func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) {} | |
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {} | |
} | |
class CameraManager: ObservableObject { | |
var captureSession = AVCaptureSession() | |
@Published var isSessionInitialized = false | |
private var movieOutput: AVCaptureMovieFileOutput? | |
private var recordingHandler: RecordingHandler | |
init() { | |
self.recordingHandler = RecordingHandler() | |
DispatchQueue.global(qos: .background).async { [weak self] in | |
guard let self = self else { return } | |
guard let camera = AVCaptureDevice.default(for: .video) else { return } | |
do { | |
let input = try AVCaptureDeviceInput(device: camera) | |
self.captureSession.addInput(input) | |
self.movieOutput = AVCaptureMovieFileOutput() | |
if self.captureSession.canAddOutput(self.movieOutput!) { | |
self.captureSession.addOutput(self.movieOutput!) | |
} | |
// Start running the capture session on the background queue | |
self.captureSession.startRunning() | |
DispatchQueue.main.async { | |
self.isSessionInitialized = true | |
} | |
} catch { | |
print(error.localizedDescription) | |
} | |
} | |
} | |
func startRecording() { | |
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] | |
let outputURL = documentsDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("mov") | |
movieOutput?.startRecording(to: outputURL, recordingDelegate: recordingHandler) | |
} | |
func stopRecording() { | |
movieOutput?.stopRecording() | |
} | |
} | |
struct RecordButton: View { | |
@EnvironmentObject private var cameraManager: CameraManager | |
@State private var isRecording = false | |
var body: some View { | |
Button(action: {}) { | |
Image(systemName: "circle.fill") | |
.resizable() | |
.aspectRatio(contentMode: .fit) | |
.frame(width: 75, height: 75) | |
.foregroundColor(isRecording ? .white : .red) | |
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local) | |
.onChanged({ _ in | |
if !isRecording { | |
cameraManager.startRecording() | |
isRecording = true | |
} | |
}) | |
.onEnded({ _ in | |
isRecording = false | |
cameraManager.stopRecording() | |
}) | |
) | |
} | |
} | |
} | |
struct VideoPreviewView: UIViewRepresentable { | |
@EnvironmentObject var cameraManager: CameraManager | |
func makeUIView(context: Context) -> UIView { | |
let view = UIView() | |
let previewLayer = AVCaptureVideoPreviewLayer(session: cameraManager.captureSession) | |
previewLayer.frame = view.layer.bounds | |
previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill | |
view.layer.addSublayer(previewLayer) | |
return view | |
} | |
func updateUIView(_ uiView: UIView, context: Context) {} | |
} |
loremipsum suggested: Pass it as a “let” from the parent from the View that has the conditional. You are just removing the wrapper that is what triggers update.
Here is the new code:
// ContentView
var body: some View {
ZStack {
VideoPreviewView(cameraManager: cameraManager).edgesIgnoringSafeArea(.all)
RecordButton()
}
.environmentObject(cameraManager)
}
and
struct VideoPreviewView: UIViewRepresentable {
let cameraManager: CameraManager
init(cameraManager: CameraManager) {
self.cameraManager = cameraManager
}
func makeUIView(context: Context) -> UIView {
let view = UIView()
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
if cameraManager.isSessionInitialized {
print("now")
let previewLayer = AVCaptureVideoPreviewLayer(session: cameraManager.captureSession)
previewLayer.frame = uiView.layer.bounds
previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
uiView.layer.addSublayer(previewLayer)
}
}
}
This doesn't seem to work either (the view is still blank). Please let me know what I've done wrong.
EDIT: the "now" statement never fires.
Another interpretation of that comment is this, which also still has a blank view (but does make the "now" print statement fire).
// ContentView
var body: some View {
ZStack {
if cameraManager.isSessionInitialized {
VideoPreviewView(cameraManager: cameraManager).edgesIgnoringSafeArea(.all)
} else {
Text("nope not in this house")
}
RecordButton()
}
.environmentObject(cameraManager)
}
and
struct VideoPreviewView: UIViewRepresentable {
let cameraManager: CameraManager
init(cameraManager: CameraManager) {
self.cameraManager = cameraManager
}
func makeUIView(context: Context) -> UIView {
let view = UIView()
if cameraManager.isSessionInitialized {
print("now")
let previewLayer = AVCaptureVideoPreviewLayer(session: cameraManager.captureSession)
previewLayer.frame = view.layer.bounds
previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
view.layer.addSublayer(previewLayer)
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
The issue as @vadian pointed out is the bounds were zero since I the view I was taking the bounds from was just initialized. The correct code is a tiny change from my desired modifications. Use UIScreen.main.bounds
rather than view.layer.bounds
. In the initial code.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
To test this code, use XCode 14.3.1 (14E300c) or compatible to create a new iOS app. Then replace the
ContentView
that is generated for you with this, and it should allow you to experiment. For code that works (but runs the preview layer attach code multiple times), see my stack overflow question.