- Implement camera capture and plant identification workflow - Add Core Data persistence for plants, care schedules, and cached API data - Create collection view with grid/list layouts and filtering - Build plant detail views with care information display - Integrate Trefle botanical API for plant care data - Add local image storage for captured plant photos - Implement dependency injection container for testability - Include accessibility support throughout the app Bug fixes in this commit: - Fix Trefle API decoding by removing duplicate CodingKeys - Fix LocalCachedImage to load from correct PlantImages directory - Set dateAdded when saving plants for proper collection sorting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
114 lines
2.8 KiB
Swift
114 lines
2.8 KiB
Swift
//
|
|
// CameraPreviewView.swift
|
|
// PlantGuide
|
|
//
|
|
// Created by Trey Tartt on 1/21/26.
|
|
//
|
|
|
|
import SwiftUI
|
|
import AVFoundation
|
|
|
|
/// UIViewRepresentable wrapper for AVCaptureVideoPreviewLayer
|
|
struct CameraPreviewView: UIViewRepresentable {
|
|
let session: AVCaptureSession
|
|
|
|
func makeUIView(context: Context) -> CameraPreviewUIView {
|
|
let view = CameraPreviewUIView()
|
|
view.session = session
|
|
return view
|
|
}
|
|
|
|
func updateUIView(_ uiView: CameraPreviewUIView, context: Context) {
|
|
uiView.session = session
|
|
}
|
|
}
|
|
|
|
/// UIView subclass that hosts the AVCaptureVideoPreviewLayer
|
|
final class CameraPreviewUIView: UIView {
|
|
// MARK: - Properties
|
|
|
|
var session: AVCaptureSession? {
|
|
didSet {
|
|
previewLayer.session = session
|
|
}
|
|
}
|
|
|
|
private var previewLayer: AVCaptureVideoPreviewLayer {
|
|
guard let layer = layer as? AVCaptureVideoPreviewLayer else {
|
|
fatalError("Expected AVCaptureVideoPreviewLayer but got \(type(of: layer))")
|
|
}
|
|
return layer
|
|
}
|
|
|
|
// MARK: - Layer Class Override
|
|
|
|
override class var layerClass: AnyClass {
|
|
AVCaptureVideoPreviewLayer.self
|
|
}
|
|
|
|
// MARK: - Initialization
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
configurePreviewLayer()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
configurePreviewLayer()
|
|
}
|
|
|
|
// MARK: - Configuration
|
|
|
|
private func configurePreviewLayer() {
|
|
previewLayer.videoGravity = .resizeAspectFill
|
|
backgroundColor = .black
|
|
}
|
|
|
|
// MARK: - Layout
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
// Update preview layer connection orientation if needed
|
|
updatePreviewLayerOrientation()
|
|
}
|
|
|
|
private func updatePreviewLayerOrientation() {
|
|
guard let connection = previewLayer.connection,
|
|
connection.isVideoRotationAngleSupported(0) else {
|
|
return
|
|
}
|
|
|
|
// Get the current interface orientation
|
|
let windowScene = window?.windowScene
|
|
let interfaceOrientation = windowScene?.interfaceOrientation ?? .portrait
|
|
|
|
// Map interface orientation to video rotation angle
|
|
let rotationAngle: CGFloat
|
|
switch interfaceOrientation {
|
|
case .portrait:
|
|
rotationAngle = 90
|
|
case .portraitUpsideDown:
|
|
rotationAngle = 270
|
|
case .landscapeLeft:
|
|
rotationAngle = 180
|
|
case .landscapeRight:
|
|
rotationAngle = 0
|
|
@unknown default:
|
|
rotationAngle = 90
|
|
}
|
|
|
|
if connection.isVideoRotationAngleSupported(rotationAngle) {
|
|
connection.videoRotationAngle = rotationAngle
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview Provider
|
|
|
|
#Preview {
|
|
CameraPreviewView(session: AVCaptureSession())
|
|
.ignoresSafeArea()
|
|
}
|