MyApp/ParticipantView.swift
4.4 KB · 97 lines · swift · Download from the repository page
import SwiftUIimport AVKit struct DirectionProbe: UIViewRepresentable { let outer: Bool let changed: ([String], Bool) -> Void final class Coordinator { var direction: AVCaptureDeviceDirectionCoordinator? } func makeCoordinator() -> Coordinator { Coordinator() } func makeUIView(context: Context) -> UIView { let view = UIView() let outer = outer let changed = changed context.coordinator.direction = AVCaptureDeviceDirectionCoordinator(view: view, deviceTypes: [.builtInWideAngleCamera]) { map in let devices = outer ? map.forwardFacingDeviceDescriptors : map.backwardFacingDeviceDescriptors changed(devices.map(\.uniqueID), outer) } return view } func updateUIView(_ uiView: UIView, context: Context) {}} struct ParticipantView: View { let presentation: ParticipantPresentation let image: UIImage? let simulated: Bool let stop: () -> Void let directionChanged: ([String], Bool) -> Void var cueAppeared: (UUID) -> Void = { _ in } var cueDisappeared: (UUID) -> Void = { _ in } @State private var recDotOn = true @Environment(\.accessibilityReduceMotion) private var reduceMotion var body: some View { VStack(spacing: RetroTheme.spaceM) { if simulated { RetroTag(title: "SIMULATED PARTICIPANT VIEW", color: RetroTheme.pink) } HStack(spacing: RetroTheme.spaceS) { if presentation.recording { Circle().fill(RetroTheme.error).frame(width: 12, height: 12) .opacity(reduceMotion ? 1 : (recDotOn ? 1 : 0.25)) .onAppear { if !reduceMotion { withAnimation(.easeInOut(duration: 0.6).repeatForever(autoreverses: true)) { recDotOn.toggle() } } } .accessibilityHidden(true) } Text(presentation.status.uppercased()) .font(RetroTheme.heading(.title)) } PreviewImage(image: image).overlay { if let cue = presentation.cue { Text(cue.letter) .font(.system(size: 120, weight: .black, design: .rounded)) .foregroundStyle(RetroTheme.ink).frame(width: 160, height: 160) .background(RetroTheme.yellow, in: .rect(cornerRadius: RetroTheme.radius)) .overlay(RoundedRectangle(cornerRadius: RetroTheme.radius).stroke(.white, lineWidth: RetroTheme.border)) .id(cue.id) .onAppear { if !simulated { cueAppeared(cue.id) } } .onDisappear { if !simulated { cueDisappeared(cue.id) } } } } Text("Keep your head and hands in view. Leave room to move safely.") .font(.callout).foregroundStyle(.white.opacity(0.75)) if let countdown = presentation.countdown { Text("\(countdown)").font(.system(size: 64, weight: .black, design: .rounded)).monospacedDigit() .foregroundStyle(RetroTheme.yellow) } Button("Stop", systemImage: "stop.fill", action: stop) .buttonStyle(RetroButtonStyle(color: RetroTheme.pink)).controlSize(.large) } .padding(.horizontal, RetroTheme.screenPaddingH) .padding(.vertical, RetroTheme.spaceL) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.black).foregroundStyle(.white) .overlay(RoundedRectangle(cornerRadius: 0).stroke(RetroTheme.yellow, lineWidth: RetroTheme.border)) .background { DirectionProbe(outer: true, changed: directionChanged) } }} struct PreviewImage: View { let image: UIImage? var body: some View { ZStack { RoundedRectangle(cornerRadius: 4).fill(.black) if let image { Image(uiImage: image).resizable().scaledToFit().scaleEffect(x: -1, y: 1) .accessibilityLabel("Mirrored participant camera preview") } else { ContentUnavailableView("Camera preview", systemImage: "camera", description: Text("No live camera image")) .foregroundStyle(.white) } }.frame(minHeight: 180, idealHeight: 280, maxHeight: 340).clipShape(.rect(cornerRadius: 4)).overlay(RoundedRectangle(cornerRadius: 4).stroke(RetroTheme.yellow, lineWidth: 2)) }}