feat: 相机

This commit is contained in:
jinyaqiu 2025-08-22 14:51:45 +08:00
parent 534c38cd8b
commit 5a784e7ae7
3 changed files with 269 additions and 2 deletions

View File

@ -0,0 +1,265 @@
import SwiftUI
import AVFoundation
struct CustomCameraView: UIViewControllerRepresentable {
@Binding var isPresented: Bool
let onImageCaptured: (UIImage) -> Void
@Environment(\.presentationMode) private var presentationMode
func makeUIViewController(context: Context) -> CustomCameraViewController {
let viewController = CustomCameraViewController()
viewController.delegate = context.coordinator
return viewController
}
func updateUIViewController(_ uiViewController: CustomCameraViewController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, CustomCameraViewControllerDelegate {
let parent: CustomCameraView
init(_ parent: CustomCameraView) {
self.parent = parent
}
func didCaptureImage(_ image: UIImage) {
parent.onImageCaptured(image)
parent.presentationMode.wrappedValue.dismiss()
}
func didCancel() {
parent.presentationMode.wrappedValue.dismiss()
}
}
}
protocol CustomCameraViewControllerDelegate: AnyObject {
func didCaptureImage(_ image: UIImage)
func didCancel()
}
class CustomCameraViewController: UIViewController {
private var captureSession: AVCaptureSession?
private var photoOutput: AVCapturePhotoOutput?
private var previewLayer: AVCaptureVideoPreviewLayer?
private var captureDevice: AVCaptureDevice?
weak var delegate: CustomCameraViewControllerDelegate?
private lazy var captureButton: UIButton = {
let button = UIButton(type: .system)
button.backgroundColor = .white
button.tintColor = .black
button.layer.cornerRadius = 35
button.layer.borderWidth = 5
button.layer.borderColor = UIColor.lightGray.cgColor
button.addTarget(self, action: #selector(capturePhoto), for: .touchUpInside)
return button
}()
private lazy var closeButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(UIImage(systemName: "xmark"), for: .normal)
button.tintColor = .white
button.addTarget(self, action: #selector(closeCamera), for: .touchUpInside)
return button
}()
private lazy var flipButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(UIImage(systemName: "arrow.triangle.2.circlepath.camera"), for: .normal)
button.tintColor = .white
button.addTarget(self, action: #selector(switchCamera), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
checkCameraPermissions()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//
previewLayer?.frame = view.bounds
//
if let connection = previewLayer?.connection, connection.isVideoOrientationSupported {
connection.videoOrientation = .portrait
}
}
private func checkCameraPermissions() {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
setupCamera()
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
DispatchQueue.main.async {
if granted {
self?.setupCamera()
} else {
self?.delegate?.didCancel()
}
}
}
default:
delegate?.didCancel()
}
}
private func setupCamera() {
let session = AVCaptureSession()
session.sessionPreset = .high
// 使
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
delegate?.didCancel()
return
}
captureDevice = device
do {
let input = try AVCaptureDeviceInput(device: device)
if session.canAddInput(input) {
session.addInput(input)
}
let output = AVCapturePhotoOutput()
if session.canAddOutput(output) {
session.addOutput(output)
photoOutput = output
}
//
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.videoGravity = .resizeAspectFill
previewLayer.frame = view.bounds
previewLayer.connection?.videoOrientation = .portrait
//
view.layer.insertSublayer(previewLayer, at: 0)
self.previewLayer = previewLayer
DispatchQueue.global(qos: .userInitiated).async {
session.startRunning()
DispatchQueue.main.async {
self.setupUI()
}
}
captureSession = session
} catch {
print("Error setting up camera: \(error)")
delegate?.didCancel()
}
}
private func setupUI() {
view.bringSubviewToFront(closeButton)
view.bringSubviewToFront(flipButton)
view.bringSubviewToFront(captureButton)
view.addSubview(closeButton)
closeButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
closeButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
closeButton.widthAnchor.constraint(equalToConstant: 44),
closeButton.heightAnchor.constraint(equalToConstant: 44)
])
view.addSubview(flipButton)
flipButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
flipButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
flipButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
flipButton.widthAnchor.constraint(equalToConstant: 44),
flipButton.heightAnchor.constraint(equalToConstant: 44)
])
view.addSubview(captureButton)
captureButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
captureButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
captureButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -30),
captureButton.widthAnchor.constraint(equalToConstant: 70),
captureButton.heightAnchor.constraint(equalToConstant: 70)
])
}
@objc private func capturePhoto() {
let settings = AVCapturePhotoSettings()
photoOutput?.capturePhoto(with: settings, delegate: self)
}
@objc private func closeCamera() {
delegate?.didCancel()
}
@objc private func switchCamera() {
guard let currentInput = captureSession?.inputs.first as? AVCaptureDeviceInput else { return }
let newPosition: AVCaptureDevice.Position = currentInput.device.position == .front ? .back : .front
guard let newDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: newPosition) else { return }
do {
let newInput = try AVCaptureDeviceInput(device: newDevice)
captureSession?.beginConfiguration()
captureSession?.removeInput(currentInput)
if captureSession?.canAddInput(newInput) == true {
captureSession?.addInput(newInput)
captureDevice = newDevice
} else {
captureSession?.addInput(currentInput)
}
captureSession?.commitConfiguration()
} catch {
print("Error switching camera: \(error)")
}
}
}
extension CustomCameraViewController: AVCapturePhotoCaptureDelegate {
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
if let error = error {
print("Error capturing photo: \(error)")
return
}
guard let imageData = photo.fileDataRepresentation(),
let image = UIImage(data: imageData) else {
return
}
let fixedImage = image.fixedOrientation()
DispatchQueue.main.async {
self.delegate?.didCaptureImage(fixedImage)
}
}
}
extension UIImage {
func fixedOrientation() -> UIImage {
if imageOrientation == .up {
return self
}
UIGraphicsBeginImageContextWithOptions(size, false, scale)
draw(in: CGRect(origin: .zero, size: size))
let normalizedImage = UIGraphicsGetImageFromCurrentImageContext() ?? self
UIGraphicsEndImageContext()
return normalizedImage
}
}

View File

@ -32,6 +32,7 @@ public struct AvatarPicker: View {
}
public var body: some View {
VStack() {
VStack(spacing: 20) {
// Avatar Image Button
Button(action: {
@ -162,7 +163,7 @@ public struct AvatarPicker: View {
}
.frame(maxWidth: .infinity)
.sheet(isPresented: $showImageCapture) {
CameraView(isPresented: $showImageCapture) { image in
CustomCameraView(isPresented: $showImageCapture) { image in
selectedImage = image
uploadManager.selectedMedia = [.image(image)]
withAnimation {
@ -173,6 +174,7 @@ public struct AvatarPicker: View {
}
}
}
}
}
// Button style for scale effect