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
100 changes: 100 additions & 0 deletions DualCameraDemo/DualCameraDemo/ContainerExample.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import DualCameraKit
import SwiftUI

struct ContainerExample: View {
var body: some View {
AppTabView()
}
}

enum Tab {
case feed, camera, map
}

let photoSaveStrategy: DualCameraPhotoSaveStrategy = .custom { image in
print("captured", image)
}

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

Check warning on line 20 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)
let vm = DualCameraViewModel(

Check warning on line 21 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)
captureScope: .container
// photoSaveStrategy: photoSaveStrategy
)

Check warning on line 25 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 {
switch selectedTab {
case .feed:
VStack {
Color.mint
}
case .camera:
ZStack {
GeometryReader { proxy in
DualCameraScreen(
viewModel: vm
)
.onChange(of: proxy.size, initial: true) { _, newSize in
vm.containerSizeChanged(newSize)
}
}
}
case .map:
VStack {
Color.teal
}
}
}
tabBar
.edgesIgnoringSafeArea(.all)
}
}

Check warning on line 55 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)
@ViewBuilder
private var tabBar: some View {
HStack {
tabBarButton(tab: .feed, image: "house.fill")
Spacer()
tabBarButton(tab: .camera, image: "camera.fill", isCenter: true)
Spacer()
tabBarButton(tab: .map, image: "bubble.left.and.bubble.right.fill")
}
.padding(.horizontal, 30)
.padding(.vertical, 10)
.background(Color(UIColor.systemBackground).opacity(0.95))
.cornerRadius(20)
.shadow(radius: 5)
.padding(.horizontal)
.padding(.bottom, 10)
}

Check warning on line 73 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)
@ViewBuilder
private func tabBarButton(tab: Tab, image: String, isCenter: Bool = false) -> some View {
Button(action: {
selectedTab = tab
}) {

Check warning on line 78 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)
.frame(width: 60, height: 60)
.shadow(radius: 4)
Image(systemName: image)
.font(.system(size: 24))
.foregroundColor(.black)
}
} else {
Image(systemName: image)
.font(.system(size: 24))
.foregroundColor(selectedTab == tab ? .blue : .gray)
}
}
}
}

#Preview {
AppTabView()
}
14 changes: 9 additions & 5 deletions DualCameraDemo/DualCameraDemo/DualCameraDemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,29 @@
import Observation
import SwiftUI

@main

Check warning on line 5 in DualCameraDemo/DualCameraDemo/DualCameraDemoApp.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
struct DualCameraDemoApp: App {
enum DemoDisplayType {
case dualCameraScreen
case dualCameraScreen(isFullScreen: Bool)
case dualCameraDisplayView
case dualCameraLowLevelComponents
}

Check warning on line 12 in DualCameraDemo/DualCameraDemo/DualCameraDemoApp.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
@State private var demoType = DemoDisplayType.dualCameraScreen
@State private var demoType = DemoDisplayType.dualCameraScreen(isFullScreen: false)

var body: some Scene {
WindowGroup {
switch demoType {
case .dualCameraScreen:
DualCameraScreen()
case .dualCameraScreen(let isFullScreen):
switch isFullScreen {
case true:
DualCameraScreen()
case false:
ContainerExample()
}
case .dualCameraDisplayView, .dualCameraLowLevelComponents:
Text("Not Implemented Yet")
}

}
}
}
4 changes: 4 additions & 0 deletions Sources/DualCameraKit/DualCameraController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ public final class DualCameraController: DualCameraControlling {
public func stopSession() {
streamSource.stopSession()
cancelRendererTasks()
// Clear renderers so they're recreated with fresh stream connections on next startSession()
renderers.removeAll()
}

/// Creates a renderer (using MetalCameraRenderer by default).
Expand Down Expand Up @@ -199,6 +201,8 @@ public final class DualCameraMockController: DualCameraControlling {
public func stopSession() {
streamSource.stopSession()
cancelRendererTasks()
// Clear renderers so they're recreated with fresh stream connections on next startSession()
renderers.removeAll()
}

public var photoCapturer: any DualCameraPhotoCapturing
Expand Down
11 changes: 1 addition & 10 deletions Sources/DualCameraKit/DualCameraDisplayView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ public struct DualCameraDisplayView: View {
for: (miniCamera == .front ? .back : .front)
)
)
.ignoresSafeArea(.all)

// Mini camera in corner
DualCameraRendererView(renderer: controller.getRenderer(for: miniCamera))
Expand All @@ -42,23 +41,15 @@ public struct DualCameraDisplayView: View {
cameraView(for: .back, widthFraction: 0.5)
cameraView(for: .front, widthFraction: 0.5)
}
.ignoresSafeArea(.all)

case .stackedVertical:
VStack(spacing: 0) {
cameraView(for: .back, heightFraction: 0.5)
cameraView(for: .front, heightFraction: 0.5)
}
.ignoresSafeArea(.all)
}
}
.task {
do {
try await controller.startSession()
} catch {
print("Camera session error: \(error)")
}
}

}

/// Renders a camera feed in partial or full size
Expand Down
9 changes: 0 additions & 9 deletions Sources/DualCameraKit/DualCameraPhotoCapturing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ public class DualCameraPhotoCapturer: DualCameraPhotoCapturing {
/// Returns an image that is a screenshot of the screen.
public func captureCurrentScreen(mode: DualCameraPhotoCaptureMode = .fullScreen) async throws -> UIImage {
let application = UIApplication.shared

guard let keyWindow = application.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first(where: { $0.activationState == .foregroundActive })?
Expand Down Expand Up @@ -90,14 +89,6 @@ public class DualCameraPhotoCapturer: DualCameraPhotoCapturing {
let capturedImage = renderer.image { context in
let cgContext = context.cgContext

// Calculate scaling
let scaleX = size.width / fullScreenSize.width
let scaleY = size.height / fullScreenSize.height
let scale = min(scaleX, scaleY)

// Apply scaling
cgContext.scaleBy(x: scale, y: scale)

keyWindow.drawHierarchy(
in: CGRect(origin: .zero, size: fullScreenSize),
afterScreenUpdates: afterScreenUpdates
Expand Down
21 changes: 0 additions & 21 deletions Sources/DualCameraKit/Screen/CameraConfiguration.swift

This file was deleted.

8 changes: 4 additions & 4 deletions Sources/DualCameraKit/Screen/DualCameraConfigView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ struct DualCameraConfigView: View {
} label: {
HStack {
Text(title)
if viewModel.configuration.layout == layout {
if viewModel.cameraLayout == layout {
Image(systemName: "checkmark")
}
}
Expand All @@ -62,13 +62,13 @@ struct DualCameraConfigView: View {
private var recorderTypePicker: some View {
VStack {
Menu {
ForEach(DualCameraVideoRecordingMode.allCases) { recorderType in
ForEach(DualCameraRecorderType.allCases) { recorderType in
Button {
viewModel.toggleRecorderType()
} label: {
HStack {
Text(recorderType.displayName)
if viewModel.videoRecorderType == recorderType {
if viewModel.selectedRecorderType == recorderType {
Image(systemName: "checkmark")
}
}
Expand All @@ -77,7 +77,7 @@ struct DualCameraConfigView: View {
} label: {
HStack {
Image(systemName: "video.fill")
Text("Recorder: \(viewModel.videoRecorderType.displayName)")
Text("Recorder: \(viewModel.selectedRecorderType.displayName)")
Image(systemName: "chevron.up.chevron.down")
.font(.caption)
}
Expand Down
65 changes: 44 additions & 21 deletions Sources/DualCameraKit/Screen/DualCameraScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,38 @@ import SwiftUI

public struct DualCameraScreen: View {
@State private var viewModel: DualCameraViewModel
private let customOverlay: ((DualCameraViewModel) -> AnyView)

public init(
viewModel: DualCameraViewModel = .default()
viewModel: DualCameraViewModel = .default(),
@ViewBuilder customOverlay: @escaping (DualCameraViewModel) -> some View = { _ in EmptyView() }
) {
_viewModel = State(initialValue: viewModel)
self.customOverlay = { AnyView(customOverlay($0)) }
}

public var body: some View {
GeometryReader { geoProxy in
ZStack {
DualCameraDisplayView(
controller: viewModel.controller,
layout: viewModel.configuration.layout
layout: viewModel.cameraLayout
)
.overlay(settingsButton, alignment: .topLeading)
.ignoresSafeArea()
.overlay(viewModel.isSettingsButtonVisible ? settingsButton : nil, alignment: .topLeading)
.overlay(recordingIndicator, alignment: .top)
.overlay(controlButtons, alignment: .bottom)

if case .error(let error) = viewModel.viewState {
errorOverlay(error)
}
}
.onChange(of: geoProxy.size, initial: true) { oldSize, newSize in
viewModel.containerSizeChanged(newSize)
}
.onAppear {
viewModel.onAppear(containerSize: geoProxy.size)
}
.onChange(of: geoProxy.size, initial: false) { oldSize, newSize in
viewModel.containerSizeChanged(newSize)
}
.onDisappear {
viewModel.onDisappear()
}
Expand All @@ -38,14 +42,16 @@ public struct DualCameraScreen: View {
switch sheetType {
case .configSheet: DualCameraConfigView(
viewModel: viewModel
)
}
)}
})
.alert(
item: $viewModel.alert
) { alert in
getAlert(for: alert)
}
.overlay(alignment: .top) {
customOverlay(viewModel)
}
}
}

Expand Down Expand Up @@ -91,22 +97,24 @@ public struct DualCameraScreen: View {
}
.disabled(!viewModel.viewState.isPhotoButtonEnabled)

// Video recording button
Button(action: viewModel.recordVideoButtonTapped) {
Image(systemName: viewModel.viewState.videoButtonIcon)
.font(.largeTitle)
.foregroundColor(viewModel.viewState.videoButtonColor)
.padding()
.background(
Circle()
.fill(viewModel.viewState.videoButtonBackgroundColor)
)
if viewModel.isVideoButtonVisible {
// Video recording button
Button(action: viewModel.recordVideoButtonTapped) {
Image(systemName: viewModel.viewState.videoButtonIcon)
.font(.largeTitle)
.foregroundColor(viewModel.viewState.videoButtonColor)
.padding()
.background(
Circle()
.fill(viewModel.viewState.videoButtonBackgroundColor)
)
}
.disabled(!viewModel.viewState.isVideoButtonEnabled)
}
.disabled(!viewModel.viewState.isVideoButtonEnabled)
}
}
.opacity(viewModel.viewState.captureInProgress ? 0 : 1)
.padding(.bottom, 30)
//.padding(.bottom, 30)
}

@ViewBuilder
Expand Down Expand Up @@ -172,12 +180,27 @@ public struct DualCameraScreen: View {
.font(.title2)
}
.tint(.gray)
.opacity(viewModel.viewState.captureInProgress ? 0 : 1)
.padding(.leading)
}
}

// MARK: - Preview

#Preview() {
#Preview("Photo & Video") {
DualCameraScreen()
}

#Preview("Photo & Video - Show Settings Button") {
DualCameraScreen(viewModel: .init(
showSettingsButton: false
))
}

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

Loading
Loading