Skip to content

Commit

Permalink
Patch 2.1.0
Browse files Browse the repository at this point in the history
feat:
- Added possibility to customise DefaultCameraView (#33)
- Added option to change camera frame rate (#28)
- Extended ability to customise behaviour after media capture (#34)
- Added ability to change camera resolution

fix:
- Fixed problem with package manager (#29)
  • Loading branch information
FulcrumOne authored Jul 28, 2024
1 parent 010eeed commit 0ff7c42
Show file tree
Hide file tree
Showing 13 changed files with 160 additions and 21 deletions.
2 changes: 1 addition & 1 deletion MijickCameraView.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Pod::Spec.new do |s|
CameraView is a free and open-source library dedicated for SwiftUI that allows you to create fully customisable camera view in no time. Keep your code clean.
DESC

s.version = '2.0.0'
s.version = '2.1.0'
s.ios.deployment_target = '14.0'
s.swift_version = '5.10'

Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ let package = Package(
.library(name: "MijickCameraView", targets: ["MijickCameraView"]),
],
dependencies: [
.package(url: "https://github.com/Mijick/Timer", branch: "main")
.package(url: "https://github.com/Mijick/Timer", from: "1.0.1")
],
targets: [
.target(name: "MijickCameraView", dependencies: [.product(name: "MijickTimer", package: "Timer")], path: "Sources")
Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,12 @@ struct CameraView: View {
outputType: .photo,
cameraPosition: .back,
cameraFilters: [.init(name: "CISepiaTone")!],
resolution: .hd4K3840x2160,
frameRate: 25,
flashMode: .off,
isGridVisible: true,
focusImageColor: .blue,
focusImageSize: 100
focusImageColor: .yellow,
focusImageSize: 92
)

(...)
Expand Down Expand Up @@ -170,8 +172,9 @@ struct CameraView: View {
.onVideoCaptured { url in
print("VIDEO CAPTURED")
}
.afterMediaCaptured {
print("IMAGE OR VIDEO WAS PROCESSED. WHAT'S NEXT?")
.afterMediaCaptured { $0
.closeCameraController(true)
.custom { print("Media object has been successfully captured") }
}
.onCloseController {
print("CLOSE THE CONTROLLER")
Expand Down
2 changes: 1 addition & 1 deletion Sources/Internal/Config/CameraConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ struct CameraConfig {
// MARK: Actions
var onImageCaptured: (UIImage) -> () = { _ in }
var onVideoCaptured: (URL) -> () = { _ in }
var afterMediaCaptured: () -> () = {}
var afterMediaCaptured: (PostCameraConfig) -> (PostCameraConfig) = { _ in .init() }
var onCloseController: () -> () = {}
}
19 changes: 19 additions & 0 deletions Sources/Internal/Config/PostCameraConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// PostCameraConfig.swift of MijickCameraView
//
// Created by Tomasz Kurylik
// - Twitter: https://twitter.com/tkurylik
// - Mail: [email protected]
// - GitHub: https://github.com/FulcrumOne
//
// Copyright ©2024 Mijick. Licensed under MIT License.


public class PostCameraConfig {
// MARK: Attributes
var shouldReturnToCameraView: Bool = false
var shouldCloseCameraController: Bool = false

// MARK: Actions
var customAction: () -> () = {}
}
41 changes: 40 additions & 1 deletion Sources/Internal/Managers/CameraManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public class CameraManager: NSObject, ObservableObject { init(_ attributes: Attr
var torchMode: CameraTorchMode = .off
var cameraExposure: CameraExposure = .init()
var hdrMode: CameraHDRMode = .auto
var resolution: AVCaptureSession.Preset = .hd1920x1080
var frameRate: Int32 = 30
var mirrorOutput: Bool = false
var isGridVisible: Bool = true
var isRecording: Bool = false
Expand Down Expand Up @@ -155,9 +157,10 @@ extension CameraManager {
try setupDeviceOutput()
try setupFrameRecorder()
try setupCameraAttributes()
try setupFrameRate()

startCaptureSession()
} catch {}
} catch { print("CANNOT SETUP CAMERA: \(error)") }
}
}
private extension CameraManager {
Expand All @@ -173,6 +176,7 @@ private extension CameraManager {
}}
func initialiseCaptureSession() {
captureSession = .init()
captureSession.sessionPreset = attributes.resolution
}
func initialiseMetal() {
metalDevice = MTLCreateSystemDefaultDevice()
Expand Down Expand Up @@ -246,6 +250,10 @@ private extension CameraManager {
attributes.cameraExposure.mode = device.exposureMode
attributes.hdrMode = device.hdrMode
}}}
func setupFrameRate() throws { if let device = getDevice(attributes.cameraPosition) {
try checkNewFrameRate(attributes.frameRate, device)
try updateFrameRate(attributes.frameRate, device)
}}
func startCaptureSession() { DispatchQueue(label: "cameraSession").async { [self] in
captureSession.startRunning()
}}
Expand Down Expand Up @@ -572,6 +580,36 @@ private extension CameraManager {
}
}

// MARK: - Changing Camera Resolution
extension CameraManager {
func changeResolution(_ newResolution: AVCaptureSession.Preset) throws { if newResolution != attributes.resolution {
captureSession.sessionPreset = newResolution
attributes.resolution = newResolution
}}
}

// MARK: - Changing Frame Rate
extension CameraManager {
func changeFrameRate(_ newFrameRate: Int32) throws { if let device = getDevice(attributes.cameraPosition), newFrameRate != attributes.frameRate {
try checkNewFrameRate(newFrameRate, device)
try updateFrameRate(newFrameRate, device)
updateFrameRate(newFrameRate)
}}
}
private extension CameraManager {
func checkNewFrameRate(_ newFrameRate: Int32, _ device: AVCaptureDevice) throws { let newFrameRate = Double(newFrameRate), maxFrameRate = device.activeFormat.videoSupportedFrameRateRanges.first?.maxFrameRate ?? 60
if newFrameRate < 15 { throw Error.incorrectFrameRate }
if newFrameRate > maxFrameRate { throw Error.incorrectFrameRate }
}
func updateFrameRate(_ newFrameRate: Int32, _ device: AVCaptureDevice) throws { try withLockingDeviceForConfiguration(device) { device in
device.activeVideoMinFrameDuration = .init(value: 1, timescale: newFrameRate)
device.activeVideoMaxFrameDuration = .init(value: 1, timescale: newFrameRate)
}}
func updateFrameRate(_ newFrameRate: Int32) {
attributes.frameRate = newFrameRate
}
}

// MARK: - Changing Mirror Mode
extension CameraManager {
func changeMirrorMode(_ shouldMirror: Bool) { if !isChanging {
Expand Down Expand Up @@ -919,4 +957,5 @@ private extension CameraManager {
public extension CameraManager { enum Error: Swift.Error {
case microphonePermissionsNotGranted, cameraPermissionsNotGranted
case cannotSetupInput, cannotSetupOutput, capturedPhotoCannotBeFetched
case incorrectFrameRate
}}
33 changes: 26 additions & 7 deletions Sources/Internal/Views/Default/DefaultCameraView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@

import SwiftUI

struct DefaultCameraView: MCameraView {
@ObservedObject var cameraManager: CameraManager
let namespace: Namespace.ID
let closeControllerAction: () -> ()
public struct DefaultCameraView: MCameraView {
@ObservedObject public var cameraManager: CameraManager
public let namespace: Namespace.ID
public let closeControllerAction: () -> ()
var config: Config = .init()


var body: some View {
public var body: some View {
VStack(spacing: 0) {
createTopView()
createContentView()
Expand Down Expand Up @@ -74,6 +75,7 @@ private extension DefaultCameraView {
.mask(Capsule())
.transition(.asymmetric(insertion: .opacity.animation(.defaultSpring.delay(1)), removal: .scale.combined(with: .opacity)))
.isActive(!isRecording)
.isActive(config.outputTypePickerVisible)
.frame(maxHeight: .infinity, alignment: .bottom)
.padding(.bottom, 8)
}
Expand Down Expand Up @@ -103,18 +105,22 @@ private extension DefaultCameraView {
}
private extension DefaultCameraView {
func createGridButton() -> some View {
TopButton(icon: gridButtonIcon, action: changeGridVisibility).rotationEffect(iconAngle)
TopButton(icon: gridButtonIcon, action: changeGridVisibility)
.rotationEffect(iconAngle)
.isActiveStackElement(config.gridButtonVisible)
}
func createFlipOutputButton() -> some View {
TopButton(icon: flipButtonIcon, action: changeMirrorOutput)
.rotationEffect(iconAngle)
.isActiveStackElement(cameraPosition == .front)
.isActiveStackElement(config.flipButtonVisible)
}
func createFlashButton() -> some View {
TopButton(icon: flashButtonIcon, action: changeFlashMode)
.rotationEffect(iconAngle)
.isActiveStackElement(hasFlash)
.isActiveStackElement(outputType == .photo)
.isActiveStackElement(config.flashButtonVisible)
}
}
private extension DefaultCameraView {
Expand All @@ -124,16 +130,18 @@ private extension DefaultCameraView {
.rotationEffect(iconAngle)
.frame(maxWidth: .infinity, alignment: .leading)
.isActive(hasTorch)
.isActive(config.torchButtonVisible)
}
func createCaptureButton() -> some View {
CaptureButton(action: captureOutput, mode: outputType, isRecording: isRecording)
CaptureButton(action: captureOutput, mode: outputType, isRecording: isRecording).isActive(config.captureButtonVisible)
}
func createChangeCameraButton() -> some View {
BottomButton(icon: "icon-change-camera", active: false, action: changeCameraPosition)
.matchedGeometryEffect(id: "button-bottom-right", in: namespace)
.rotationEffect(iconAngle)
.frame(maxWidth: .infinity, alignment: .trailing)
.isActive(!isRecording)
.isActive(config.changeCameraButtonVisible)
}
func createOutputTypeButton(_ cameraOutputType: CameraOutputType) -> some View {
OutputTypeButton(type: cameraOutputType, active: cameraOutputType == outputType, action: { changeCameraOutputType(cameraOutputType) })
Expand Down Expand Up @@ -185,6 +193,17 @@ private extension DefaultCameraView {
}
}

// MARK: - Configurables
extension DefaultCameraView { struct Config {
var outputTypePickerVisible: Bool = true
var torchButtonVisible: Bool = true
var captureButtonVisible: Bool = true
var changeCameraButtonVisible: Bool = true
var gridButtonVisible: Bool = true
var flipButtonVisible: Bool = true
var flashButtonVisible: Bool = true
}}


// MARK: - CloseButton
fileprivate struct CloseButton: View {
Expand Down
7 changes: 6 additions & 1 deletion Sources/Internal/Views/Main/MCameraController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,17 @@ private extension MCameraController {
}
func performAfterMediaCapturedAction() { if let capturedMedia = cameraManager.attributes.capturedMedia {
notifyUserOfMediaCaptured(capturedMedia)
config.afterMediaCaptured()
performPostCameraAction()
}}
}
private extension MCameraController {
func notifyUserOfMediaCaptured(_ capturedMedia: MCameraMedia) {
if let image = capturedMedia.image { config.onImageCaptured(image) }
else if let video = capturedMedia.video { config.onVideoCaptured(video) }
}
func performPostCameraAction() { let afterMediaCaptured = config.afterMediaCaptured(.init())
afterMediaCaptured.shouldReturnToCameraView ? cameraManager.resetCapturedMedia() : ()
afterMediaCaptured.shouldCloseCameraController ? config.onCloseController() : ()
afterMediaCaptured.customAction()
}
}
30 changes: 30 additions & 0 deletions Sources/Public/Config/Public+DefaultCameraView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// Public+DefaultCameraView.swift of MijickCameraView
//
// Created by Tomasz Kurylik
// - Twitter: https://twitter.com/tkurylik
// - Mail: [email protected]
// - GitHub: https://github.com/FulcrumOne
//
// Copyright ©2024 Mijick. Licensed under MIT License.


import SwiftUI

// MARK: - Initialiser
public extension DefaultCameraView {
init(cameraManager: CameraManager, namespace: Namespace.ID, closeControllerAction: @escaping () -> Void) {
self.init(cameraManager: cameraManager, namespace: namespace, closeControllerAction: closeControllerAction, config: .init())
}
}

// MARK: - Customising View
public extension DefaultCameraView {
func outputTypePickerVisible(_ value: Bool) -> Self { setAndReturnSelf { $0.config.outputTypePickerVisible = value } }
func torchButtonVisible(_ value: Bool) -> Self { setAndReturnSelf { $0.config.torchButtonVisible = value } }
func captureButtonVisible(_ value: Bool) -> Self { setAndReturnSelf { $0.config.captureButtonVisible = value } }
func cameraPositionButtonVisible(_ value: Bool) -> Self { setAndReturnSelf { $0.config.changeCameraButtonVisible = value } }
func gridButtonVisible(_ value: Bool) -> Self { setAndReturnSelf { $0.config.gridButtonVisible = value } }
func flipButtonVisible(_ value: Bool) -> Self { setAndReturnSelf { $0.config.flipButtonVisible = value } }
func flashButtonVisible(_ value: Bool) -> Self { setAndReturnSelf { $0.config.flashButtonVisible = value } }
}
2 changes: 1 addition & 1 deletion Sources/Public/Config/Public+MCameraController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public extension MCameraController {
func onVideoCaptured(_ action: @escaping (URL) -> ()) -> Self { setAndReturnSelf { $0.config.onVideoCaptured = action } }

/// Sets the action triggered when a photo or video is taken
func afterMediaCaptured(_ action: @escaping () -> ()) -> Self { setAndReturnSelf { $0.config.afterMediaCaptured = action } }
func afterMediaCaptured(_ action: @escaping (PostCameraConfig) -> (PostCameraConfig)) -> Self { setAndReturnSelf { $0.config.afterMediaCaptured = action } }

/// Determines what happens when the Camera Controller should be closed
func onCloseController(_ action: @escaping () -> ()) -> Self { setAndReturnSelf { $0.config.onCloseController = action } }
Expand Down
16 changes: 16 additions & 0 deletions Sources/Public/Config/Public+PostCameraConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// Public+PostCameraConfig.swift of MijickCameraView
//
// Created by Tomasz Kurylik
// - Twitter: https://twitter.com/tkurylik
// - Mail: [email protected]
// - GitHub: https://github.com/FulcrumOne
//
// Copyright ©2024 Mijick. Licensed under MIT License.


extension PostCameraConfig {
public func returnToCameraView(_ value: Bool) -> Self { shouldReturnToCameraView = value; return self }
public func closeCameraController(_ value: Bool) -> Self { shouldCloseCameraController = value; return self }
public func custom(_ action: @escaping () -> ()) -> Self { customAction = action; return self }
}
11 changes: 7 additions & 4 deletions Sources/Public/Managers/Public+CameraManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,36 @@


import SwiftUI
import AVFoundation

// MARK: - Initialiser
public extension CameraManager {
convenience init(
outputType: CameraOutputType? = nil,
cameraPosition: CameraPosition? = nil,
cameraFilters: [CIFilter]? = nil,
resolution: AVCaptureSession.Preset? = nil,
frameRate: Int32? = nil,
flashMode: CameraFlashMode? = nil,
isGridVisible: Bool? = nil,
focusImage: UIImage? = nil,
focusImageColor: UIColor? = nil,
focusImageSize: CGFloat? = nil
) {
self.init(.init(outputType, cameraPosition, cameraFilters, flashMode, isGridVisible))
self.init(.init(outputType, cameraPosition, cameraFilters, resolution, frameRate, flashMode, isGridVisible))

if let focusImage { self.cameraFocusView.image = focusImage }
if let focusImageColor { self.cameraFocusView.tintColor = focusImageColor }
if let focusImageSize { self.cameraFocusView.frame.size = .init(width: focusImageSize, height: focusImageSize) }
}
}
private extension CameraManager.Attributes {
init(_ outputType: CameraOutputType?, _ cameraPosition: CameraPosition?, _ cameraFilters: [CIFilter]?, _ flashMode: CameraFlashMode?, _ isGridVisible: Bool?) {
self.init()

init(_ outputType: CameraOutputType?, _ cameraPosition: CameraPosition?, _ cameraFilters: [CIFilter]?, _ resolution: AVCaptureSession.Preset?, _ frameRate: Int32?, _ flashMode: CameraFlashMode?, _ isGridVisible: Bool?) { self.init()
if let outputType { self.outputType = outputType }
if let cameraPosition { self.cameraPosition = cameraPosition }
if let cameraFilters { self.cameraFilters = cameraFilters }
if let resolution { self.resolution = resolution }
if let frameRate { self.frameRate = frameRate }
if let flashMode { self.flashMode = flashMode }
if let isGridVisible { self.isGridVisible = isGridVisible }
}
Expand Down
5 changes: 5 additions & 0 deletions Sources/Public/View Protocols/Public+MCameraView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public extension MCameraView {
func changeOutputType(_ type: CameraOutputType) throws { try cameraManager.changeOutputType(type) }
func changeCamera(_ position: CameraPosition) throws { try cameraManager.changeCamera(position) }
func changeCameraFilters(_ filters: [CIFilter]) throws { try cameraManager.changeCameraFilters(filters) }
func changeResolution(_ resolution: AVCaptureSession.Preset) throws { try cameraManager.changeResolution(resolution) }
func changeFrameRate(_ frameRate: Int32) throws { try cameraManager.changeFrameRate(frameRate) }
func changeZoomFactor(_ value: CGFloat) throws { try cameraManager.changeZoomFactor(value) }
func changeFlashMode(_ mode: CameraFlashMode) throws { try cameraManager.changeFlashMode(mode) }
func changeTorchMode(_ mode: CameraTorchMode) throws { try cameraManager.changeTorchMode(mode) }
Expand All @@ -46,6 +48,9 @@ public extension MCameraView {
public extension MCameraView {
var outputType: CameraOutputType { cameraManager.attributes.outputType }
var cameraPosition: CameraPosition { cameraManager.attributes.cameraPosition }
var resolution: AVCaptureSession.Preset { cameraManager.attributes.resolution }
var frameRate: Int32 { cameraManager.attributes.frameRate }
var zoomFactor: CGFloat { cameraManager.attributes.zoomFactor }
var torchMode: CameraTorchMode { cameraManager.attributes.torchMode }
var flashMode: CameraFlashMode { cameraManager.attributes.flashMode }
var exposureMode: AVCaptureDevice.ExposureMode { cameraManager.attributes.cameraExposure.mode }
Expand Down

0 comments on commit 0ff7c42

Please sign in to comment.