Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions DualCameraDemo/DualCameraDemo/CapturePreviewOverlay.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import SwiftUI

struct CapturePreviewOverlay: View {
let image: UIImage
let onDismiss: () -> Void
let onConfirm: () -> Void

var body: some View {
ZStack {
Color.black.opacity(0.7)
.ignoresSafeArea()

VStack(spacing: 16) {
HStack {
Button(action: onDismiss) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 24))
.foregroundColor(.white)
}

Spacer()

Text("Review")
.font(.headline)
.foregroundColor(.white)

Spacer()

Button(action: onConfirm) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 24))
.foregroundColor(.green)
}
}
.padding()

Image(uiImage: image)
.resizable()
.scaledToFit()
.cornerRadius(12)
.padding()

Spacer()
}
.padding()
}
}
}
116 changes: 83 additions & 33 deletions DualCameraDemo/DualCameraDemo/ContainerExample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,56 +11,106 @@
case feed, camera, map
}

let photoSaveStrategy: DualCameraPhotoSaveStrategy = .custom { image in
print("captured", image)
@Observable
final class CaptureReviewState {
enum PreviewPhase: Equatable {
case hidden
case showing(UIImage)
}
var previewPhase = PreviewPhase.hidden

Check warning on line 21 in DualCameraDemo/DualCameraDemo/ContainerExample.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
func showPreview(_ image: UIImage) {
withAnimation {
previewPhase = .showing(image)
}
}

Check warning on line 27 in DualCameraDemo/DualCameraDemo/ContainerExample.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
func reset() {
withAnimation {
previewPhase = .hidden
}

Check warning on line 32 in DualCameraDemo/DualCameraDemo/ContainerExample.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
}
}

private struct AppTabView: View {
@State private var selectedTab: Tab = .camera

let vm = DualCameraViewModel(
captureScope: .container
// photoSaveStrategy: photoSaveStrategy
)
@State private var captureReviewState = CaptureReviewState()
private var vm: DualCameraViewModel

Check warning on line 39 in DualCameraDemo/DualCameraDemo/ContainerExample.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Identifier Name Violation: Variable name 'vm' should be between 3 and 40 characters long (identifier_name)

init() {
vm = DualCameraViewModel(
captureScope: .container,
includeVideoRecording: false,
saveToLibrary: false
)
}

Check warning on line 48 in DualCameraDemo/DualCameraDemo/ContainerExample.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
var body: some View {
VStack {
Group {
ZStack {
VStack {
switch selectedTab {
case .feed:
VStack {
Color.mint
}
feedMock
case .camera:
ZStack {
GeometryReader { proxy in
DualCameraScreen(
viewModel: vm
)
.onChange(of: proxy.size, initial: true) { _, newSize in
vm.containerSizeChanged(newSize)
}
}
}
cameraCapture
case .map:
VStack {
Color.teal
}
mapMock
}
tabBar
.edgesIgnoringSafeArea(.all)
}
tabBar
.edgesIgnoringSafeArea(.all)

Check warning on line 63 in DualCameraDemo/DualCameraDemo/ContainerExample.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
switch captureReviewState.previewPhase {
case .hidden:
EmptyView()
case .showing(let image):
CapturePreviewOverlay(
image: image,
onDismiss: {
captureReviewState.reset()
},
onConfirm: {
captureReviewState.reset()
}
)
.transition(.scale.combined(with: .opacity))
}
}
.background(.primary)
.onChange(of: vm.capturedPhoto) { oldValue, newValue in
if let image = newValue {
captureReviewState.showPreview(image)
}
}
}

Check warning on line 87 in DualCameraDemo/DualCameraDemo/ContainerExample.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
private var cameraCapture: some View {
DualCameraScreen(
viewModel: vm
)
}

private var feedMock: some View {
VStack {
Color(.systemMint)
}
}

private var mapMock: some View {
VStack {
Color(.systemTeal)
}
}

@ViewBuilder
private var tabBar: some View {
HStack {
tabBarButton(tab: .feed, image: "house.fill")
tabBarButton(tab: .map, image: "map.fill")
Spacer()
tabBarButton(tab: .camera, image: "camera.fill", isCenter: true)
tabBarButton(tab: .camera, image: "house.fill", isCenter: true)
Spacer()
tabBarButton(tab: .map, image: "bubble.left.and.bubble.right.fill")
tabBarButton(tab: .feed, image: "bubble.left.and.bubble.right.fill")
}
.padding(.horizontal, 30)
.padding(.vertical, 10)
Expand All @@ -75,21 +125,21 @@
private func tabBarButton(tab: Tab, image: String, isCenter: Bool = false) -> some View {
Button(action: {
selectedTab = tab
}) {

Check warning on line 128 in DualCameraDemo/DualCameraDemo/ContainerExample.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Multiple Closures with Trailing Closure Violation: Trailing closure syntax should not be used when passing more than one closure argument (multiple_closures_with_trailing_closure)
if isCenter {
ZStack {
Circle()
.fill(Color.white)
.fill(Color(.systemBackground))
.frame(width: 60, height: 60)
.shadow(radius: 4)
Image(systemName: image)
.font(.system(size: 24))
.foregroundColor(.black)
.foregroundColor(Color(.label))
}
} else {
Image(systemName: image)
.font(.system(size: 24))
.foregroundColor(selectedTab == tab ? .blue : .gray)
.foregroundColor(selectedTab == tab ? Color.accentColor : Color(.secondaryLabel))
}
}
}
Expand Down
37 changes: 27 additions & 10 deletions Sources/DualCameraKit/DualCameraPhotoCapturing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,21 @@ public protocol DualCameraPhotoCapturing: AnyObject, Sendable {
func captureCurrentScreen(mode: DualCameraPhotoCaptureMode) async throws -> UIImage
}

/// determines whether the photos are captured in as if displayed in `fullScreen` or in a layout not fillingl the fullscreen aka a container via `containerSize`
/// Determines the capture mode for photo screenshots.
///
/// - `fullScreen`: Captures the entire screen
/// - `containerFrame`: Captures only the specified frame region (used for container mode)
public enum DualCameraPhotoCaptureMode: Sendable, Equatable {
case fullScreen
case containerSize(CGSize)
/// Captures a specific rectangular region of the screen in global window coordinates.
/// The frame's origin determines the top-left corner to start capturing from,
/// and the size determines the dimensions of the captured area.
case containerFrame(CGRect)

@available(*, deprecated, message: "Use containerFrame instead")
public static func containerSize(_ size: CGSize) -> DualCameraPhotoCaptureMode {
.containerFrame(CGRect(origin: .zero, size: size))
}
}

public class DualCameraPhotoCapturer: DualCameraPhotoCapturing {
Expand Down Expand Up @@ -73,27 +84,33 @@ public class DualCameraPhotoCapturer: DualCameraPhotoCapturing {
}
return capturedImage

case .containerSize(let size):
guard !size.width.isZero && !size.height.isZero else {
case .containerFrame(let frame):
guard !frame.size.width.isZero && !frame.size.height.isZero else {
throw DualCameraError.captureFailure(.unknownDimensions)
}



let format = UIGraphicsImageRendererFormat()
format.scale = screenScale
format.opaque = true

// Create renderer with optimized format for container size
let renderer = UIGraphicsImageRenderer(size: size, format: format)
// Generate scaled image with optimized drawing
let renderer = UIGraphicsImageRenderer(size: frame.size, format: format)

// Generate cropped image by translating the drawing context
let capturedImage = renderer.image { context in
let cgContext = context.cgContext


// Translate the context to "shift" the window so the desired frame is at origin
cgContext.translateBy(x: -frame.origin.x, y: -frame.origin.y)

// Draw the full window hierarchy, but only the translated portion will be visible
keyWindow.drawHierarchy(
in: CGRect(origin: .zero, size: fullScreenSize),
afterScreenUpdates: afterScreenUpdates
)
}

return capturedImage
}
}
Expand Down
7 changes: 1 addition & 6 deletions Sources/DualCameraKit/Screen/DualCameraConfigView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,7 @@ struct DualCameraConfigView: View {
DualCameraConfigView(
viewModel: DualCameraViewModel(
dualCameraController: DualCameraMockController(),
videoSaveStrategy: .custom({ savedFile in
print("video recorded: \(savedFile)")
}),
photoSaveStrategy: .custom( {capturedImage in
print("photo captured: \(capturedImage)")
})
saveToLibrary: false
)
)
}
17 changes: 14 additions & 3 deletions Sources/DualCameraKit/Screen/DualCameraScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ public struct DualCameraScreen: View {
.overlay(viewModel.isSettingsButtonVisible ? settingsButton : nil, alignment: .topLeading)
.overlay(recordingIndicator, alignment: .top)
.overlay(controlButtons, alignment: .bottom)

if case .error(let error) = viewModel.viewState {
errorOverlay(error)
}
}
.onAppear {
viewModel.onAppear(containerSize: geoProxy.size)
}
.onChange(of: geoProxy.size, initial: false) { oldSize, newSize in
.onChange(of: geoProxy.size, initial: true) { oldSize, newSize in
viewModel.containerSizeChanged(newSize)
}
.onDisappear {
Expand All @@ -52,6 +52,18 @@ public struct DualCameraScreen: View {
.overlay(alignment: .top) {
customOverlay(viewModel)
}
.background(
GeometryReader { innerProxy in
Color.clear
.onAppear {
let globalFrame = innerProxy.frame(in: .global)
viewModel.containerFrameChanged(globalFrame)
}
.onChange(of: innerProxy.frame(in: .global)) { oldFrame, newFrame in
viewModel.containerFrameChanged(newFrame)
}
}
)
}
}

Expand Down Expand Up @@ -199,7 +211,6 @@ public struct DualCameraScreen: View {

#Preview("Photo") {
DualCameraScreen(viewModel: .init(
videoSaveStrategy: nil,
showSettingsButton: false
))
}
Expand Down
Loading
Loading