diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 000000000..baabc5060 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "8628b222c8e99b1566b2448a6c0b171754962f8e594eba418b8bdd4d6cb30bcf", + "pins" : [ + { + "identity" : "timer", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Mijick/Timer", + "state" : { + "revision" : "342371c33c3f084d82a4818447ba77d858064c85", + "version" : "2.0.0" + } + } + ], + "version" : 3 +} diff --git a/Sources/Internal/Manager/CameraManager+MotionManager.swift b/Sources/Internal/Manager/CameraManager+MotionManager.swift index a1faaf9d2..f79cd1024 100644 --- a/Sources/Internal/Manager/CameraManager+MotionManager.swift +++ b/Sources/Internal/Manager/CameraManager+MotionManager.swift @@ -13,7 +13,7 @@ import CoreMotion import AVKit @MainActor class CameraManagerMotionManager { - private(set) var parent: CameraManager! + private(set) weak var parent: CameraManager? private(set) var manager: CMMotionManager = .init() } @@ -27,7 +27,7 @@ extension CameraManagerMotionManager { } private extension CameraManagerMotionManager { func handleAccelerometerUpdates(_ data: CMAccelerometerData?, _ error: Error?) { - guard let data, error == nil else { return } + guard let data, error == nil, parent != nil else { return } let newDeviceOrientation = getDeviceOrientation(data.acceleration) updateDeviceOrientation(newDeviceOrientation) @@ -37,67 +37,106 @@ private extension CameraManagerMotionManager { } } private extension CameraManagerMotionManager { - func getDeviceOrientation(_ acceleration: CMAcceleration) -> AVCaptureVideoOrientation { switch acceleration { + func getDeviceOrientation(_ acceleration: CMAcceleration) -> AVCaptureVideoOrientation { + switch acceleration { case let acceleration where acceleration.x >= 0.75: .landscapeLeft case let acceleration where acceleration.x <= -0.75: .landscapeRight case let acceleration where acceleration.y <= -0.75: .portrait case let acceleration where acceleration.y >= 0.75: .portraitUpsideDown - default: parent.attributes.deviceOrientation - }} - func updateDeviceOrientation(_ newDeviceOrientation: AVCaptureVideoOrientation) { if newDeviceOrientation != parent.attributes.deviceOrientation { - parent.attributes.deviceOrientation = newDeviceOrientation - }} + default: parent?.attributes.deviceOrientation ?? AVCaptureVideoOrientation.portrait + } + } + func updateDeviceOrientation(_ newDeviceOrientation: AVCaptureVideoOrientation) { + if newDeviceOrientation != parent?.attributes.deviceOrientation { + parent?.attributes.deviceOrientation = newDeviceOrientation + } + } func updateUserBlockedScreenRotation() { let newUserBlockedScreenRotation = getNewUserBlockedScreenRotation() - if newUserBlockedScreenRotation != parent.attributes.userBlockedScreenRotation { parent.attributes.userBlockedScreenRotation = newUserBlockedScreenRotation } + if newUserBlockedScreenRotation != parent?.attributes.userBlockedScreenRotation { + parent?.attributes.userBlockedScreenRotation = newUserBlockedScreenRotation + } + } + func updateFrameOrientation() { + guard let parent else { return } + + if UIDevice.current.orientation != .portraitUpsideDown { + let newFrameOrientation = getNewFrameOrientation(parent.attributes.orientationLocked ? .portrait : UIDevice.current.orientation) + updateFrameOrientation(newFrameOrientation) + } + } + func redrawGrid() { + guard let parent else { return } + + if !parent.attributes.orientationLocked { + parent.cameraGridView.draw(.zero) + } } - func updateFrameOrientation() { if UIDevice.current.orientation != .portraitUpsideDown { - let newFrameOrientation = getNewFrameOrientation(parent.attributes.orientationLocked ? .portrait : UIDevice.current.orientation) - updateFrameOrientation(newFrameOrientation) - }} - func redrawGrid() { if !parent.attributes.orientationLocked { - parent.cameraGridView.draw(.zero) - }} } private extension CameraManagerMotionManager { - func getNewUserBlockedScreenRotation() -> Bool { switch parent.attributes.deviceOrientation.rawValue == UIDevice.current.orientation.rawValue { + func getNewUserBlockedScreenRotation() -> Bool { + guard let parent else { return false } + + return switch parent.attributes.deviceOrientation.rawValue == UIDevice.current.orientation.rawValue { case true: false case false: !parent.attributes.orientationLocked - }} - func getNewFrameOrientation(_ orientation: UIDeviceOrientation) -> CGImagePropertyOrientation { switch parent.attributes.cameraPosition { + } + } + func getNewFrameOrientation(_ orientation: UIDeviceOrientation) -> CGImagePropertyOrientation { + guard let parent else { return .up } + + return switch parent.attributes.cameraPosition { case .back: getNewFrameOrientationForBackCamera(orientation) case .front: getNewFrameOrientationForFrontCamera(orientation) - }} - func updateFrameOrientation(_ newFrameOrientation: CGImagePropertyOrientation) { if newFrameOrientation != parent.attributes.frameOrientation { - let shouldAnimate = shouldAnimateFrameOrientationChange(newFrameOrientation) - updateFrameOrientation(withAnimation: shouldAnimate, newFrameOrientation: newFrameOrientation) - }} + } + } + func updateFrameOrientation(_ newFrameOrientation: CGImagePropertyOrientation) { + guard let parent else { return } + + if newFrameOrientation != parent.attributes.frameOrientation { + let shouldAnimate = shouldAnimateFrameOrientationChange(newFrameOrientation) + updateFrameOrientation(withAnimation: shouldAnimate, newFrameOrientation: newFrameOrientation) + } + } } private extension CameraManagerMotionManager { - func getNewFrameOrientationForBackCamera(_ orientation: UIDeviceOrientation) -> CGImagePropertyOrientation { switch orientation { + func getNewFrameOrientationForBackCamera(_ orientation: UIDeviceOrientation) -> CGImagePropertyOrientation { + guard let parent else { return .up } + + return switch orientation { case .portrait: parent.attributes.mirrorOutput ? .leftMirrored : .right case .landscapeLeft: parent.attributes.mirrorOutput ? .upMirrored : .up case .landscapeRight: parent.attributes.mirrorOutput ? .downMirrored : .down default: parent.attributes.mirrorOutput ? .leftMirrored : .right - }} - func getNewFrameOrientationForFrontCamera(_ orientation: UIDeviceOrientation) -> CGImagePropertyOrientation { switch orientation { + } + } + func getNewFrameOrientationForFrontCamera(_ orientation: UIDeviceOrientation) -> CGImagePropertyOrientation { + guard let parent else { return .up } + + return switch orientation { case .portrait: parent.attributes.mirrorOutput ? .right : .leftMirrored case .landscapeLeft: parent.attributes.mirrorOutput ? .down : .downMirrored case .landscapeRight: parent.attributes.mirrorOutput ? .up : .upMirrored default: parent.attributes.mirrorOutput ? .right : .leftMirrored - }} + } + } func shouldAnimateFrameOrientationChange(_ newFrameOrientation: CGImagePropertyOrientation) -> Bool { + guard let parent else { return false } + let backCameraOrientations: [CGImagePropertyOrientation] = [.left, .right, .up, .down], frontCameraOrientations: [CGImagePropertyOrientation] = [.leftMirrored, .rightMirrored, .upMirrored, .downMirrored] return (backCameraOrientations.contains(newFrameOrientation) && backCameraOrientations.contains(parent.attributes.frameOrientation)) || (frontCameraOrientations.contains(parent.attributes.frameOrientation) && frontCameraOrientations.contains(newFrameOrientation)) } - func updateFrameOrientation(withAnimation shouldAnimate: Bool, newFrameOrientation: CGImagePropertyOrientation) { Task { - await parent.cameraMetalView.beginCameraOrientationAnimation(if: shouldAnimate) - parent.attributes.frameOrientation = newFrameOrientation - parent.cameraMetalView.finishCameraOrientationAnimation(if: shouldAnimate) - }} + func updateFrameOrientation(withAnimation shouldAnimate: Bool, newFrameOrientation: CGImagePropertyOrientation) { + Task { + guard let parent else { return } + await parent.cameraMetalView.beginCameraOrientationAnimation(if: shouldAnimate) + parent.attributes.frameOrientation = newFrameOrientation + parent.cameraMetalView.finishCameraOrientationAnimation(if: shouldAnimate) + } + } } // MARK: Reset diff --git a/Sources/Internal/Manager/CameraManager+NotificationCenter.swift b/Sources/Internal/Manager/CameraManager+NotificationCenter.swift index 4f9aebe02..802a69d26 100644 --- a/Sources/Internal/Manager/CameraManager+NotificationCenter.swift +++ b/Sources/Internal/Manager/CameraManager+NotificationCenter.swift @@ -12,7 +12,7 @@ import Foundation @MainActor class CameraManagerNotificationCenter { - private(set) var parent: CameraManager! + private(set) weak var parent: CameraManager? } // MARK: Setup @@ -24,6 +24,8 @@ extension CameraManagerNotificationCenter { } private extension CameraManagerNotificationCenter { @objc func handleSessionWasInterrupted() { + guard let parent else { return } + parent.attributes.lightMode = .off parent.videoOutput.reset() } diff --git a/Sources/Internal/Manager/CameraManager+PhotoOutput.swift b/Sources/Internal/Manager/CameraManager+PhotoOutput.swift index 107413496..bdecc5110 100644 --- a/Sources/Internal/Manager/CameraManager+PhotoOutput.swift +++ b/Sources/Internal/Manager/CameraManager+PhotoOutput.swift @@ -12,7 +12,7 @@ import AVKit @MainActor class CameraManagerPhotoOutput: NSObject { - private(set) var parent: CameraManager! + private(set) weak var parent: CameraManager? private(set) var output: AVCapturePhotoOutput = .init() } @@ -20,7 +20,7 @@ import AVKit extension CameraManagerPhotoOutput { func setup(parent: CameraManager) throws(MCameraError) { self.parent = parent - try self.parent.captureSession.add(output: output) + try parent.captureSession.add(output: output) } } @@ -36,17 +36,21 @@ extension CameraManagerPhotoOutput { configureOutput() output.capturePhoto(with: settings, delegate: self) - parent.cameraMetalView.performImageCaptureAnimation() + parent?.cameraMetalView.performImageCaptureAnimation() } } private extension CameraManagerPhotoOutput { func getPhotoOutputSettings() -> AVCapturePhotoSettings { let settings = AVCapturePhotoSettings() - settings.flashMode = parent.attributes.flashMode.toDeviceFlashMode() + if let parent { + settings.flashMode = parent.attributes.flashMode.toDeviceFlashMode() + } return settings } func configureOutput() { - guard let connection = output.connection(with: .video), connection.isVideoMirroringSupported else { return } + guard let connection = output.connection(with: .video), connection.isVideoMirroringSupported, + let parent + else { return } connection.isVideoMirrored = parent.attributes.mirrorOutput ? parent.attributes.cameraPosition != .front : parent.attributes.cameraPosition == .front connection.videoOrientation = parent.attributes.deviceOrientation @@ -57,7 +61,8 @@ private extension CameraManagerPhotoOutput { extension CameraManagerPhotoOutput: @preconcurrency AVCapturePhotoCaptureDelegate { func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: (any Error)?) { guard let imageData = photo.fileDataRepresentation(), - let ciImage = CIImage(data: imageData) + let ciImage = CIImage(data: imageData), + let parent else { return } let capturedCIImage = prepareCIImage(ciImage, parent.attributes.cameraFilters) @@ -86,7 +91,11 @@ private extension CameraManagerPhotoOutput { } private extension CameraManagerPhotoOutput { func getFixedFrameOrientation() -> CGImagePropertyOrientation { - guard UIDevice.current.orientation != parent.attributes.deviceOrientation.toDeviceOrientation() else { return parent.attributes.frameOrientation } + guard let parent, + UIDevice.current.orientation != parent.attributes.deviceOrientation.toDeviceOrientation() + else { + return parent?.attributes.frameOrientation ?? .up + } return switch (parent.attributes.deviceOrientation, parent.attributes.cameraPosition) { case (.portrait, .front): .left diff --git a/Sources/Internal/Manager/CameraManager+VideoOutput.swift b/Sources/Internal/Manager/CameraManager+VideoOutput.swift index b5cb37a71..8c47d39a7 100644 --- a/Sources/Internal/Manager/CameraManager+VideoOutput.swift +++ b/Sources/Internal/Manager/CameraManager+VideoOutput.swift @@ -14,7 +14,7 @@ import SwiftUI import MijickTimer @MainActor class CameraManagerVideoOutput: NSObject { - private(set) var parent: CameraManager! + private(set) weak var parent: CameraManager? private(set) var output: AVCaptureMovieFileOutput = .init() private(set) var timer: MTimer = .init(.camera) private(set) var recordingTime: MTime = .zero @@ -58,7 +58,7 @@ private extension CameraManagerVideoOutput { storeLastFrame() output.startRecording(to: url, recordingDelegate: self) startRecordingTimer() - parent.objectWillChange.send() + parent?.objectWillChange.send() } } private extension CameraManagerVideoOutput { @@ -66,21 +66,34 @@ private extension CameraManagerVideoOutput { FileManager.prepareURLForVideoOutput() } func configureOutput() { - guard let connection = output.connection(with: .video), connection.isVideoMirroringSupported else { return } + guard let connection = output.connection(with: .video), + connection.isVideoMirroringSupported, + let parent + else { return } connection.isVideoMirrored = parent.attributes.mirrorOutput ? parent.attributes.cameraPosition != .front : parent.attributes.cameraPosition == .front connection.videoOrientation = parent.attributes.deviceOrientation } func storeLastFrame() { - guard let texture = parent.cameraMetalView.currentDrawable?.texture, + guard let texture = parent?.cameraMetalView.currentDrawable?.texture, let ciImage = CIImage(mtlTexture: texture, options: nil), - let cgImage = parent.cameraMetalView.ciContext.createCGImage(ciImage, from: ciImage.extent) + let cgImage = parent?.cameraMetalView.ciContext.createCGImage(ciImage, from: ciImage.extent), + let orientation = parent?.attributes.deviceOrientation.toImageOrientation() else { return } - firstRecordedFrame = UIImage(cgImage: cgImage, scale: 1.0, orientation: parent.attributes.deviceOrientation.toImageOrientation()) + firstRecordedFrame = UIImage( + cgImage: cgImage, + scale: 1.0, + orientation: orientation + ) } func startRecordingTimer() { try? timer .publish(every: 1) { [self] in + guard let parent else { + timer.cancel() + return + } + recordingTime = $0 parent.objectWillChange.send() } @@ -99,19 +112,22 @@ private extension CameraManagerVideoOutput { private extension CameraManagerVideoOutput { func presentLastFrame() { let firstRecordedFrame = MCameraMedia(data: firstRecordedFrame) - parent.setCapturedMedia(firstRecordedFrame) + parent?.setCapturedMedia(firstRecordedFrame) } } // MARK: Receive Data extension CameraManagerVideoOutput: @preconcurrency AVCaptureFileOutputRecordingDelegate { - func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: (any Error)?) { Task { - let videoURL = try await prepareVideo(outputFileURL: outputFileURL, cameraFilters: parent.attributes.cameraFilters) - let capturedVideo = MCameraMedia(data: videoURL) - - await Task.sleep(seconds: Animation.duration) - parent.setCapturedMedia(capturedVideo) - }} + func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: (any Error)?) { + guard let cameraFilters = parent?.attributes.cameraFilters else { return } + Task { + let videoURL = try await prepareVideo(outputFileURL: outputFileURL, cameraFilters: cameraFilters) + let capturedVideo = MCameraMedia(data: videoURL) + + await Task.sleep(seconds: Animation.duration) + parent?.setCapturedMedia(capturedVideo) + } + } } private extension CameraManagerVideoOutput { func prepareVideo(outputFileURL: URL, cameraFilters: [CIFilter]) async throws -> URL { diff --git a/Sources/Internal/UI/Camera View/CameraView+Grid.swift b/Sources/Internal/UI/Camera View/CameraView+Grid.swift index 2648d79b9..9848e0388 100644 --- a/Sources/Internal/UI/Camera View/CameraView+Grid.swift +++ b/Sources/Internal/UI/Camera View/CameraView+Grid.swift @@ -12,7 +12,7 @@ import SwiftUI class CameraGridView: UIView { - var parent: CameraManager! + weak var parent: CameraManager? } // MARK: Setup @@ -28,7 +28,7 @@ extension CameraGridView { extension CameraGridView { func setVisibility(_ isVisible: Bool) { UIView.animate(withDuration: 0.2) { self.alpha = isVisible ? 1 : 0 } - parent.attributes.isGridVisible = isVisible + parent?.attributes.isGridVisible = isVisible } } diff --git a/Sources/Internal/UI/Camera View/CameraView+Metal.swift b/Sources/Internal/UI/Camera View/CameraView+Metal.swift index 2741a24d7..ac1139d6c 100644 --- a/Sources/Internal/UI/Camera View/CameraView+Metal.swift +++ b/Sources/Internal/UI/Camera View/CameraView+Metal.swift @@ -14,7 +14,7 @@ import MetalKit import AVKit @MainActor class CameraMetalView: MTKView { - private(set) var parent: CameraManager! + private(set) weak var parent: CameraManager? private(set) var ciContext: CIContext! private(set) var commandQueue: MTLCommandQueue! private(set) var currentFrame: CIImage? @@ -39,7 +39,7 @@ private extension CameraMetalView { self.commandQueue = metalDevice.makeCommandQueue() } func configureMetalView(metalDevice: MTLDevice) { - self.parent.cameraView.alpha = 0 + self.parent?.cameraView.alpha = 0 self.delegate = self self.device = metalDevice @@ -60,7 +60,7 @@ private extension CameraMetalView { // MARK: Camera Entrance extension CameraMetalView { func performCameraEntranceAnimation() { UIView.animate(withDuration: 0.33) { [self] in - parent.cameraView.alpha = 1 + parent?.cameraView.alpha = 1 }} } @@ -69,14 +69,14 @@ extension CameraMetalView { func performImageCaptureAnimation() { let blackMatte = createBlackMatte() - parent.cameraView.addSubview(blackMatte) + parent?.cameraView.addSubview(blackMatte) animateBlackMatte(blackMatte) } } private extension CameraMetalView { func createBlackMatte() -> UIView { let view = UIView() - view.frame = parent.cameraView.frame + view.frame = parent?.cameraView.frame ?? .zero view.backgroundColor = .init(resource: .mijickBackgroundPrimary) view.alpha = 0 return view @@ -101,7 +101,7 @@ extension CameraMetalView { await Task.sleep(seconds: 0.01) } func finishCameraFlipAnimation() async { - guard let blurView = parent.cameraView.viewWithTag(.blurViewTag) else { return } + guard let blurView = parent?.cameraView.viewWithTag(.blurViewTag) else { return } await Task.sleep(seconds: 0.44) UIView.animate(withDuration: 0.3, animations: { blurView.alpha = 0 }) { [self] _ in @@ -118,6 +118,8 @@ private extension CameraMetalView { return image } func insertBlurView(_ snapshot: UIImage?) { + guard let parent else { return } + let blurView = UIImageView(frame: parent.cameraView.frame) blurView.image = snapshot blurView.contentMode = .scaleAspectFill @@ -128,11 +130,14 @@ private extension CameraMetalView { parent.cameraView.addSubview(blurView) } func animateBlurFlip() { + guard let parent else { return } UIView.transition(with: parent.cameraView, duration: 0.44, options: cameraFlipAnimationTransition) {} } } private extension CameraMetalView { - var cameraFlipAnimationTransition: UIView.AnimationOptions { parent.attributes.cameraPosition == .back ? .transitionFlipFromLeft : .transitionFlipFromRight } + var cameraFlipAnimationTransition: UIView.AnimationOptions { + parent?.attributes.cameraPosition == .back ? .transitionFlipFromLeft : .transitionFlipFromRight + } } // MARK: Camera Focus @@ -141,14 +146,16 @@ extension CameraMetalView { removeExistingFocusIndicatorAnimations() let focusIndicator = focusIndicator.create(at: touchPoint) - parent.cameraView.addSubview(focusIndicator) + parent?.cameraView.addSubview(focusIndicator) animateFocusIndicator(focusIndicator) } } private extension CameraMetalView { - func removeExistingFocusIndicatorAnimations() { if let view = parent.cameraView.viewWithTag(.focusIndicatorTag) { - view.removeFromSuperview() - }} + func removeExistingFocusIndicatorAnimations() { + if let view = parent?.cameraView.viewWithTag(.focusIndicatorTag) { + view.removeFromSuperview() + } + } func animateFocusIndicator(_ focusIndicator: UIImageView) { UIView.animate(withDuration: 0.44, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, animations: { focusIndicator.transform = .init(scaleX: 1, y: 1) }) { _ in UIView.animate(withDuration: 0.44, delay: 1.44, animations: { focusIndicator.alpha = 0.2 }) { _ in @@ -160,13 +167,19 @@ private extension CameraMetalView { // MARK: Camera Orientation extension CameraMetalView { - func beginCameraOrientationAnimation(if shouldAnimate: Bool) async { if shouldAnimate { - parent.cameraView.alpha = 0 - await Task.sleep(seconds: 0.1) - }} - func finishCameraOrientationAnimation(if shouldAnimate: Bool) { if shouldAnimate { - UIView.animate(withDuration: 0.2, delay: 0.1) { self.parent.cameraView.alpha = 1 } - }} + func beginCameraOrientationAnimation(if shouldAnimate: Bool) async { + if shouldAnimate { + parent?.cameraView.alpha = 0 + await Task.sleep(seconds: 0.1) + } + } + func finishCameraOrientationAnimation(if shouldAnimate: Bool) { + if shouldAnimate { + UIView.animate(withDuration: 0.2, delay: 0.1) { + self.parent?.cameraView.alpha = 1 + } + } + } } @@ -186,11 +199,15 @@ extension CameraMetalView: @preconcurrency AVCaptureVideoDataOutputSampleBufferD } private extension CameraMetalView { func captureCurrentFrame(_ cvImageBuffer: CVImageBuffer) -> CIImage { + guard let parent else { return .empty() } + let currentFrame = CIImage(cvImageBuffer: cvImageBuffer) return currentFrame.oriented(parent.attributes.frameOrientation) } func applyingFiltersToCurrentFrame(_ currentFrame: CIImage) -> CIImage { - currentFrame.applyingFilters(parent.attributes.cameraFilters) + guard let parent else { return .empty() } + + return currentFrame.applyingFilters(parent.attributes.cameraFilters) } func redrawCameraView(_ frame: CIImage) { currentFrame = frame